effect-machine 0.10.0 → 0.11.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 (38) hide show
  1. package/README.md +65 -62
  2. package/dist/actor.d.ts +26 -94
  3. package/dist/actor.js +38 -95
  4. package/dist/index.d.ts +3 -7
  5. package/dist/index.js +1 -6
  6. package/dist/internal/transition.d.ts +1 -1
  7. package/dist/internal/transition.js +1 -1
  8. package/dist/internal/utils.js +1 -5
  9. package/dist/machine.d.ts +31 -35
  10. package/dist/machine.js +63 -13
  11. package/package.json +2 -2
  12. package/v3/dist/actor.d.ts +25 -93
  13. package/v3/dist/actor.js +37 -94
  14. package/v3/dist/index.d.ts +3 -7
  15. package/v3/dist/index.js +1 -6
  16. package/v3/dist/internal/utils.js +1 -5
  17. package/v3/dist/machine.d.ts +31 -35
  18. package/v3/dist/machine.js +63 -13
  19. package/dist/persistence/adapter.d.ts +0 -135
  20. package/dist/persistence/adapter.js +0 -25
  21. package/dist/persistence/adapters/in-memory.d.ts +0 -32
  22. package/dist/persistence/adapters/in-memory.js +0 -174
  23. package/dist/persistence/index.d.ts +0 -5
  24. package/dist/persistence/index.js +0 -5
  25. package/dist/persistence/persistent-actor.d.ts +0 -50
  26. package/dist/persistence/persistent-actor.js +0 -404
  27. package/dist/persistence/persistent-machine.d.ts +0 -105
  28. package/dist/persistence/persistent-machine.js +0 -22
  29. package/v3/dist/persistence/adapter.d.ts +0 -138
  30. package/v3/dist/persistence/adapter.js +0 -25
  31. package/v3/dist/persistence/adapters/in-memory.d.ts +0 -32
  32. package/v3/dist/persistence/adapters/in-memory.js +0 -174
  33. package/v3/dist/persistence/index.d.ts +0 -5
  34. package/v3/dist/persistence/index.js +0 -5
  35. package/v3/dist/persistence/persistent-actor.d.ts +0 -50
  36. package/v3/dist/persistence/persistent-actor.js +0 -404
  37. package/v3/dist/persistence/persistent-machine.d.ts +0 -105
  38. package/v3/dist/persistence/persistent-machine.js +0 -22
package/README.md CHANGED
@@ -208,7 +208,7 @@ machine
208
208
 
209
209
  ### Event Postpone
210
210
 
211
- `.postpone()` — gen_statem-style event postpone. When a matching event arrives in the given state, it is buffered. After the next state transition (tag change), buffered events drain in FIFO order:
211
+ `.postpone()` — gen_statem-style event postpone. When a matching event arrives in the given state, it is buffered. After the next state transition (tag change), buffered events drain in FIFO order, looping until stable:
212
212
 
213
213
  ```ts
214
214
  machine
@@ -257,6 +257,31 @@ const child = yield * parent.system.get("worker-1"); // Option<ActorRef>
257
257
 
258
258
  Every actor always has a system — `Machine.spawn` creates an implicit one if no `ActorSystem` is in context.
259
259
 
260
+ ### Persistence
261
+
262
+ Persistence is composed from primitives — no built-in adapter or framework:
263
+
264
+ ```ts
265
+ // Snapshot persistence — observe state changes, save externally
266
+ yield * actor.changes.pipe(Stream.runForEach((state) => saveSnapshot(actor.id, state)));
267
+
268
+ // Event journal — observe transitions
269
+ yield * actor.transitions.pipe(Stream.runForEach(({ event }) => appendEvent(actor.id, event)));
270
+
271
+ // Restore from snapshot
272
+ const savedState = yield * loadSnapshot(id);
273
+ const actor = yield * Machine.spawn(machine, { hydrate: savedState });
274
+
275
+ // Restore from event log
276
+ const events = yield * loadEvents(id);
277
+ const state = yield * Machine.replay(machine, events);
278
+ const actor = yield * Machine.spawn(machine, { hydrate: state });
279
+
280
+ // Restore from snapshot + tail events
281
+ const state = yield * Machine.replay(machine, tailEvents, { from: snapshot });
282
+ const actor = yield * Machine.spawn(machine, { hydrate: state });
283
+ ```
284
+
260
285
  ### System Observation
261
286
 
262
287
  React to actors joining and leaving the system:
@@ -296,21 +321,6 @@ expect(result.states.map((s) => s._tag)).toEqual(["Idle", "Loading", "Done"]);
296
321
  yield * assertPath(machine, events, ["Idle", "Loading", "Done"]);
297
322
  ```
