effect-machine 0.8.0 → 0.10.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 (76) hide show
  1. package/README.md +76 -16
  2. package/dist/_virtual/_rolldown/runtime.js +6 -11
  3. package/dist/actor.d.ts +58 -72
  4. package/dist/actor.js +166 -32
  5. package/dist/cluster/entity-machine.d.ts +0 -1
  6. package/dist/cluster/entity-machine.js +6 -6
  7. package/dist/cluster/index.js +1 -2
  8. package/dist/cluster/to-entity.js +1 -3
  9. package/dist/errors.d.ts +12 -1
  10. package/dist/errors.js +8 -3
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +2 -3
  13. package/dist/inspection.js +1 -3
  14. package/dist/internal/inspection.js +1 -3
  15. package/dist/internal/transition.d.ts +26 -2
  16. package/dist/internal/transition.js +37 -10
  17. package/dist/internal/utils.d.ts +7 -2
  18. package/dist/internal/utils.js +1 -3
  19. package/dist/machine.d.ts +66 -4
  20. package/dist/machine.js +67 -31
  21. package/dist/persistence/adapter.js +1 -3
  22. package/dist/persistence/adapters/in-memory.js +1 -3
  23. package/dist/persistence/index.js +1 -2
  24. package/dist/persistence/persistent-actor.js +54 -19
  25. package/dist/persistence/persistent-machine.js +1 -3
  26. package/dist/schema.js +1 -3
  27. package/dist/slot.js +1 -3
  28. package/dist/testing.js +58 -6
  29. package/package.json +19 -18
  30. package/v3/dist/_virtual/_rolldown/runtime.js +13 -0
  31. package/{dist-v3 → v3/dist}/actor.d.ts +65 -78
  32. package/{dist-v3 → v3/dist}/actor.js +173 -37
  33. package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -2
  34. package/{dist-v3 → v3/dist}/cluster/entity-machine.js +9 -9
  35. package/{dist-v3 → v3/dist}/cluster/index.js +1 -2
  36. package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
  37. package/{dist-v3 → v3/dist}/cluster/to-entity.js +2 -4
  38. package/v3/dist/errors.d.ts +76 -0
  39. package/{dist-v3 → v3/dist}/errors.js +9 -4
  40. package/v3/dist/index.d.ts +13 -0
  41. package/v3/dist/index.js +13 -0
  42. package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
  43. package/v3/dist/inspection.js +156 -0
  44. package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
  45. package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
  46. package/v3/dist/internal/inspection.js +20 -0
  47. package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
  48. package/{dist-v3 → v3/dist}/internal/transition.js +47 -17
  49. package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
  50. package/{dist-v3 → v3/dist}/internal/utils.js +2 -4
  51. package/{dist-v3 → v3/dist}/machine.d.ts +86 -10
  52. package/{dist-v3 → v3/dist}/machine.js +130 -33
  53. package/{dist-v3 → v3/dist}/persistence/adapter.d.ts +18 -5
  54. package/{dist-v3 → v3/dist}/persistence/adapter.js +2 -4
  55. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.d.ts +1 -1
  56. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.js +2 -4
  57. package/{dist-v3 → v3/dist}/persistence/index.js +1 -2
  58. package/{dist-v3 → v3/dist}/persistence/persistent-actor.d.ts +7 -6
  59. package/{dist-v3 → v3/dist}/persistence/persistent-actor.js +59 -22
  60. package/{dist-v3 → v3/dist}/persistence/persistent-machine.d.ts +1 -1
  61. package/{dist-v3 → v3/dist}/persistence/persistent-machine.js +2 -4
  62. package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
  63. package/{dist-v3 → v3/dist}/schema.js +6 -5
  64. package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
  65. package/{dist-v3 → v3/dist}/slot.js +2 -4
  66. package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
  67. package/{dist-v3 → v3/dist}/testing.js +61 -9
  68. package/dist-v3/_virtual/_rolldown/runtime.js +0 -18
  69. package/dist-v3/errors.d.ts +0 -27
  70. package/dist-v3/index.d.ts +0 -13
  71. package/dist-v3/index.js +0 -14
  72. package/dist-v3/inspection.js +0 -50
  73. package/dist-v3/internal/inspection.js +0 -15
  74. /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
  75. /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
  76. /package/{dist-v3 → v3/dist}/persistence/index.d.ts +0 -0
