effect-machine 0.3.0 → 0.3.2

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 (61) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +18 -0
  2. package/dist/actor.d.ts +251 -0
  3. package/dist/actor.js +385 -0
  4. package/dist/cluster/entity-machine.d.ts +90 -0
  5. package/dist/cluster/entity-machine.js +74 -0
  6. package/dist/cluster/index.d.ts +3 -0
  7. package/dist/cluster/index.js +4 -0
  8. package/dist/cluster/to-entity.d.ts +64 -0
  9. package/dist/cluster/to-entity.js +53 -0
  10. package/dist/errors.d.ts +61 -0
  11. package/dist/errors.js +38 -0
  12. package/dist/index.d.ts +13 -0
  13. package/dist/index.js +14 -0
  14. package/dist/inspection.d.ts +125 -0
  15. package/dist/inspection.js +50 -0
  16. package/dist/internal/brands.d.ts +40 -0
  17. package/dist/internal/brands.js +0 -0
  18. package/dist/internal/inspection.d.ts +11 -0
  19. package/dist/internal/inspection.js +15 -0
  20. package/dist/internal/transition.d.ts +159 -0
  21. package/dist/internal/transition.js +235 -0
  22. package/dist/internal/utils.d.ts +52 -0
  23. package/dist/internal/utils.js +31 -0
  24. package/dist/machine.d.ts +271 -0
  25. package/dist/machine.js +317 -0
  26. package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
  27. package/dist/persistence/adapter.js +27 -0
  28. package/dist/persistence/adapters/in-memory.d.ts +32 -0
  29. package/dist/persistence/adapters/in-memory.js +176 -0
  30. package/dist/persistence/index.d.ts +5 -0
  31. package/dist/persistence/index.js +6 -0
  32. package/dist/persistence/persistent-actor.d.ts +50 -0
  33. package/dist/persistence/persistent-actor.js +348 -0
  34. package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
  35. package/dist/persistence/persistent-machine.js +24 -0
  36. package/dist/schema.d.ts +141 -0
  37. package/dist/schema.js +165 -0
  38. package/dist/slot.d.ts +128 -0
  39. package/dist/slot.js +99 -0
  40. package/dist/testing.d.ts +142 -0
  41. package/dist/testing.js +131 -0
  42. package/package.json +18 -7
  43. package/src/actor.ts +0 -1050
  44. package/src/cluster/entity-machine.ts +0 -201
  45. package/src/cluster/index.ts +0 -43
  46. package/src/cluster/to-entity.ts +0 -99
  47. package/src/errors.ts +0 -64
  48. package/src/index.ts +0 -105
  49. package/src/inspection.ts +0 -178
  50. package/src/internal/brands.ts +0 -51
  51. package/src/internal/inspection.ts +0 -18
  52. package/src/internal/transition.ts +0 -489
  53. package/src/internal/utils.ts +0 -80
  54. package/src/machine.ts +0 -836
  55. package/src/persistence/adapters/in-memory.ts +0 -294
  56. package/src/persistence/index.ts +0 -24
  57. package/src/persistence/persistent-actor.ts +0 -791
  58. package/src/schema.ts +0 -362
  59. package/src/slot.ts +0 -281
  60. package/src/testing.ts +0 -284
  61. package/tsconfig.json +0 -65