298
323
 
299
- ## Documentation
300
-
301
- See the [primer](./primer/) for comprehensive documentation:
302
-
303
- | Topic | File | Description |
304
- | ----------- | ----------------------------------------- | ------------------------------ |
305
- | Overview | [index.md](./primer/index.md) | Navigation and quick reference |
306
- | Basics | [basics.md](./primer/basics.md) | Core concepts |
307
- | Handlers | [handlers.md](./primer/handlers.md) | Transitions, guards, reply |
308
- | Effects | [effects.md](./primer/effects.md) | spawn, background, timeouts |
309
- | Testing | [testing.md](./primer/testing.md) | simulate, harness, assertions |
310
- | Actors | [actors.md](./primer/actors.md) | ActorSystem, ActorRef |
311
- | Persistence | [persistence.md](./primer/persistence.md) | Snapshots, event sourcing |
312
- | Gotchas | [gotchas.md](./primer/gotchas.md) | Common mistakes |
313
-
314
324
  ## API Quick Reference
315
325
 
316
326
  ### Building
@@ -330,58 +340,41 @@ See the [primer](./primer/) for comprehensive documentation:
330
340
  | `.final(State.X)` | Mark final state |
331
341
  | `.build({ slot: impl })` | Provide implementations, returns `BuiltMachine` (terminal) |
332
342
  | `.build()` | Finalize no-slot machine, returns `BuiltMachine` (terminal) |
333
- | `.persist(config)` | Enable persistence |
334
-
335
- ### State Constructors
336
-
337
- | Method | Purpose |
338
- | -------------------------------------- | ------------------------------ |
339
- | `State.X.derive(source)` | Pick target fields from source |
340
- | `State.X.derive(source, { field: v })` | Pick fields + apply overrides |
341
- | `State.$is("X")(value)` | Type guard |
342
- | `State.$match(value, { X: fn, ... })` | Pattern matching |
343
343
 
344
344
  ### Running
345
345
 
346
- | Method | Purpose |
347
- | ---------------------------- | ------------------------------------------------------------------------------------------------------- |
348
- | `Machine.spawn(machine)` | Single actor, no registry. Caller manages lifetime via `actor.stop`. Auto-cleans up if `Scope` present. |
349
- | `Machine.spawn(machine, id)` | Same as above with custom ID |
350
- | `system.spawn(id, machine)` | Registry, lookup by ID, bulk ops, persistence. Cleans up on system teardown. |
351
-
352
- ### Testing
353
-
354
- | Function | Description |
355
- | ------------------------------------------ | ---------------------------------------------------------------- |
356
- | `simulate(machine, events)` | Run events, get all states (accepts `Machine` or `BuiltMachine`) |
357
- | `createTestHarness(machine)` | Step-by-step testing (accepts `Machine` or `BuiltMachine`) |
358
- | `assertPath(machine, events, path)` | Assert exact path |
359
- | `assertReaches(machine, events, tag)` | Assert final state |
360
- | `assertNeverReaches(machine, events, tag)` | Assert state never visited |
346
+ | Method | Purpose |
347
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- |
348
+ | `Machine.spawn(machine)` | Single actor, no registry. Caller manages lifetime via `actor.stop`. Auto-cleans up if `Scope` present. |
349
+ | `Machine.spawn(machine, id)` | Same as above with custom ID |
350
+ | `Machine.spawn(machine, { hydrate: s })` | Restore from saved state re-runs spawn effects for that state |
351
+ | `Machine.replay(machine, events)` | Fold events through handlers to compute state (for event sourcing restore) |
352
+ | `system.spawn(id, machine)` | Registry, lookup by ID, bulk ops. Cleans up on system teardown. |
361
353
 