package/README.md CHANGED
@@ -74,8 +74,12 @@ const orderMachine = Machine.make({
74
74
  const program = Effect.gen(function* () {
75
75
  const actor = yield* Machine.spawn(orderMachine);
76
76
 
77
+ // fire-and-forget
77
78
  yield* actor.send(OrderEvent.Process);
78
- yield* actor.send(OrderEvent.Ship({ trackingId: "TRACK-123" }));
79
+
80
+ // request-reply — get ProcessEventResult back
81
+ const result = yield* actor.call(OrderEvent.Ship({ trackingId: "TRACK-123" }));
82
+ console.log(result.transitioned); // true
79
83
 
80
84
  const state = yield* actor.waitFor(OrderState.Shipped);
81
85
  console.log(state); // Shipped { orderId: "order-1", trackingId: "TRACK-123" }
@@ -183,6 +187,53 @@ machine.task(State.Loading, ({ effects, state }) => effects.fetchData({ url: sta
183
187
  });
184
188
  ```
185
189
 
190
+ ### State Timeouts
191
+
192
+ `.timeout()` — gen_statem-style state timeouts. Timer starts on state entry, cancels on exit:
193
+
194
+ ```ts
195
+ machine
196
+ .timeout(State.Loading, {
197
+ duration: Duration.seconds(30),
198
+ event: Event.Timeout,
199
+ })
200
+ // Dynamic duration from state
201
+ .timeout(State.Retrying, {
202
+ duration: (state) => Duration.seconds(state.backoff),
203
+ event: Event.GiveUp,
204
+ });
205
+ ```
206
+
207
+ `.reenter()` restarts the timer with fresh state values.
208
+
209
+ ### Event Postpone
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:
212
+
213
+ ```ts
214
+ machine
215
+ .postpone(State.Connecting, Event.Data) // single event
216
+ .postpone(State.Connecting, [Event.Data, Event.Cmd]); // multiple events
217
+ ```
218
+
219
+ Reply-bearing events (`call`/`ask`) in the postpone buffer are settled with `ActorStoppedError` on stop/interrupt/final-state.
220
+
221
+ ### ask / reply
222
+
223
+ Handlers can return a domain reply via `{ state, reply }`:
224
+
225
+ ```ts
226
+ .on(State.Active, Event.GetCount, ({ state }) => ({
227
+ state, // stay in same state
228
+ reply: state.count, // domain value returned to caller
229
+ }))
230
+
231
+ // Caller side:
232
+ const count = yield* actor.ask<number>(Event.GetCount);
233
+ ```
234
+
235
+ `ask` fails with `NoReplyError` if the handler doesn't provide a reply, and `ActorStoppedError` if the actor stops while the request is pending.
236
+
186
237
  ### Child Actors
187
238
 
188
239
  Spawn children from `.spawn()` handlers with `self.spawn`. Children are state-scoped — auto-stopped on state exit:
@@ -253,7 +304,7 @@ See the [primer](./primer/) for comprehensive documentation:
253
304
  | ----------- | ----------------------------------------- | ------------------------------ |
254
305
  | Overview | [index.md](./primer/index.md) | Navigation and quick reference |
255
306
  | Basics | [basics.md](./primer/basics.md) | Core concepts |
256
- | Handlers | [handlers.md](./primer/handlers.md) | Transitions and guards |
307
+ | Handlers | [handlers.md](./primer/handlers.md) | Transitions, guards, reply |
257
308
  | Effects | [effects.md](./primer/effects.md) | spawn, background, timeouts |
258
309
  | Testing | [testing.md](./primer/testing.md) | simulate, harness, assertions |
259
310
  | Actors | [actors.md](./primer/actors.md) | ActorSystem, ActorRef |
@@ -273,6 +324,8 @@ See the [primer](./primer/) for comprehensive documentation:
273
324
  | `.reenter(State.X, Event.Y, handler)` | Force re-entry on same state |
274
325
  | `.spawn(State.X, handler)` | State-scoped effect |
275
326
  | `.task(State.X, run, { onSuccess })` | State-scoped task |
327
+ | `.timeout(State.X, { duration, event })` | State timeout (gen_statem) |
328
+ | `.postpone(State.X, Event.Y)` | Postpone event in state (gen_statem) |
276
329
  | `.background(handler)` | Machine-lifetime effect |
277
330
  | `.final(State.X)` | Mark final state |
278
331
  | `.build({ slot: impl })` | Provide implementations, returns `BuiltMachine` (terminal) |
@@ -308,20 +361,27 @@ See the [primer](./primer/) for comprehensive documentation:
308
361
 
309
362
  ### Actor
310
363
 
311
- | Method | Description |
312
- | -------------------------------- | ---------------------------------- |
313
- | `actor.send(event)` | Queue event |
314
- | `actor.sendSync(event)` | Fire-and-forget (sync, for UI) |
315
- | `actor.snapshot` | Get current state |
316
- | `actor.matches(tag)` | Check state tag |
317
- | `actor.can(event)` | Can handle event? |
318
- | `actor.changes` | Stream of changes |
319
- | `actor.waitFor(State.X)` | Wait for state (constructor or fn) |
320
- | `actor.awaitFinal` | Wait final state |
321
- | `actor.sendAndWait(ev, State.X)` | Send + wait for state |
322
- | `actor.subscribe(fn)` | Sync callback |
323
- | `actor.system` | Access the actor's `ActorSystem` |
324
- | `actor.children` | Child actors (`ReadonlyMap`) |
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`) |
325
385
 
326
386
  ### ActorSystem
327
387
 
@@ -2,17 +2,12 @@
2
2
  var __defProp = Object.defineProperty;
3
3
  var __exportAll = (all, no_symbols) => {
4
4
  let target = {};
5
- for (var name in all) {
6
- __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true
9
- });
10
- }
11
- if (!no_symbols) {
12
- __defProp(target, Symbol.toStringTag, { value: "Module" });
13
- }
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
14
10
  return target;
15
11
  };
16
-
17
12
  //#endregion
18
- export { __exportAll };
13
+ export { __exportAll };
package/dist/actor.d.ts CHANGED
@@ -1,88 +1,85 @@
1
1
  import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
2
2
  import { PersistentMachine } from "./persistence/persistent-machine.js";
3
- import { DuplicateActorError } from "./errors.js";
3
+ import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
4
  import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
5
5
  import { PersistentActorRef } from "./persistence/persistent-actor.js";
6
6
  import { ActorMetadata, PersistenceAdapterTag, PersistenceError, RestoreResult, VersionConflictError } from "./persistence/adapter.js";
7
7
  import { BuiltMachine, Machine, MachineRef } from "./machine.js";
8
- import { Effect, Layer, Option, Queue, Ref, Scope, ServiceMap, Stream, SubscriptionRef } from "effect";
8
+ import { Deferred, Effect, Layer, Option, Queue, Ref, Scope, ServiceMap, Stream, SubscriptionRef } from "effect";
9
9
  import * as effect_Tracer0 from "effect/Tracer";
10
10
 
11
11
  //#region src/actor.d.ts
12
+ /** Discriminated mailbox request */
13
+ type QueuedEvent<E> = {
14
+ readonly _tag: "send";
15
+ readonly event: E;
16
+ } | {
17
+ readonly _tag: "call";
18
+ readonly event: E;
19
+ readonly reply: Deferred.Deferred<ProcessEventResult<{
20
+ readonly _tag: string;
21
+ }>, ActorStoppedError>;
22
+ } | {
23
+ readonly _tag: "ask";
24
+ readonly event: E;
25
+ readonly reply: Deferred.Deferred<unknown, NoReplyError | ActorStoppedError>;
26
+ };
12
27
  /**
13
28
  * Reference to a running actor.
14
29
  */
30
+ /**
31
+ * Sync projection of ActorRef for non-Effect boundaries (React hooks, framework callbacks).
32
+ */
33
+ interface ActorRefSync<State extends {
34
+ readonly _tag: string;
35
+ }, Event> {
36
+ readonly send: (event: Event) => void;
37
+ readonly stop: () => void;
38
+ readonly snapshot: () => State;
39
+ readonly matches: (tag: State["_tag"]) => boolean;
40
+ readonly can: (event: Event) => boolean;
41
+ }
15
42
  interface ActorRef<State extends {
16
43
  readonly _tag: string;
17
44
  }, Event> {
18
- /**
19
- * Unique identifier for this actor
20
- */
21
45
  readonly id: string;
22
- /**
23
- * Send an event to the actor
24
- */
46
+ /** Send an event (fire-and-forget). */
25
47
  readonly send: (event: Event) => Effect.Effect<void>;
48
+ /** Fire-and-forget alias for send (OTP gen_server:cast). */
49
+ readonly cast: (event: Event) => Effect.Effect<void>;
26
50
  /**
27
- * Observable state of the actor
51
+ * Serialized request-reply (OTP gen_server:call).
52
+ * Event is processed through the queue; caller gets ProcessEventResult back.
28
53
  */
29
- readonly state: SubscriptionRef.SubscriptionRef<State>;
54
+ readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
30
55
  /**
31
- * Stop the actor gracefully
56
+ * Typed request-reply. Event is processed through the queue; caller gets
57
+ * the domain value returned by the handler's `reply` field.
58
+ * Fails with NoReplyError if the handler doesn't provide a reply.
32
59
  */
60
+ readonly ask: <R>(event: Event) => Effect.Effect<R, NoReplyError | ActorStoppedError>;
61
+ /** Observable state. */
62
+ readonly state: SubscriptionRef.SubscriptionRef<State>;
63
+ /** Stop the actor gracefully. */
33
64
  readonly stop: Effect.Effect<void>;
34
- /**
35
- * Stop the actor (fire-and-forget).
36
- * Signals graceful shutdown without waiting for completion.
37
- * Use when stopping from sync contexts (e.g. framework cleanup hooks).
38
- */
39
- readonly stopSync: () => void;
40
- /**
41
- * Get current state snapshot (Effect)
42
- */
65
+ /** Get current state snapshot. */
43
66
  readonly snapshot: Effect.Effect<State>;
44
- /**
45
- * Get current state snapshot (sync)
46
- */
47
- readonly snapshotSync: () => State;
48
- /**
49
- * Check if current state matches tag (Effect)
50
- */
67
+ /** Check if current state matches tag. */
51
68
  readonly matches: (tag: State["_tag"]) => Effect.Effect<boolean>;
52
- /**
53
- * Check if current state matches tag (sync)
54
- */
55
- readonly matchesSync: (tag: State["_tag"]) => boolean;
56
- /**
57
- * Check if event can be handled in current state (Effect)
58
- */
69
+ /** Check if event can be handled in current state. */
59
70
  readonly can: (event: Event) => Effect.Effect<boolean>;
60
- /**
61
- * Check if event can be handled in current state (sync)
62
- */
63
- readonly canSync: (event: Event) => boolean;
64
- /**
65
- * Stream of state changes
66
- */
71
+ /** Stream of state changes. */
67
72
  readonly changes: Stream.Stream<State>;
68
- /**
69
- * Wait for a state that matches predicate or state variant (includes current snapshot).
70
- * Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
71
- */
73
+ /** Wait for a state matching predicate or variant (includes current snapshot). */
72
74
  readonly waitFor: {
73
75
  (predicate: (state: State) => boolean): Effect.Effect<State>;
74
76
  (state: {
75
77
  readonly _tag: State["_tag"];
76
78
  }): Effect.Effect<State>;
77
79
  };
78
- /**
79
- * Wait for a final state (includes current snapshot)
80
- */
80
+ /** Wait for a final state (includes current snapshot). */
81
81
  readonly awaitFinal: Effect.Effect<State>;
82
- /**
83
- * Send event and wait for predicate, state variant, or final state.
84
- * Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
85
- */
82
+ /** Send event and wait for predicate, state variant, or final state. */
86
83
  readonly sendAndWait: {
87
84
  (event: Event, predicate: (state: State) => boolean): Effect.Effect<State>;
88
85
  (event: Event, state: {
@@ -90,26 +87,13 @@ interface ActorRef<State extends {
90
87
  }): Effect.Effect<State>;
91
88
  (event: Event): Effect.Effect<State>;
92
89
  };
93
- /**
94
- * Send event synchronously (fire-and-forget).
95
- * No-op on stopped actors. Use when you need to send from sync contexts
96
- * (e.g. framework hooks, event handlers).
97
- */
98
- readonly sendSync: (event: Event) => void;
99
- /**
100
- * Subscribe to state changes (sync callback)
101
- * Returns unsubscribe function
102
- */
90
+ /** Subscribe to state changes (sync callback). Returns unsubscribe function. */
103
91
  readonly subscribe: (fn: (state: State) => void) => () => void;
104
- /**
105
- * The actor system this actor belongs to.
106
- * Every actor always has a system either inherited from context or implicitly created.
107
- */
92
+ /** Sync helpers for non-Effect boundaries. */
93
+ readonly sync: ActorRefSync<State, Event>;
94
+ /** The actor system this actor belongs to. */
108
95
  readonly system: ActorSystem;
109
- /**
110
- * Child actors spawned via `self.spawn` in this actor's handlers.
111
- * State-scoped children are auto-removed on state exit.
112
- */
96
+ /** Child actors spawned via `self.spawn` in this actor's handlers. */
113
97
  readonly children: ReadonlyMap<string, ActorRef<AnyState, unknown>>;
114
98
  }
115
99
  /** Base type for stored actors (internal) */
@@ -275,7 +259,7 @@ declare const buildActorRefCore: <S extends {
275
259
  readonly _tag: string;
276
260
  }, E extends {
277
261
  readonly _tag: string;
278
- }, 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<E>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>) => ActorRef<S, E>;
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>;
279
263
  /**
280
264
  * Create and start an actor for a machine
281
265
  */
@@ -284,9 +268,11 @@ declare const createActor: <S extends {
284
268
  }, E extends {
285
269
  readonly _tag: string;
286
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>>;
271
+ /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
272
+ declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
287
273
  /**
288
274
  * Default ActorSystem layer
289
275
  */
290
276
  declare const Default: Layer.Layer<ActorSystem, never, never>;
291
277
  //#endregion
292
- export { ActorRef, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
278
+ export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
package/dist/actor.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import { Inspector } from "./inspection.js";
2
2
  import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
3
- import { DuplicateActorError } from "./errors.js";
3
+ import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
4
  import { isPersistentMachine } from "./persistence/persistent-machine.js";
5
5
  import { emitWithTimestamp } from "./internal/inspection.js";
6
- import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
6
+ import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
7
7
  import { PersistenceAdapterTag, PersistenceError } from "./persistence/adapter.js";
8
8
  import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
9
9
  import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
10
-
11
10
  //#region src/actor.ts
12
11
  /**
13
12
  * Actor system: spawning, lifecycle, and event processing.
@@ -32,10 +31,50 @@ const notifyListeners = (listeners, state) => {
32
31
  /**
33
32
  * Build core ActorRef methods shared between regular and persistent actors.
34
33
  */
35
- const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap) => {
34
+ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies) => {
36
35
  const send = Effect.fn("effect-machine.actor.send")(function* (event) {
37
36
  if (yield* Ref.get(stoppedRef)) return;
38
- yield* Queue.offer(eventQueue, event);
37
+ yield* Queue.offer(eventQueue, {
38
+ _tag: "send",
39
+ event
40
+ });
41
+ });
42
+ const call = Effect.fn("effect-machine.actor.call")(function* (event) {
43
+ if (yield* Ref.get(stoppedRef)) {
44
+ const currentState = yield* SubscriptionRef.get(stateRef);
45
+ return {
46
+ newState: currentState,
47
+ previousState: currentState,
48
+ transitioned: false,
49
+ lifecycleRan: false,
50
+ isFinal: machine.finalStates.has(currentState._tag)
51
+ };
52
+ }
53
+ const reply = yield* Deferred.make();
54
+ pendingReplies.add(reply);
55
+ yield* Queue.offer(eventQueue, {
56
+ _tag: "call",
57
+ event,
58
+ reply
59
+ });
60
+ return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))), Effect.catchTag("ActorStoppedError", () => SubscriptionRef.get(stateRef).pipe(Effect.map((currentState) => ({
61
+ newState: currentState,
62
+ previousState: currentState,
63
+ transitioned: false,
64
+ lifecycleRan: false,
65
+ isFinal: machine.finalStates.has(currentState._tag)
66
+ })))));
67
+ });
68
+ const ask = Effect.fn("effect-machine.actor.ask")(function* (event) {
69
+ if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
70
+ const reply = yield* Deferred.make();
71
+ pendingReplies.add(reply);
72
+ yield* Queue.offer(eventQueue, {
73
+ _tag: "ask",
74
+ event,
75
+ reply
76
+ });
77
+ return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))));
39
78
  });