package/src/actor.ts DELETED
@@ -1,1050 +0,0 @@
1
- /**
2
- * Actor system: spawning, lifecycle, and event processing.
3
- *
4
- * Combines:
5
- * - ActorRef interface (running actor handle)
6
- * - ActorSystem service (spawn/stop/get actors)
7
- * - Actor creation and event loop
8
- */
9
- import type { Stream } from "effect";
10
- import {
11
- Cause,
12
- Context,
13
- Deferred,
14
- Effect,
15
- Exit,
16
- Fiber,
17
- Layer,
18
- MutableHashMap,
19
- Option,
20
- Queue,
21
- Ref,
22
- Runtime,
23
- Scope,
24
- SubscriptionRef,
25
- } from "effect";
26
-
27
- import type { Machine, MachineRef, BuiltMachine } from "./machine.js";
28
- import type { Inspector } from "./inspection.js";
29
- import { Inspector as InspectorTag } from "./inspection.js";
30
- import { processEventCore, runSpawnEffects, resolveTransition } from "./internal/transition.js";
31
- import type { ProcessEventError, ProcessEventHooks } from "./internal/transition.js";
32
- import { emitWithTimestamp } from "./internal/inspection.js";
33
-
34
- // Re-export for external use (cluster, persistence)
35
- export { resolveTransition, runSpawnEffects, processEventCore } from "./internal/transition.js";
36
- export type {
37
- ProcessEventError,
38
- ProcessEventHooks,
39
- ProcessEventResult,
40
- } from "./internal/transition.js";
41
- import type { GuardsDef, EffectsDef } from "./slot.js";
42
- import { DuplicateActorError } from "./errors.js";
43
- import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
44
- import type {
45
- ActorMetadata,
46
- PersistenceError,
47
- RestoreResult,
48
- VersionConflictError,
49
- } from "./persistence/adapter.js";
50
- import {
51
- PersistenceAdapterTag,
52
- PersistenceError as PersistenceErrorClass,
53
- } from "./persistence/adapter.js";
54
- import type { PersistentMachine } from "./persistence/persistent-machine.js";
55
- import { isPersistentMachine } from "./persistence/persistent-machine.js";
56
- import type { PersistentActorRef } from "./persistence/persistent-actor.js";
57
- import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
58
-
59
- // ============================================================================
60
- // ActorRef Interface
61
- // ============================================================================
62
-
63
- /**
64
- * Reference to a running actor.
65
- */
66
- export interface ActorRef<State extends { readonly _tag: string }, Event> {
67
- /**
68
- * Unique identifier for this actor
69
- */
70
- readonly id: string;
71
-
72
- /**
73
- * Send an event to the actor
74
- */
75
- readonly send: (event: Event) => Effect.Effect<void>;
76
-
77
- /**
78
- * Observable state of the actor
79
- */
80
- readonly state: SubscriptionRef.SubscriptionRef<State>;
81
-
82
- /**
83
- * Stop the actor gracefully
84
- */
85
- readonly stop: Effect.Effect<void>;
86
-
87
- /**
88
- * Get current state snapshot (Effect)
89
- */
90
- readonly snapshot: Effect.Effect<State>;
91
-
92
- /**
93
- * Get current state snapshot (sync)
94
- */
95
- readonly snapshotSync: () => State;
96
-
97
- /**
98
- * Check if current state matches tag (Effect)
99
- */
100
- readonly matches: (tag: State["_tag"]) => Effect.Effect<boolean>;
101
-
102
- /**
103
- * Check if current state matches tag (sync)
104
- */
105
- readonly matchesSync: (tag: State["_tag"]) => boolean;
106
-
107
- /**
108
- * Check if event can be handled in current state (Effect)
109
- */
110
- readonly can: (event: Event) => Effect.Effect<boolean>;
111
-
112
- /**
113
- * Check if event can be handled in current state (sync)
114
- */
115
- readonly canSync: (event: Event) => boolean;
116
-
117
- /**
118
- * Stream of state changes
119
- */
120
- readonly changes: Stream.Stream<State>;
121
-
122
- /**
123
- * Wait for a state that matches predicate or state variant (includes current snapshot).
124
- * Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
125
- */
126
- readonly waitFor: {
127
- (predicate: (state: State) => boolean): Effect.Effect<State>;
128
- (state: { readonly _tag: State["_tag"] }): Effect.Effect<State>;
129
- };
130
-
131
- /**
132
- * Wait for a final state (includes current snapshot)
133
- */
134
- readonly awaitFinal: Effect.Effect<State>;
135
-
136
- /**
137
- * Send event and wait for predicate, state variant, or final state.
138
- * Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
139
- */
140
- readonly sendAndWait: {
141
- (event: Event, predicate: (state: State) => boolean): Effect.Effect<State>;
142
- (event: Event, state: { readonly _tag: State["_tag"] }): Effect.Effect<State>;
143
- (event: Event): Effect.Effect<State>;
144
- };
145
-
146
- /**
147
- * Send event synchronously (fire-and-forget).
148
- * No-op on stopped actors. Use when you need to send from sync contexts
149
- * (e.g. framework hooks, event handlers).
150
- */
151
- readonly sendSync: (event: Event) => void;
152
-
153
- /**
154
- * Subscribe to state changes (sync callback)
155
- * Returns unsubscribe function
156
- */
157
- readonly subscribe: (fn: (state: State) => void) => () => void;
158
- }
159
-
160
- // ============================================================================
161
- // ActorSystem Interface
162
- // ============================================================================
163
-
164
- /** Base type for stored actors (internal) */
165
- type AnyState = { readonly _tag: string };
166
-
167
- /**
168
- * Actor system for managing actor lifecycles
169
- */
170
- export interface ActorSystem {
171
- /**
172
- * Spawn a new actor with the given machine.
173
- *
174
- * For regular machines, returns ActorRef.
175
- * For persistent machines (created with Machine.persist), returns PersistentActorRef.
176
- *
177
- * All effect slots must be provided via `.build()` before spawning.
178
- *
179
- * @example
180
- * ```ts
181
- * // Regular machine (built)
182
- * const built = machine.build({ fetchData: ... })
183
- * const actor = yield* system.spawn("my-actor", built);
184
- *
185
- * // Persistent machine (auto-detected)
186
- * const persistentActor = yield* system.spawn("my-actor", persistentMachine);
187
- * persistentActor.persist; // available
188
- * persistentActor.version; // available
189
- * ```
190
- */
191
- readonly spawn: {
192
- // Regular machine overload (BuiltMachine)
193
- <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
194
- id: string,
195
- machine: BuiltMachine<S, E, R>,
196
- ): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
197
-
198
- // Persistent machine overload
199
- <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
200
- id: string,
201
- machine: PersistentMachine<S, E, R>,
202
- ): Effect.Effect<
203
- PersistentActorRef<S, E, R>,
204
- PersistenceError | VersionConflictError | DuplicateActorError,
205
- R | PersistenceAdapterTag
206
- >;
207
- };
208
-
209
- /**
210
- * Restore an actor from persistence.
211
- * Returns None if no persisted state exists for the given ID.
212
- *
213
- * @example
214
- * ```ts
215
- * const maybeActor = yield* system.restore("order-1", persistentMachine);
216
- * if (Option.isSome(maybeActor)) {
217
- * const actor = maybeActor.value;
218
- * const state = yield* actor.snapshot;
219
- * console.log(`Restored to state: ${state._tag}`);
220
- * }
221
- * ```
222
- */
223
- readonly restore: <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
224
- id: string,
225
- machine: PersistentMachine<S, E, R>,
226
- ) => Effect.Effect<
227
- Option.Option<PersistentActorRef<S, E, R>>,
228
- PersistenceError | DuplicateActorError,
229
- R | PersistenceAdapterTag
230
- >;
231
-
232
- /**
233
- * Get an existing actor by ID
234
- */
235
- readonly get: (id: string) => Effect.Effect<Option.Option<ActorRef<AnyState, unknown>>>;
236
-
237
- /**
238
- * Stop an actor by ID
239
- */
240
- readonly stop: (id: string) => Effect.Effect<boolean>;
241
-
242
- /**
243
- * List all persisted actor metadata.
244
- * Returns empty array if adapter doesn't support registry.
245
- *
246
- * @example
247
- * ```ts
248
- * const actors = yield* system.listPersisted();
249
- * for (const meta of actors) {
250
- * console.log(`${meta.id}: ${meta.stateTag} (v${meta.version})`);
251
- * }
252
- * ```
253
- */
254
- readonly listPersisted: () => Effect.Effect<
255
- ReadonlyArray<ActorMetadata>,
256
- PersistenceError,
257
- PersistenceAdapterTag
258
- >;
259
-
260
- /**
261
- * Restore multiple actors by ID.
262
- * Returns both successfully restored actors and failures.
263
- *
264
- * @example
265
- * ```ts
266
- * const result = yield* system.restoreMany(["order-1", "order-2"], orderMachine);
267
- * console.log(`Restored: ${result.restored.length}, Failed: ${result.failed.length}`);
268
- * ```
269
- */
270
- readonly restoreMany: <
271
- S extends { readonly _tag: string },
272
- E extends { readonly _tag: string },
273
- R,
274
- >(
275
- ids: ReadonlyArray<string>,
276
- machine: PersistentMachine<S, E, R>,
277
- ) => Effect.Effect<RestoreResult<S, E, R>, never, R | PersistenceAdapterTag>;
278
-
279
- /**
280
- * Restore all persisted actors for a machine type.
281
- * Uses adapter registry if available, otherwise returns empty result.
282
- *
283
- * @example
284
- * ```ts
285
- * const result = yield* system.restoreAll(orderMachine, {
286
- * filter: (meta) => meta.stateTag !== "Done"
287
- * });
288
- * console.log(`Restored ${result.restored.length} active orders`);
289
- * ```
290
- */
291
- readonly restoreAll: <
292
- S extends { readonly _tag: string },
293
- E extends { readonly _tag: string },
294
- R,
295
- >(
296
- machine: PersistentMachine<S, E, R>,
297
- options?: { filter?: (meta: ActorMetadata) => boolean },
298
- ) => Effect.Effect<RestoreResult<S, E, R>, PersistenceError, R | PersistenceAdapterTag>;
299
- }
300
-
301
- /**
302
- * ActorSystem service tag
303
- */
304
- export const ActorSystem = Context.GenericTag<ActorSystem>("@effect/machine/ActorSystem");
305
-
306
- // ============================================================================
307
- // Actor Core Helpers
308
- // ============================================================================
309
-
310
- /** Listener set for sync subscriptions */
311
- export type Listeners<S> = Set<(state: S) => void>;
312
-
313
- /**
314
- * Notify all listeners of state change.
315
- */
316
- export const notifyListeners = <S>(listeners: Listeners<S>, state: S): void => {
317
- for (const listener of listeners) {
318
- try {
319
- listener(state);
320
- } catch {
321
- // Ignore listener failures to avoid crashing the actor loop
322
- }
323
- }
324
- };
325
-
326
- /**
327
- * Build core ActorRef methods shared between regular and persistent actors.
328
- */
329
- export const buildActorRefCore = <
330
- S extends { readonly _tag: string },
331
- E extends { readonly _tag: string },
332
- R,
333
- GD extends GuardsDef,
334
- EFD extends EffectsDef,
335
- >(
336
- id: string,
337
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
338
- machine: Machine<S, E, R, any, any, GD, EFD>,
339
- stateRef: SubscriptionRef.SubscriptionRef<S>,
340
- eventQueue: Queue.Queue<E>,
341
- stoppedRef: Ref.Ref<boolean>,
342
- listeners: Listeners<S>,
343
- stop: Effect.Effect<void>,
344
- ): ActorRef<S, E> => {
345
- const send = Effect.fn("effect-machine.actor.send")(function* (event: E) {
346
- const stopped = yield* Ref.get(stoppedRef);
347
- if (stopped) {
348
- return;
349
- }
350
- yield* Queue.offer(eventQueue, event);
351
- });
352
-
353
- const snapshot = SubscriptionRef.get(stateRef).pipe(
354
- Effect.withSpan("effect-machine.actor.snapshot"),
355
- );
356
-
357
- const matches = Effect.fn("effect-machine.actor.matches")(function* (tag: S["_tag"]) {
358
- const state = yield* SubscriptionRef.get(stateRef);
359
- return state._tag === tag;
360
- });
361
-
362
- const can = Effect.fn("effect-machine.actor.can")(function* (event: E) {
363
- const state = yield* SubscriptionRef.get(stateRef);
364
- return resolveTransition(machine, state, event) !== undefined;
365
- });
366
-
367
- const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (
368
- predicateOrState: ((state: S) => boolean) | { readonly _tag: S["_tag"] },
369
- ) {
370
- const predicate =
371
- typeof predicateOrState === "function" && !("_tag" in predicateOrState)
372
- ? predicateOrState
373
- : (s: S) => s._tag === (predicateOrState as { readonly _tag: string })._tag;
374
-
375
- // Check current state first — SubscriptionRef.get acquires/releases
376
- // the semaphore quickly (read-only), no deadlock risk.
377
- const current = yield* SubscriptionRef.get(stateRef);
378
- if (predicate(current)) return current;
379
-
380
- // Use sync listener + Deferred to avoid holding the SubscriptionRef
381
- // semaphore for the duration of a stream (which causes deadlock when
382
- // send triggers SubscriptionRef.set concurrently).
383
- const done = yield* Deferred.make<S>();
384
- const rt = yield* Effect.runtime<never>();
385
- const runFork = Runtime.runFork(rt);
386
- const listener = (state: S) => {
387
- if (predicate(state)) {
388
- runFork(Deferred.succeed(done, state));
389
- }
390
- };
391
- listeners.add(listener);
392
-
393
- // Re-check after subscribing to close the race window
394
- const afterSubscribe = yield* SubscriptionRef.get(stateRef);
395
- if (predicate(afterSubscribe)) {
396
- listeners.delete(listener);
397
- return afterSubscribe;
398
- }
399
-
400
- const result = yield* Deferred.await(done);
401
- listeners.delete(listener);
402
- return result;
403
- });
404
-
405
- const awaitFinal = waitFor((state) => machine.finalStates.has(state._tag)).pipe(
406
- Effect.withSpan("effect-machine.actor.awaitFinal"),
407
- );
408
-
409
- const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (
410
- event: E,
411
- predicateOrState?: ((state: S) => boolean) | { readonly _tag: S["_tag"] },
412
- ) {
413
- yield* send(event);
414
- if (predicateOrState !== undefined) {
415
- return yield* waitFor(predicateOrState);
416
- }
417
- return yield* awaitFinal;
418
- });
419
-
420
- return {
421
- id,
422
- send,
423
- state: stateRef,
424
- stop,
425
- snapshot,
426
- snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
427
- matches,
428
- matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
429
- can,
430
- canSync: (event) => {
431
- const state = Effect.runSync(SubscriptionRef.get(stateRef));
432
- return resolveTransition(machine, state, event) !== undefined;
433
- },
434
- changes: stateRef.changes,
435
- waitFor,
436
- awaitFinal,
437
- sendAndWait,
438
- sendSync: (event) => {
439
- const stopped = Effect.runSync(Ref.get(stoppedRef));
440
- if (!stopped) {
441
- Effect.runSync(Queue.offer(eventQueue, event));
442
- }
443
- },
444
- subscribe: (fn) => {
445
- listeners.add(fn);
446
- return () => {
447
- listeners.delete(fn);
448
- };
449
- },
450
- };
451
- };
452
-
453
- // ============================================================================
454
- // Actor Creation and Event Loop
455
- // ============================================================================
456
-
457
- /**
458
- * Create and start an actor for a machine
459
- */
460
- export const createActor = Effect.fn("effect-machine.actor.spawn")(function* <
461
- S extends { readonly _tag: string },
462
- E extends { readonly _tag: string },
463
- R,
464
- GD extends GuardsDef,
465
- EFD extends EffectsDef,
466
- >(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) {
467
- yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
468
-
469
- // Get optional inspector from context
470
- const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(InspectorTag)) as
471
- | Inspector<S, E>
472
- | undefined;
473
-
474
- // Create self reference for sending events
475
- const eventQueue = yield* Queue.unbounded<E>();
476
- const stoppedRef = yield* Ref.make(false);
477
- const self: MachineRef<E> = {
478
- send: Effect.fn("effect-machine.actor.self.send")(function* (event: E) {
479
- const stopped = yield* Ref.get(stoppedRef);
480
- if (stopped) {
481
- return;
482
- }
483
- yield* Queue.offer(eventQueue, event);
484
- }),
485
- };
486
-
487
- // Annotate span with initial state
488
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
489
-
490
- // Emit spawn event
491
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
492
- type: "@machine.spawn",
493
- actorId: id,
494
- initialState: machine.initial,
495
- timestamp,
496
- }));
497
-
498
- // Initialize state
499
- const stateRef = yield* SubscriptionRef.make(machine.initial);
500
- const listeners: Listeners<S> = new Set();
501
-
502
- // Fork background effects (run for entire machine lifetime)
503
- const backgroundFibers: Fiber.Fiber<void, never>[] = [];
504
- const initEvent = { _tag: INTERNAL_INIT_EVENT } as E;
505
- const ctx = { state: machine.initial, event: initEvent, self };
506
- const { effects: effectSlots } = machine._slots;
507
-
508
- for (const bg of machine.backgroundEffects) {
509
- const fiber = yield* Effect.forkDaemon(
510
- bg
511
- .handler({ state: machine.initial, event: initEvent, self, effects: effectSlots })
512
- .pipe(Effect.provideService(machine.Context, ctx)),
513
- );
514
- backgroundFibers.push(fiber);
515
- }
516
-
517
- // Create state scope for initial state's spawn effects
518
- const stateScopeRef: { current: Scope.CloseableScope } = {
519
- current: yield* Scope.make(),
520
- };
521
-
522
- // Run initial spawn effects
523
- yield* runSpawnEffectsWithInspection(
524
- machine,
525
- machine.initial,
526
- initEvent,
527
- self,
528
- stateScopeRef.current,
529
- id,
530
- inspectorValue,
531
- );
532
-
533
- // Check if initial state (after always) is final
534
- if (machine.finalStates.has(machine.initial._tag)) {
535
- // Close state scope and interrupt background effects
536
- yield* Scope.close(stateScopeRef.current, Exit.void);
537
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
538
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
539
- type: "@machine.stop",
540
- actorId: id,
541
- finalState: machine.initial,
542
- timestamp,
543
- }));
544
- yield* Ref.set(stoppedRef, true);
545
- const stop = Ref.set(stoppedRef, true).pipe(
546
- Effect.withSpan("effect-machine.actor.stop"),
547
- Effect.asVoid,
548
- );
549
- return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, stop);
550
- }
551
-
552
- // Start the event loop — use forkDaemon so the event loop fiber's lifetime
553
- // is detached from any parent scope/fiber. actor.stop handles cleanup.
554
- const loopFiber = yield* Effect.forkDaemon(
555
- eventLoop(
556
- machine,
557
- stateRef,
558
- eventQueue,
559
- stoppedRef,
560
- self,
561
- listeners,
562
- backgroundFibers,
563
- stateScopeRef,
564
- id,
565
- inspectorValue,
566
- ),
567
- );
568
-
569
- const stop = Effect.gen(function* () {
570
- const finalState = yield* SubscriptionRef.get(stateRef);
571
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
572
- type: "@machine.stop",
573
- actorId: id,
574
- finalState,
575
- timestamp,
576
- }));
577
- yield* Ref.set(stoppedRef, true);
578
- yield* Fiber.interrupt(loopFiber);
579
- // Close state scope (interrupts spawn fibers)
580
- yield* Scope.close(stateScopeRef.current, Exit.void);
581
- // Interrupt background effects (in parallel)
582
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
583
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid);
584
-
585
- return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, stop);
586
- });
587
-
588
- /**
589
- * Main event loop for the actor
590
- */
591
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* <
592
- S extends { readonly _tag: string },
593
- E extends { readonly _tag: string },
594
- R,
595
- GD extends GuardsDef,
596
- EFD extends EffectsDef,
597
- >(
598
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
599
- stateRef: SubscriptionRef.SubscriptionRef<S>,
600
- eventQueue: Queue.Queue<E>,
601
- stoppedRef: Ref.Ref<boolean>,
602
- self: MachineRef<E>,
603
- listeners: Listeners<S>,
604
- backgroundFibers: Fiber.Fiber<void, never>[],
605
- stateScopeRef: { current: Scope.CloseableScope },
606
- actorId: string,
607
- inspector?: Inspector<S, E>,
608
- ) {
609
- while (true) {
610
- // Block waiting for next event - will fail with QueueShutdown when queue is shut down
611
- const event = yield* Queue.take(eventQueue);
612
-
613
- const currentState = yield* SubscriptionRef.get(stateRef);
614
-
615
- // Process event in a span
616
- const shouldStop = yield* Effect.withSpan("effect-machine.event.process", {
617
- attributes: {
618
- "effect_machine.actor.id": actorId,
619
- "effect_machine.state.current": currentState._tag,
620
- "effect_machine.event.type": event._tag,
621
- },
622
- })(
623
- processEvent(
624
- machine,
625
- currentState,
626
- event,
627
- stateRef,
628
- self,
629
- listeners,
630
- stateScopeRef,
631
- actorId,
632
- inspector,
633
- ),
634
- );
635
-
636
- if (shouldStop) {
637
- // Close state scope and interrupt background effects when reaching final state
638
- yield* Ref.set(stoppedRef, true);
639
- yield* Scope.close(stateScopeRef.current, Exit.void);
640
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
641
- return;
642
- }
643
- }
644
- });
645
-
646
- /**
647
- * Process a single event, returning true if the actor should stop.
648
- * Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
649
- */
650
- const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* <
651
- S extends { readonly _tag: string },
652
- E extends { readonly _tag: string },
653
- R,
654
- GD extends GuardsDef,
655
- EFD extends EffectsDef,
656
- >(
657
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
658
- currentState: S,
659
- event: E,
660
- stateRef: SubscriptionRef.SubscriptionRef<S>,
661
- self: MachineRef<E>,
662
- listeners: Listeners<S>,
663
- stateScopeRef: { current: Scope.CloseableScope },
664
- actorId: string,
665
- inspector?: Inspector<S, E>,
666
- ) {
667
- // Emit event received
668
- yield* emitWithTimestamp(inspector, (timestamp) => ({
669
- type: "@machine.event",
670
- actorId,
671
- state: currentState,
672
- event,
673
- timestamp,
674
- }));
675
-
676
- // Build inspection hooks for processEventCore
677
- const hooks: ProcessEventHooks<S, E> | undefined =
678
- inspector === undefined
679
- ? undefined
680
- : {
681
- onSpawnEffect: (state) =>
682
- emitWithTimestamp(inspector, (timestamp) => ({
683
- type: "@machine.effect",
684
- actorId,
685
- effectType: "spawn",
686
- state,
687
- timestamp,
688
- })),
689
- onTransition: (from, to, ev) =>
690
- emitWithTimestamp(inspector, (timestamp) => ({
691
- type: "@machine.transition",
692
- actorId,
693
- fromState: from,
694
- toState: to,
695
- event: ev,
696
- timestamp,
697
- })),
698
- onError: (info) =>
699
- emitWithTimestamp(inspector, (timestamp) => ({
700
- type: "@machine.error",
701
- actorId,
702
- phase: info.phase,
703
- state: info.state,
704
- event: info.event,
705
- error: Cause.pretty(info.cause),
706
- timestamp,
707
- })),
708
- };
709
-
710
- // Process event using shared core
711
- const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, hooks);
712
-
713
- if (!result.transitioned) {
714
- yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
715
- return false;
716
- }
717
-
718
- yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
719
-
720
- // Update state ref and notify listeners
721
- yield* SubscriptionRef.set(stateRef, result.newState);
722
- notifyListeners(listeners, result.newState);
723
-
724
- if (result.lifecycleRan) {
725
- yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
726
- yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
727
-
728
- // Transition inspection event emitted via hooks in processEventCore
729
-
730
- // Check if new state is final
731
- if (result.isFinal) {
732
- yield* emitWithTimestamp(inspector, (timestamp) => ({
733
- type: "@machine.stop",
734
- actorId,
735
- finalState: result.newState,
736
- timestamp,
737
- }));
738
- return true;
739
- }
740
- }
741
-
742
- return false;
743
- });
744
-
745
- /**
746
- * Run spawn effects with actor-specific inspection and tracing.
747
- * Wraps the core runSpawnEffects with inspection events and spans.
748
- * @internal
749
- */
750
- const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* <
751
- S extends { readonly _tag: string },
752
- E extends { readonly _tag: string },
753
- R,
754
- GD extends GuardsDef,
755
- EFD extends EffectsDef,
756
- >(
757
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
758
- state: S,
759
- event: E,
760
- self: MachineRef<E>,
761
- stateScope: Scope.CloseableScope,
762
- actorId: string,
763
- inspector?: Inspector<S, E>,
764
- ) {
765
- // Emit inspection event before running effects
766
- yield* emitWithTimestamp(inspector, (timestamp) => ({
767
- type: "@machine.effect",
768
- actorId,
769
- effectType: "spawn",
770
- state,
771
- timestamp,
772
- }));
773
-
774
- // Use shared core
775
- const onError =
776
- inspector === undefined
777
- ? undefined
778
- : (info: ProcessEventError<S, E>) =>
779
- emitWithTimestamp(inspector, (timestamp) => ({
780
- type: "@machine.error",
781
- actorId,
782
- phase: info.phase,
783
- state: info.state,
784
- event: info.event,
785
- error: Cause.pretty(info.cause),
786
- timestamp,
787
- }));
788
-
789
- yield* runSpawnEffects(machine, state, event, self, stateScope, onError);
790
- });
791
-
792
- // ============================================================================
793
- // ActorSystem Implementation
794
- // ============================================================================
795
-
796
- /**
797
- * Internal implementation
798
- */
799
- const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
800
- // MutableHashMap for O(1) spawn/stop/get operations
801
- const actors = MutableHashMap.empty<string, ActorRef<AnyState, unknown>>();
802
- const spawnGate = yield* Effect.makeSemaphore(1);
803
- const withSpawnGate = spawnGate.withPermits(1);
804
-
805
- // Stop all actors on system teardown
806
- yield* Effect.addFinalizer(() => {
807
- const stops: Effect.Effect<void>[] = [];
808
- MutableHashMap.forEach(actors, (actor) => {
809
- stops.push(actor.stop);
810
- });
811
- return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.asVoid);
812
- });
813
-
814
- /** Check for duplicate ID, register actor, attach scope cleanup if available */
815
- const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* <
816
- T extends { stop: Effect.Effect<void> },
817
- >(id: string, actor: T) {
818
- // Check if actor already exists
819
- if (MutableHashMap.has(actors, id)) {
820
- // Stop the newly created actor to avoid leaks
821
- yield* actor.stop;
822
- return yield* new DuplicateActorError({ actorId: id });
823
- }
824
-
825
- // Register it - O(1)
826
- MutableHashMap.set(actors, id, actor as unknown as ActorRef<AnyState, unknown>);
827
-
828
- // If scope available, attach per-actor cleanup
829
- const maybeScope = yield* Effect.serviceOption(Scope.Scope);
830
- if (Option.isSome(maybeScope)) {
831
- yield* Scope.addFinalizer(
832
- maybeScope.value,
833
- Effect.gen(function* () {
834
- yield* actor.stop;
835
- MutableHashMap.remove(actors, id);
836
- }),
837
- );
838
- }
839
-
840
- return actor;
841
- });
842
-
843
- const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* <
844
- S extends { readonly _tag: string },
845
- E extends { readonly _tag: string },
846
- R,
847
- >(id: string, built: BuiltMachine<S, E, R>) {
848
- if (MutableHashMap.has(actors, id)) {
849
- return yield* new DuplicateActorError({ actorId: id });
850
- }
851
- // Create and register the actor
852
- const actor = yield* createActor(id, built._inner);
853
- return yield* registerActor(id, actor);
854
- });
855
-
856
- const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* <
857
- S extends { readonly _tag: string },
858
- E extends { readonly _tag: string },
859
- R,
860
- >(id: string, persistentMachine: PersistentMachine<S, E, R>) {
861
- if (MutableHashMap.has(actors, id)) {
862
- return yield* new DuplicateActorError({ actorId: id });
863
- }
864
- const adapter = yield* PersistenceAdapterTag;
865
-
866
- // Try to load existing snapshot
867
- const maybeSnapshot = yield* adapter.loadSnapshot(
868
- id,
869
- persistentMachine.persistence.stateSchema,
870
- );
871
-
872
- // Load events after snapshot (or all events if no snapshot)
873
- const events = yield* adapter.loadEvents(
874
- id,
875
- persistentMachine.persistence.eventSchema,
876
- Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : undefined,
877
- );
878
-
879
- // Create and register the persistent actor
880
- const actor = yield* createPersistentActor(id, persistentMachine, maybeSnapshot, events);
881
- return yield* registerActor(id, actor);
882
- });
883
-
884
- const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* <
885
- S extends { readonly _tag: string },
886
- E extends { readonly _tag: string },
887
- R,
888
- >(id: string, machine: BuiltMachine<S, E, R> | PersistentMachine<S, E, R>) {
889
- if (isPersistentMachine(machine)) {
890
- // TypeScript can't narrow union with invariant generic params
891
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
892
- return yield* spawnPersistent(id, machine as PersistentMachine<S, E, R>);
893
- }
894
- return yield* spawnRegular(id, machine as BuiltMachine<S, E, R>);
895
- });
896
-
897
- // Type-safe overloaded spawn implementation
898
- function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
899
- id: string,
900
- machine: BuiltMachine<S, E, R>,
901
- ): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
902
- function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
903
- id: string,
904
- machine: PersistentMachine<S, E, R>,
905
- ): Effect.Effect<
906
- PersistentActorRef<S, E, R>,
907
- PersistenceError | VersionConflictError | DuplicateActorError,
908
- R | PersistenceAdapterTag
909
- >;
910
- function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
911
- id: string,
912
- machine: BuiltMachine<S, E, R> | PersistentMachine<S, E, R>,
913
- ):
914
- | Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>
915
- | Effect.Effect<
916
- PersistentActorRef<S, E, R>,
917
- PersistenceError | VersionConflictError | DuplicateActorError,
918
- R | PersistenceAdapterTag
919
- > {
920
- return withSpawnGate(spawnImpl(id, machine)) as
921
- | Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>
922
- | Effect.Effect<
923
- PersistentActorRef<S, E, R>,
924
- PersistenceError | VersionConflictError | DuplicateActorError,
925
- R | PersistenceAdapterTag
926
- >;
927
- }
928
-
929
- const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* <
930
- S extends { readonly _tag: string },
931
- E extends { readonly _tag: string },
932
- R,
933
- >(id: string, persistentMachine: PersistentMachine<S, E, R>) {
934
- // Try to restore from persistence
935
- const maybeActor = yield* restorePersistentActor(id, persistentMachine);
936
-
937
- if (Option.isSome(maybeActor)) {
938
- yield* registerActor(id, maybeActor.value);
939
- }
940
-
941
- return maybeActor;
942
- });
943
- const restore = <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
944
- id: string,
945
- persistentMachine: PersistentMachine<S, E, R>,
946
- ) => withSpawnGate(restoreImpl(id, persistentMachine));
947
-
948
- const get = Effect.fn("effect-machine.actorSystem.get")(function* (id: string) {
949
- return yield* Effect.sync(() => MutableHashMap.get(actors, id));
950
- });
951
-
952
- const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (id: string) {
953
- const maybeActor = MutableHashMap.get(actors, id);
954
- if (Option.isNone(maybeActor)) {
955
- return false;
956
- }
957
-
958
- yield* maybeActor.value.stop;
959
- MutableHashMap.remove(actors, id);
960
- return true;
961
- });
962
-
963
- const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
964
- const adapter = yield* PersistenceAdapterTag;
965
- if (adapter.listActors === undefined) {
966
- return [];
967
- }
968
- return yield* adapter.listActors();
969
- });
970
-
971
- const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* <
972
- S extends { readonly _tag: string },
973
- E extends { readonly _tag: string },
974
- R,
975
- >(ids: ReadonlyArray<string>, persistentMachine: PersistentMachine<S, E, R>) {
976
- const restored: PersistentActorRef<S, E, R>[] = [];
977
- const failed: {
978
- id: string;
979
- error: PersistenceError | DuplicateActorError;
980
- }[] = [];
981
-
982
- for (const id of ids) {
983
- // Skip if already running
984
- if (MutableHashMap.has(actors, id)) {
985
- continue;
986
- }
987
-
988
- const result = yield* Effect.either(restore(id, persistentMachine));
989
- if (result._tag === "Left") {
990
- failed.push({ id, error: result.left });
991
- } else if (Option.isSome(result.right)) {
992
- restored.push(result.right.value);
993
- } else {
994
- // No persisted state for this ID
995
- failed.push({
996
- id,
997
- error: new PersistenceErrorClass({
998
- operation: "restore",
999
- actorId: id,
1000
- message: "No persisted state found",
1001
- }),
1002
- });
1003
- }
1004
- }
1005
-
1006
- return { restored, failed };
1007
- });
1008
-
1009
- const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* <
1010
- S extends { readonly _tag: string },
1011
- E extends { readonly _tag: string },
1012
- R,
1013
- >(
1014
- persistentMachine: PersistentMachine<S, E, R>,
1015
- options?: { filter?: (meta: ActorMetadata) => boolean },
1016
- ) {
1017
- const adapter = yield* PersistenceAdapterTag;
1018
- if (adapter.listActors === undefined) {
1019
- return { restored: [], failed: [] };
1020
- }
1021
-
1022
- // Require explicit machineType to prevent cross-machine restores
1023
- const machineType = persistentMachine.persistence.machineType;
1024
- if (machineType === undefined) {
1025
- return yield* new PersistenceErrorClass({
1026
- operation: "restoreAll",
1027
- actorId: "*",
1028
- message: "restoreAll requires explicit machineType in persistence config",
1029
- });
1030
- }
1031
-
1032
- const allMetadata = yield* adapter.listActors();
1033
-
1034
- // Filter by machineType and optional user filter
1035
- let filtered = allMetadata.filter((meta) => meta.machineType === machineType);
1036
- if (options?.filter !== undefined) {
1037
- filtered = filtered.filter(options.filter);
1038
- }
1039
-
1040
- const ids = filtered.map((meta) => meta.id);
1041
- return yield* restoreMany(ids, persistentMachine);
1042
- });
1043
-
1044
- return ActorSystem.of({ spawn, restore, get, stop, listPersisted, restoreMany, restoreAll });
1045
- });
1046
-
1047
- /**
1048
- * Default ActorSystem layer
1049
- */
1050
- export const Default = Layer.scoped(ActorSystem, make());