362
354
  ### Actor
363
355
 
364
- | Method | Description |
365
- | -------------------------------- | ------------------------------------------- |
366
- | `actor.send(event)` | Fire-and-forget (queue event) |
367
- | `actor.cast(event)` | Alias for send (OTP gen_server:cast) |
368
- | `actor.call(event)` | Request-reply, returns `ProcessEventResult` |
369
- | `actor.ask<R>(event)` | Typed domain reply from handler |
370
- | `actor.snapshot` | Get current state |
371
- | `actor.matches(tag)` | Check state tag |
372
- | `actor.can(event)` | Can handle event? |
373
- | `actor.changes` | Stream of changes |
374
- | `actor.waitFor(State.X)` | Wait for state (constructor or fn) |
375
- | `actor.awaitFinal` | Wait final state |
376
- | `actor.sendAndWait(ev, State.X)` | Send + wait for state |
377
- | `actor.subscribe(fn)` | Sync callback |
378
- | `actor.sync.send(event)` | Sync fire-and-forget (for UI) |
379
- | `actor.sync.stop()` | Sync stop |
380
- | `actor.sync.snapshot()` | Sync get state |
381
- | `actor.sync.matches(tag)` | Sync check state tag |
382
- | `actor.sync.can(event)` | Sync can handle event? |
383
- | `actor.system` | Access the actor's `ActorSystem` |
384
- | `actor.children` | Child actors (`ReadonlyMap`) |
356
+ | Method | Description |
357
+ | -------------------------------- | ----------------------------------------------- |
358
+ | `actor.send(event)` | Fire-and-forget (queue event) |
359
+ | `actor.cast(event)` | Alias for send (OTP gen_server:cast) |
360
+ | `actor.call(event)` | Request-reply, returns `ProcessEventResult` |
361
+ | `actor.ask<R>(event)` | Typed domain reply from handler |
362
+ | `actor.snapshot` | Get current state |
363
+ | `actor.matches(tag)` | Check state tag |
364
+ | `actor.can(event)` | Can handle event? |
365
+ | `actor.changes` | Stream of state changes |
366
+ | `actor.transitions` | Stream of `{ fromState, toState, event }` edges |
367
+ | `actor.waitFor(State.X)` | Wait for state (constructor or fn) |
368
+ | `actor.awaitFinal` | Wait final state |
369
+ | `actor.sendAndWait(ev, State.X)` | Send + wait for state |
370
+ | `actor.subscribe(fn)` | Sync callback |
371
+ | `actor.sync.send(event)` | Sync fire-and-forget (for UI) |
372
+ | `actor.sync.stop()` | Sync stop |
373
+ | `actor.sync.snapshot()` | Sync get state |
374
+ | `actor.sync.matches(tag)` | Sync check state tag |
375
+ | `actor.sync.can(event)` | Sync can handle event? |
376
+ | `actor.system` | Access the actor's `ActorSystem` |
377
+ | `actor.children` | Child actors (`ReadonlyMap`) |
385
378
 
386
379
  ### ActorSystem
387
380
 
@@ -394,6 +387,16 @@ See the [primer](./primer/) for comprehensive documentation:
394
387
  | `system.subscribe(fn)` | Sync callback for spawn/stop events |
395
388
  | `system.events` | Async `Stream<SystemEvent>` for spawn/stop |
396
389
 
390
+ ### Testing
391
+
392
+ | Function | Description |
393
+ | ------------------------------------------ | ---------------------------------------------------------------- |
394
+ | `simulate(machine, events)` | Run events, get all states (accepts `Machine` or `BuiltMachine`) |
395
+ | `createTestHarness(machine)` | Step-by-step testing (accepts `Machine` or `BuiltMachine`) |
396
+ | `assertPath(machine, events, path)` | Assert exact path |
397
+ | `assertReaches(machine, events, tag)` | Assert final state |
398
+ | `assertNeverReaches(machine, events, tag)` | Assert state never visited |
399
+
397
400
  ## License