40
79
  const snapshot = SubscriptionRef.get(stateRef).pipe(Effect.withSpan("effect-machine.actor.snapshot"));
41
80
  const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
@@ -71,30 +110,38 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
71
110
  return {
72
111
  id,
73
112
  send,
113
+ cast: send,
114
+ call,
115
+ ask,
74
116
  state: stateRef,
75
117
  stop,
76
- stopSync: () => Effect.runFork(stop),
77
118
  snapshot,
78
- snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
79
119
  matches,
80
- matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
81
120
  can,
82
- canSync: (event) => {
83
- return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
84
- },
85
121
  changes: SubscriptionRef.changes(stateRef),
86
122
  waitFor,
87
123
  awaitFinal,
88
124
  sendAndWait,
89
- sendSync: (event) => {
90
- if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, event));
91
- },
92
125
  subscribe: (fn) => {
93
126
  listeners.add(fn);
94
127
  return () => {
95
128
  listeners.delete(fn);
96
129
  };
97
130
  },
131
+ sync: {
132
+ send: (event) => {
133
+ if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, {
134
+ _tag: "send",
135
+ event
136
+ }));
137
+ },
138
+ stop: () => Effect.runFork(stop),
139
+ snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
140
+ matches: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
141
+ can: (event) => {
142
+ return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
143
+ }
144
+ },
98
145
  system,