398
401
 
399
402
  MIT
package/dist/actor.d.ts CHANGED
@@ -1,11 +1,8 @@
1
1
  import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
2
- import { PersistentMachine } from "./persistence/persistent-machine.js";
3
2
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
3
  import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
5
- import { PersistentActorRef } from "./persistence/persistent-actor.js";
6
- import { ActorMetadata, PersistenceAdapterTag, PersistenceError, RestoreResult, VersionConflictError } from "./persistence/adapter.js";
7
4
  import { BuiltMachine, Machine, MachineRef } from "./machine.js";
8
- import { Deferred, Effect, Layer, Option, Queue, Ref, Scope, ServiceMap, Stream, SubscriptionRef } from "effect";
5
+ import { Deferred, Effect, Layer, Option, PubSub, Queue, Ref, Scope, ServiceMap, Stream, SubscriptionRef } from "effect";
9
6
  import * as effect_Tracer0 from "effect/Tracer";
10
7
 
11
8
  //#region src/actor.d.ts
@@ -39,6 +36,15 @@ interface ActorRefSync<State extends {
39
36
  readonly matches: (tag: State["_tag"]) => boolean;
40
37
  readonly can: (event: Event) => boolean;
41
38
  }
39
+ /**
40
+ * Information about a successful transition.
41
+ * Emitted on the `transitions` stream after each accepted event.
42
+ */
43
+ interface TransitionInfo<State, Event> {
44
+ readonly fromState: State;
45
+ readonly toState: State;
46
+ readonly event: Event;
47
+ }
42
48
  interface ActorRef<State extends {
43
49
  readonly _tag: string;
44
50
  }, Event> {
@@ -70,6 +76,14 @@ interface ActorRef<State extends {
70
76
  readonly can: (event: Event) => Effect.Effect<boolean>;
71
77
  /** Stream of state changes. */
72
78
  readonly changes: Stream.Stream<State>;
79
+ /**
80
+ * Stream of accepted transitions (edge stream).
81
+ *
82
+ * Emits `{ fromState, toState, event }` on every successful transition,
83
+ * including same-state reenters. PubSub-backed — late subscribers miss
84
+ * past edges. This is observational, not a durability guarantee.
85
+ */
86
+ readonly transitions: Stream.Stream<TransitionInfo<State, Event>>;
73
87
  /** Wait for a state matching predicate or variant (includes current snapshot). */
74
88
  readonly waitFor: {
75
89
  (predicate: (state: State) => boolean): Effect.Effect<State>;
@@ -123,54 +137,17 @@ interface ActorSystem {
123
137
  /**
124
138
  * Spawn a new actor with the given machine.
125
139
  *
126
- * For regular machines, returns ActorRef.
127
- * For persistent machines (created with Machine.persist), returns PersistentActorRef.
128
- *
129
- * All effect slots must be provided via `.build()` before spawning.
130
- *
131
140
  * @example
132
141
  * ```ts
133
- * // Regular machine (built)
134
142
  * const built = machine.build({ fetchData: ... })
135
143
  * const actor = yield* system.spawn("my-actor", built);
136
- *
137
- * // Persistent machine (auto-detected)
138
- * const persistentActor = yield* system.spawn("my-actor", persistentMachine);
139
- * persistentActor.persist; // available
140
- * persistentActor.version; // available
141
- * ```
142
- */
143
- readonly spawn: {
144
- <S extends {
145
- readonly _tag: string;
146
- }, E extends {
147
- readonly _tag: string;
148
- }, R>(id: string, machine: BuiltMachine<S, E, R>): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
149
- <S extends {
150
- readonly _tag: string;
151
- }, E extends {
152
- readonly _tag: string;
153
- }, R>(id: string, machine: PersistentMachine<S, E, R>): Effect.Effect<PersistentActorRef<S, E, R>, PersistenceError | VersionConflictError | DuplicateActorError, R | PersistenceAdapterTag>;
154
- };
155
- /**
156
- * Restore an actor from persistence.
157
- * Returns None if no persisted state exists for the given ID.
158
- *
159
- * @example
160
- * ```ts
161
- * const maybeActor = yield* system.restore("order-1", persistentMachine);
162
- * if (Option.isSome(maybeActor)) {
163
- * const actor = maybeActor.value;
164
- * const state = yield* actor.snapshot;
165
- * console.log(`Restored to state: ${state._tag}`);
166
- * }
167
144
  * ```
168
145
  */
169
- readonly restore: <S extends {
146
+ readonly spawn: <S extends {
170
147
  readonly _tag: string;
171
148
  }, E extends {
172
149
  readonly _tag: string;
173
- }, R>(id: string, machine: PersistentMachine<S, E, R>) => Effect.Effect<Option.Option<PersistentActorRef<S, E, R>>, PersistenceError | DuplicateActorError, R | PersistenceAdapterTag>;
150
+ }, R>(id: string, machine: BuiltMachine<S, E, R>) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
174
151
  /**
175
152
  * Get an existing actor by ID
176
153
  */
@@ -194,53 +171,6 @@ interface ActorSystem {
194
171
  * Returns an unsubscribe function.
195
172
  */
196
173
  readonly subscribe: (fn: SystemEventListener) => () => void;
197
- /**
198
- * List all persisted actor metadata.
199
- * Returns empty array if adapter doesn't support registry.
200
- *
201
- * @example
202
- * ```ts
203
- * const actors = yield* system.listPersisted();
204
- * for (const meta of actors) {
205
- * console.log(`${meta.id}: ${meta.stateTag} (v${meta.version})`);
206
- * }
207
- * ```
208
- */
209
- readonly listPersisted: () => Effect.Effect<ReadonlyArray<ActorMetadata>, PersistenceError, PersistenceAdapterTag>;
210
- /**
211
- * Restore multiple actors by ID.
212
- * Returns both successfully restored actors and failures.
213
- *
214
- * @example
215
- * ```ts
216
- * const result = yield* system.restoreMany(["order-1", "order-2"], orderMachine);
217
- * console.log(`Restored: ${result.restored.length}, Failed: ${result.failed.length}`);
218
- * ```
219
- */
220
- readonly restoreMany: <S extends {
221
- readonly _tag: string;
222
- }, E extends {
223
- readonly _tag: string;
224
- }, R>(ids: ReadonlyArray<string>, machine: PersistentMachine<S, E, R>) => Effect.Effect<RestoreResult<S, E, R>, never, R | PersistenceAdapterTag>;
225
- /**
226
- * Restore all persisted actors for a machine type.
227
- * Uses adapter registry if available, otherwise returns empty result.
228
- *
229
- * @example
230
- * ```ts
231
- * const result = yield* system.restoreAll(orderMachine, {
232
- * filter: (meta) => meta.stateTag !== "Done"
233
- * });
234
- * console.log(`Restored ${result.restored.length} active orders`);
235
- * ```
236
- */
237
- readonly restoreAll: <S extends {
238
- readonly _tag: string;
239
- }, E extends {
240
- readonly _tag: string;
241
- }, R>(machine: PersistentMachine<S, E, R>, options?: {
242
- filter?: (meta: ActorMetadata) => boolean;
243
- }) => Effect.Effect<RestoreResult<S, E, R>, PersistenceError, R | PersistenceAdapterTag>;
244
174
  }
245
175
  /**
246
176
  * ActorSystem service tag
@@ -253,13 +183,13 @@ type Listeners<S> = Set<(state: S) => void>;
253
183
  */
254
184
  declare const notifyListeners: <S>(listeners: Listeners<S>, state: S) => void;
255
185
  /**
256
- * Build core ActorRef methods shared between regular and persistent actors.
186
+ * Build core ActorRef methods.
257
187
  */
258
188
  declare const buildActorRefCore: <S extends {
259
189
  readonly _tag: string;
260
190
  }, E extends {
261
191
  readonly _tag: string;
262
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<QueuedEvent<E>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>) => ActorRef<S, E>;
192
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<QueuedEvent<E>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub?: PubSub.PubSub<TransitionInfo<S, E>>) => ActorRef<S, E>;
263
193
  /**
264
194
  * Create and start an actor for a machine
265
195
  */
@@ -267,7 +197,9 @@ declare const createActor: <S extends {
267
197
  readonly _tag: string;
268
198
  }, E extends {
269
199
  readonly _tag: string;
270
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) => Effect.Effect<ActorRef<S, E>, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, effect_Tracer0.ParentSpan> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope> | Exclude<Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>, effect_Tracer0.ParentSpan>>;
200
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, options?: {
201
+ initialState?: S;
202
+ } | undefined) => Effect.Effect<ActorRef<S, E>, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, effect_Tracer0.ParentSpan> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope> | Exclude<Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>, effect_Tracer0.ParentSpan>>;
271
203
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
272
204
  declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
273
205
  /**
@@ -275,4 +207,4 @@ declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unkno
275
207
  */
276
208
  declare const Default: Layer.Layer<ActorSystem, never, never>;
277
209
  //#endregion
278
- export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
210
+ export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
package/dist/actor.js CHANGED
@@ -1,11 +1,8 @@
1
1
  import { Inspector } from "./inspection.js";
2
2
  import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
3
3
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
- import { isPersistentMachine } from "./persistence/persistent-machine.js";
5
4
  import { emitWithTimestamp } from "./internal/inspection.js";
6
5
  import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
7
- import { PersistenceAdapterTag, PersistenceError } from "./persistence/adapter.js";
8
- import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
9
6
  import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
10
7
  //#region src/actor.ts
11
8
  /**
@@ -29,9 +26,9 @@ const notifyListeners = (listeners, state) => {
29
26
  } catch {}
30
27
  };
31
28
  /**
32
- * Build core ActorRef methods shared between regular and persistent actors.
29
+ * Build core ActorRef methods.
33
30
  */
34
- const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies) => {
31
+ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub) => {
35
32
  const send = Effect.fn("effect-machine.actor.send")(function* (event) {
36
33
  if (yield* Ref.get(stoppedRef)) return;
37
34
  yield* Queue.offer(eventQueue, {
@@ -119,6 +116,7 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
119
116
  matches,
120
117
  can,
121
118
  changes: SubscriptionRef.changes(stateRef),
119
+ transitions: transitionsPubSub !== void 0 ? Stream.fromPubSub(transitionsPubSub) : Stream.empty,
122
120
  waitFor,
123
121
  awaitFinal,
124
122
  sendAndWait,
@@ -149,7 +147,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
149
147
  /**
150
148
  * Create and start an actor for a machine
151
149
  */
152
- const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
150
+ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
151
+ const initial = options?.initialState ?? machine.initial;
153
152
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
154
153
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
155
154
  let system;
@@ -184,20 +183,20 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
184
183
  return child;
185
184
  })
186
185
  };
187
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
186
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
188
187
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
189
188
  type: "@machine.spawn",
190
189
  actorId: id,
191
- initialState: machine.initial,
190
+ initialState: initial,
192
191
  timestamp
193
192
  }));
194
- const stateRef = yield* SubscriptionRef.make(machine.initial);
193
+ const stateRef = yield* SubscriptionRef.make(initial);
195
194
  const listeners = /* @__PURE__ */ new Set();
196
195
  const backgroundFibers = [];
197
196
  const initEvent = { _tag: INTERNAL_INIT_EVENT };
198
197
  const ctx = {
199
198
  actorId: id,
200
- state: machine.initial,
199
+ state: initial,
201
200
  event: initEvent,
202
201
  self,
203
202
  system
@@ -206,7 +205,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
206
205
  for (const bg of machine.backgroundEffects) {
207
206
  const fiber = yield* Effect.forkDetach(bg.handler({
208
207
  actorId: id,
209
- state: machine.initial,
208
+ state: initial,
210
209
  event: initEvent,
211
210
  self,
212
211
  effects: effectSlots,
@@ -215,14 +214,14 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
215
214
  backgroundFibers.push(fiber);
216
215
  }
217
216
  const stateScopeRef = { current: yield* Scope.make() };
218
- yield* runSpawnEffectsWithInspection(machine, machine.initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
219
- if (machine.finalStates.has(machine.initial._tag)) {
217
+ yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
218
+ if (machine.finalStates.has(initial._tag)) {
220
219
  yield* Scope.close(stateScopeRef.current, Exit.void);
221
220
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
222
221
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
223
222
  type: "@machine.stop",
224
223
  actorId: id,
225
- finalState: machine.initial,
224
+ finalState: initial,
226
225
  timestamp
227
226
  }));
228
227
  yield* Ref.set(stoppedRef, true);
@@ -230,7 +229,8 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
230
229
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, /* @__PURE__ */ new Set());
231
230
  }
232
231
  const pendingReplies = /* @__PURE__ */ new Set();
233
- const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies));
232
+ const transitionsPubSub = yield* PubSub.unbounded();
233
+ const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub));
234
234
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
235
235
  const finalState = yield* SubscriptionRef.get(stateRef);