99
146
  children: childrenMap
100
147
  };
@@ -117,11 +164,16 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
117
164
  const eventQueue = yield* Queue.unbounded();
118
165
  const stoppedRef = yield* Ref.make(false);
119
166
  const childrenMap = /* @__PURE__ */ new Map();
167
+ const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
168
+ if (yield* Ref.get(stoppedRef)) return;
169
+ yield* Queue.offer(eventQueue, {
170
+ _tag: "send",
171
+ event
172
+ });
173
+ });
120
174
  const self = {
121
- send: Effect.fn("effect-machine.actor.self.send")(function* (event) {
122
- if (yield* Ref.get(stoppedRef)) return;
123
- yield* Queue.offer(eventQueue, event);
124
- }),
175
+ send: selfSend,
176
+ cast: selfSend,
125
177
  spawn: (childId, childMachine) => Effect.gen(function* () {
126
178
  const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
127
179
  childrenMap.set(childId, child);
@@ -175,9 +227,10 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
175
227
  }));
176
228
  yield* Ref.set(stoppedRef, true);
177
229
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
178
- return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
230
+ 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());
179
231
  }
180
- const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system));
232
+ const pendingReplies = /* @__PURE__ */ new Set();
233
+ const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies));
181
234
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
182
235
  const finalState = yield* SubscriptionRef.get(stateRef);