236
236
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
@@ -245,7 +245,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
245
245
  yield* Scope.close(stateScopeRef.current, Exit.void);
246
246
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
247
247
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
248
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies);
248
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
249
249
  });
250
250
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
251
251
  const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
@@ -258,7 +258,7 @@ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
258
258
  * Includes postpone buffer — events matching postpone rules are buffered
259
259
  * and drained after state tag changes (gen_statem semantics).
260
260
  */
261
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies) {
261
+ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub) {
262
262
  const postponed = [];
263
263
  const hasPostponeRules = machine.postponeRules.length > 0;
264
264
  const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
@@ -301,6 +301,11 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
301
301
  }));
302
302
  break;
303
303
  }
304
+ if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
305
+ fromState: result.previousState,
306
+ toState: result.newState,
307
+ event
308
+ });
304
309
  return {
305
310
  shouldStop,
306
311
  stateChanged: result.lifecycleRan
@@ -316,15 +321,21 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
316
321
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
317
322
  return;
318
323
  }
319
- if (stateChanged && postponed.length > 0) {
324
+ let drainTriggered = stateChanged;
325
+ while (drainTriggered && postponed.length > 0) {
326
+ drainTriggered = false;
320
327
  const drained = postponed.splice(0);
321
- for (const entry of drained) if ((yield* processQueued(entry)).shouldStop) {
322
- yield* Ref.set(stoppedRef, true);
323
- settlePostponedBuffer(postponed, pendingReplies, actorId);
324
- yield* settlePendingReplies(pendingReplies, actorId);
325
- yield* Scope.close(stateScopeRef.current, Exit.void);
326
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
327
- return;
328
+ for (const entry of drained) {
329
+ const drain = yield* processQueued(entry);
330
+ if (drain.shouldStop) {
331
+ yield* Ref.set(stoppedRef, true);
332
+ settlePostponedBuffer(postponed, pendingReplies, actorId);
333
+ yield* settlePendingReplies(pendingReplies, actorId);
334
+ yield* Scope.close(stateScopeRef.current, Exit.void);
335
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
336
+ return;
337
+ }
338
+ if (drain.stateChanged) drainTriggered = true;
328
339
  }
329
340
  }
330
341
  }
@@ -480,25 +491,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
480
491
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
481
492
  return yield* registerActor(id, yield* createActor(id, built._inner));
482
493
  });
483
- const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* (id, persistentMachine) {
484
- if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
485
- const adapter = yield* PersistenceAdapterTag;
486
- const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
487
- return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
488
- });
489
- const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
490
- if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
491
- return yield* spawnRegular(id, machine);
492
- });
493
- function spawn(id, machine) {
494
- return withSpawnGate(spawnImpl(id, machine));
495
- }
496
- const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
497
- const maybeActor = yield* restorePersistentActor(id, persistentMachine);
498
- if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
499
- return maybeActor;
500
- });
501
- const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
494
+ const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
502
495
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
503
496
  return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
504
497
  });
@@ -515,55 +508,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
515
508
  yield* actor.stop;
516
509
  return true;
517
510
  });
518
- const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
519
- const adapter = yield* PersistenceAdapterTag;
520
- if (adapter.listActors === void 0) return [];
521
- return yield* adapter.listActors();
522
- });
523
- const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
524
- const restored = [];
525
- const failed = [];
526
- for (const id of ids) {
527
- if (MutableHashMap.has(actorsMap, id)) continue;
528
- const result = yield* Effect.result(restore(id, persistentMachine));
529
- if (result._tag === "Failure") failed.push({
530
- id,
531
- error: result.failure
532
- });
533
- else if (Option.isSome(result.success)) restored.push(result.success.value);
534
- else failed.push({
535
- id,
536
- error: new PersistenceError({
537
- operation: "restore",
538
- actorId: id,
539
- message: "No persisted state found"
540
- })
541
- });
542
- }
543
- return {
544
- restored,
545
- failed
546
- };
547
- });
548
- const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
549
- const adapter = yield* PersistenceAdapterTag;
550
- if (adapter.listActors === void 0) return {
551
- restored: [],
552
- failed: []
553
- };
554
- const machineType = persistentMachine.persistence.machineType;
555
- if (machineType === void 0) return yield* new PersistenceError({
556
- operation: "restoreAll",
557
- actorId: "*",
558
- message: "restoreAll requires explicit machineType in persistence config"
559
- });
560
- let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
561
- if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
562
- return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
563
- });
564
511
  return ActorSystem.of({
565
512
  spawn,
566
- restore,
567
513
  get,
568
514
  stop,
569
515
  events: Stream.fromPubSub(eventPubSub),
@@ -579,10 +525,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
579
525
  return () => {
580
526
  eventListeners.delete(fn);
581
527
  };
582
- },
583
- listPersisted,
584
- restoreMany,
585
- restoreAll
528
+ }
586
529
  });