183
236
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
@@ -188,31 +241,104 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
188
241
  }));
189
242
  yield* Ref.set(stoppedRef, true);
190
243
  yield* Fiber.interrupt(loopFiber);
244
+ yield* settlePendingReplies(pendingReplies, id);
191
245
  yield* Scope.close(stateScopeRef.current, Exit.void);
192
246
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
193
247
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
194
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
248
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies);
249
+ });
250
+ /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
251
+ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
252
+ const error = new ActorStoppedError({ actorId });
253
+ for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
254
+ pendingReplies.clear();
195
255
  });
196
256
  /**
197
- * Main event loop for the actor
257
+ * Main event loop for the actor.
258
+ * Includes postpone buffer — events matching postpone rules are buffered
259
+ * and drained after state tag changes (gen_statem semantics).
198
260
  */
199
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system) {
200
- while (true) {
201
- const event = yield* Queue.take(eventQueue);
261
+ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies) {
262
+ const postponed = [];
263
+ const hasPostponeRules = machine.postponeRules.length > 0;
264
+ const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
265
+ const event = queued.event;
202
266
  const currentState = yield* SubscriptionRef.get(stateRef);
203
- if (yield* Effect.withSpan("effect-machine.event.process", { attributes: {
267
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
268
+ postponed.push(queued);
269
+ if (queued._tag === "call") {
270
+ const postponedResult = {
271
+ newState: currentState,
272
+ previousState: currentState,
273
+ transitioned: false,
274
+ lifecycleRan: false,
275
+ isFinal: false,
276
+ hasReply: false,
277
+ reply: void 0,
278
+ postponed: true
279
+ };
280
+ yield* Deferred.succeed(queued.reply, postponedResult);
281
+ }
282
+ return {
283
+ shouldStop: false,
284
+ stateChanged: false
285
+ };
286
+ }
287
+ const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
204
288
  "effect_machine.actor.id": actorId,
205
289
  "effect_machine.state.current": currentState._tag,
206
290
  "effect_machine.event.type": event._tag
207
- } })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system))) {
291
+ } })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
292
+ switch (queued._tag) {
293
+ case "call":
294
+ yield* Deferred.succeed(queued.reply, result);
295
+ break;
296
+ case "ask":
297
+ if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
298
+ else yield* Deferred.fail(queued.reply, new NoReplyError({
299
+ actorId,
300
+ eventTag: event._tag
301
+ }));
302
+ break;
303
+ }
304
+ return {
305
+ shouldStop,
306
+ stateChanged: result.lifecycleRan
307
+ };
308
+ });
309
+ while (true) {
310
+ const { shouldStop, stateChanged } = yield* processQueued(yield* Queue.take(eventQueue));
311
+ if (shouldStop) {
208
312
  yield* Ref.set(stoppedRef, true);
313
+ settlePostponedBuffer(postponed, pendingReplies, actorId);
314
+ yield* settlePendingReplies(pendingReplies, actorId);
209
315
  yield* Scope.close(stateScopeRef.current, Exit.void);
210
316
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
211
317
  return;
212
318
  }
319
+ if (stateChanged && postponed.length > 0) {
320
+ 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
+ }
329
+ }
213
330
  }
214
331
  });
215
332
  /**
333
+ * Settle all reply-bearing entries in the postpone buffer on shutdown.
334
+ * Call entries already had their Deferred settled with the postponed result
335
+ * (so their pendingReplies entry is already removed). Ask/send entries
336
+ * with Deferreds are settled via the pendingReplies registry.
337
+ */
338
+ const settlePostponedBuffer = (postponed, _pendingReplies, _actorId) => {
339
+ postponed.length = 0;
340
+ };
341
+ /**
216
342
  * Process a single event, returning true if the actor should stop.
217
343
  * Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
218
344
  */
@@ -252,7 +378,10 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
252
378
  });
253
379
  if (!result.transitioned) {
254
380
  yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
255
- return false;
381
+ return {
382
+ shouldStop: false,
383
+ result
384
+ };
256
385
  }
257
386
  yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
258
387
  yield* SubscriptionRef.set(stateRef, result.newState);
@@ -267,10 +396,16 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
267
396
  finalState: result.newState,
268
397
  timestamp
269
398
  }));
270
- return true;
399
+ return {
400
+ shouldStop: true,
401
+ result
402
+ };
271
403
  }
272
404
  }
273
- return false;
405
+ return {
406
+ shouldStop: false,
407
+ result
408
+ };
274
409
  });
275
410
  /**
276
411
  * Run spawn effects with actor-specific inspection and tracing.
@@ -454,6 +589,5 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
454
589
  * Default ActorSystem layer
455
590
  */
456
591
  const Default = Layer.effect(ActorSystem, make());
457
-
458
592
  //#endregion
459
- export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
593
+ export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
@@ -1,7 +1,6 @@
1
1
  import { EffectsDef, GuardsDef } from "../slot.js";
2
2
  import { ProcessEventHooks } from "../internal/transition.js";
3
3
  import { Machine } from "../machine.js";
4
- import "../actor.js";
5
4
  import { Layer } from "effect";
6
5
  import { Entity } from "effect/unstable/cluster";
7
6
  import { Rpc } from "effect/unstable/rpc";