587
530
  });
588
531
  /**
package/dist/index.d.ts CHANGED
@@ -1,13 +1,9 @@
1
1
  import { EffectHandlers, EffectSlot, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlot, GuardSlots, GuardsDef, GuardsSchema, MachineContext, Slot } from "./slot.js";
2
2
  import { Event, MachineEventSchema, MachineStateSchema, State } from "./schema.js";
3
- import { PersistenceConfig, PersistentMachine, isPersistentMachine } from "./persistence/persistent-machine.js";
4
3
  import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
5
4
  import { ProcessEventResult } from "./internal/transition.js";
6
- import { PersistentActorRef, createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
7
- import { ActorMetadata, PersistedEvent, PersistenceAdapter, PersistenceAdapterTag, PersistenceError, RestoreFailure, RestoreResult, Snapshot, VersionConflictError } from "./persistence/adapter.js";
8
- import { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter } from "./persistence/adapters/in-memory.js";
9
- import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, ProvideHandlers, SpawnEffect, StateHandlerContext, TaskOptions, Transition, machine_d_exports } from "./machine.js";
10
- import { ActorRef, ActorRefSync, ActorSystem, Default, SystemEvent, SystemEventListener } from "./actor.js";
5
+ import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SpawnEffect, StateHandlerContext, TaskOptions, Transition, machine_d_exports } from "./machine.js";
6
+ import { ActorRef, ActorRefSync, ActorSystem, Default, SystemEvent, SystemEventListener, TransitionInfo } from "./actor.js";
11
7
  import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
12
8
  import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
13
- export { type ActorMetadata, type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, InMemoryPersistenceAdapter, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, type PersistOptions, type PersistedEvent, type PersistenceAdapter, PersistenceAdapterTag, type PersistenceConfig, PersistenceError, type PersistentActorRef, type PersistentMachine, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type RestoreFailure, type RestoreResult, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type Snapshot, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, makeInspectorEffect, restorePersistentActor, simulate, tracingInspector };
9
+ export { type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, UnprovidedSlotsError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };