effect-machine 0.9.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 (62) hide show
  1. package/README.md +76 -16
  2. package/dist/actor.d.ts +55 -89
  3. package/dist/actor.js +135 -30
  4. package/dist/cluster/entity-machine.js +5 -3
  5. package/dist/errors.d.ts +12 -1
  6. package/dist/errors.js +8 -1
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +2 -2
  9. package/dist/internal/transition.d.ts +26 -2
  10. package/dist/internal/transition.js +37 -8
  11. package/dist/internal/utils.d.ts +7 -2
  12. package/dist/machine.d.ts +66 -3
  13. package/dist/machine.js +65 -0
  14. package/dist/persistence/persistent-actor.js +52 -16
  15. package/dist/testing.js +57 -3
  16. package/package.json +9 -8
  17. package/{dist-v3 → v3/dist}/actor.d.ts +65 -78
  18. package/{dist-v3 → v3/dist}/actor.js +173 -35
  19. package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -1
  20. package/{dist-v3 → v3/dist}/cluster/entity-machine.js +8 -6
  21. package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
  22. package/{dist-v3 → v3/dist}/cluster/to-entity.js +1 -1
  23. package/v3/dist/errors.d.ts +76 -0
  24. package/{dist-v3 → v3/dist}/errors.js +9 -2
  25. package/v3/dist/index.d.ts +13 -0
  26. package/v3/dist/index.js +13 -0
  27. package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
  28. package/v3/dist/inspection.js +156 -0
  29. package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
  30. package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
  31. package/v3/dist/internal/inspection.js +20 -0
  32. package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
  33. package/{dist-v3 → v3/dist}/internal/transition.js +47 -15
  34. package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
  35. package/{dist-v3 → v3/dist}/internal/utils.js +1 -1
  36. package/{dist-v3 → v3/dist}/machine.d.ts +86 -9
  37. package/{dist-v3 → v3/dist}/machine.js +128 -2
  38. package/{dist-v3 → v3/dist}/persistence/adapter.d.ts +18 -5
  39. package/{dist-v3 → v3/dist}/persistence/adapter.js +1 -1
  40. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.d.ts +1 -1
  41. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.js +1 -1
  42. package/{dist-v3 → v3/dist}/persistence/persistent-actor.d.ts +7 -6
  43. package/{dist-v3 → v3/dist}/persistence/persistent-actor.js +58 -19
  44. package/{dist-v3 → v3/dist}/persistence/persistent-machine.d.ts +1 -1
  45. package/{dist-v3 → v3/dist}/persistence/persistent-machine.js +1 -1
  46. package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
  47. package/{dist-v3 → v3/dist}/schema.js +5 -2
  48. package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
  49. package/{dist-v3 → v3/dist}/slot.js +1 -1
  50. package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
  51. package/{dist-v3 → v3/dist}/testing.js +60 -6
  52. package/dist-v3/errors.d.ts +0 -27
  53. package/dist-v3/index.d.ts +0 -12
  54. package/dist-v3/index.js +0 -13
  55. package/dist-v3/inspection.js +0 -48
  56. package/dist-v3/internal/inspection.js +0 -13
  57. /package/{dist-v3 → v3/dist}/_virtual/_rolldown/runtime.js +0 -0
  58. /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
  59. /package/{dist-v3 → v3/dist}/cluster/index.js +0 -0
  60. /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
  61. /package/{dist-v3 → v3/dist}/persistence/index.d.ts +0 -0
  62. /package/{dist-v3 → v3/dist}/persistence/index.js +0 -0
@@ -1,12 +1,14 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
+ import { Inspector } from "./inspection.js";
2
3
  import { getTag } from "./internal/utils.js";
3
4
  import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
4
5
  import { persist } from "./persistence/persistent-machine.js";
6
+ import { emitWithTimestamp } from "./internal/inspection.js";
5
7
  import { MachineContextTag } from "./slot.js";
6
8
  import { findTransitions, invalidateIndex } from "./internal/transition.js";
7
9
  import { createActor } from "./actor.js";
8
10
  import { Cause, Effect, Exit, Option, Scope } from "effect";
9
- //#region src-v3/machine.ts
11
+ //#region src/machine.ts
10
12
  var machine_exports = /* @__PURE__ */ __exportAll({
11
13
  BuiltMachine: () => BuiltMachine,
12
14
  Machine: () => Machine,
@@ -14,6 +16,15 @@ var machine_exports = /* @__PURE__ */ __exportAll({
14
16
  make: () => make,
15
17
  spawn: () => spawn
16
18
  });
19
+ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Inspector).pipe(Effect.option), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
20
+ type: "@machine.task",
21
+ actorId: input.actorId,
22
+ state: input.state,
23
+ taskName: input.taskName,
24
+ phase: input.phase,
25
+ error: input.error,
26
+ timestamp
27
+ })));
17
28
  /**
18
29
  * A finalized machine ready for spawning.
19
30
  *
@@ -53,6 +64,7 @@ var Machine = class Machine {
53
64
  /** @internal */ _spawnEffects;
54
65
  /** @internal */ _backgroundEffects;
55
66
  /** @internal */ _finalStates;
67
+ /** @internal */ _postponeRules;
56
68
  /** @internal */ _guardsSchema;
57
69
  /** @internal */ _effectsSchema;
58
70
  /** @internal */ _guardHandlers;
@@ -77,6 +89,9 @@ var Machine = class Machine {
77
89
  get finalStates() {
78
90
  return this._finalStates;
79
91
  }
92
+ get postponeRules() {
93
+ return this._postponeRules;
94
+ }
80
95
  get guardsSchema() {
81
96
  return this._guardsSchema;
82
97
  }
@@ -90,6 +105,7 @@ var Machine = class Machine {
90
105
  this._spawnEffects = [];
91
106
  this._backgroundEffects = [];
92
107
  this._finalStates = /* @__PURE__ */ new Set();
108
+ this._postponeRules = [];
93
109
  this._guardsSchema = guardsSchema;
94
110
  this._effectsSchema = effectsSchema;
95
111
  this._guardHandlers = /* @__PURE__ */ new Map();
@@ -116,6 +132,15 @@ var Machine = class Machine {
116
132
  })) : {}
117
133
  };
118
134
  }
135
+ from(stateOrStates, build) {
136
+ build(new TransitionScope(this, Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates]));
137
+ return this;
138
+ }
139
+ /** @internal */
140
+ scopeTransition(states, event, handler, reenter) {
141
+ for (const state of states) this.addTransition(state, event, handler, reenter);
142
+ return this;
143
+ }
119
144
  on(stateOrStates, event, handler) {
120
145
  const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
121
146
  for (const s of states) this.addTransition(s, event, handler, false);
@@ -190,14 +215,41 @@ var Machine = class Machine {
190
215
  */
191
216
  task(state, run, options) {
192
217
  const handler = Effect.fn("effect-machine.task")(function* (ctx) {
218
+ yield* emitTaskInspection({
219
+ actorId: ctx.actorId,
220
+ state: ctx.state,
221
+ taskName: options.name,
222
+ phase: "start"
223
+ });
193
224
  const exit = yield* Effect.exit(run(ctx));
194
225
  if (Exit.isSuccess(exit)) {
226
+ yield* emitTaskInspection({
227
+ actorId: ctx.actorId,
228
+ state: ctx.state,
229
+ taskName: options.name,
230
+ phase: "success"
231
+ });
195
232
  yield* ctx.self.send(options.onSuccess(exit.value, ctx));
196
233
  yield* Effect.yieldNow();
197
234
  return;
198
235
  }
199
236
  const cause = exit.cause;
200
- if (Cause.isInterruptedOnly(cause)) return;
237
+ if (Cause.isInterruptedOnly(cause)) {
238
+ yield* emitTaskInspection({
239
+ actorId: ctx.actorId,
240
+ state: ctx.state,
241
+ taskName: options.name,
242
+ phase: "interrupt"
243
+ });
244
+ return;
245
+ }
246
+ yield* emitTaskInspection({
247
+ actorId: ctx.actorId,
248
+ state: ctx.state,
249
+ taskName: options.name,
250
+ phase: "failure",
251
+ error: Cause.pretty(cause)
252
+ });
201
253
  if (options.onFailure !== void 0) {
202
254
  yield* ctx.self.send(options.onFailure(cause, ctx));
203
255
  yield* Effect.yieldNow();
@@ -208,6 +260,36 @@ var Machine = class Machine {
208
260
  return this.spawn(state, handler);
209
261
  }
210
262
  /**
263
+ * State timeout — gen_statem's `state_timeout`.
264
+ *
265
+ * Entering the state starts a timer. Leaving cancels it (via state scope).
266
+ * `.reenter()` restarts the timer with fresh state values.
267
+ * Compiles to `.task()` internally — preserves `@machine.task` inspection events.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * machine
272
+ * .timeout(State.Loading, {
273
+ * duration: Duration.seconds(30),
274
+ * event: Event.Timeout,
275
+ * })
276
+ * // Dynamic duration from state
277
+ * .timeout(State.Retrying, {
278
+ * duration: (state) => Duration.seconds(state.backoff),
279
+ * event: Event.GiveUp,
280
+ * })
281
+ * ```
282
+ */
283
+ timeout(state, config) {
284
+ const stateTag = getTag(state);
285
+ const resolveDuration = typeof config.duration === "function" ? config.duration : () => config.duration;
286
+ const resolveEvent = typeof config.event === "function" ? config.event : () => config.event;
287
+ return this.task(state, (ctx) => Effect.sleep(resolveDuration(ctx.state)), {
288
+ onSuccess: (_, ctx) => resolveEvent(ctx.state),
289
+ name: `$timeout:${stateTag}`
290
+ });
291
+ }
292
+ /**
211
293
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
212
294
  * Use effect slots defined via `Slot.Effects` for the actual work.
213
295
  *
@@ -231,6 +313,35 @@ var Machine = class Machine {
231
313
  this._backgroundEffects.push({ handler });
232
314
  return this;
233
315
  }
316
+ /**
317
+ * Postpone events — gen_statem's event postpone.
318
+ *
319
+ * When a matching event arrives in the given state, it is buffered instead of
320
+ * processed. After the next state transition (tag change), all buffered events
321
+ * are drained through the loop in FIFO order.
322
+ *
323
+ * Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
324
+ * with `ActorStoppedError` on stop/interrupt/final-state.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * machine
329
+ * .postpone(State.Connecting, Event.Data) // single event
330
+ * .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
331
+ * ```
332
+ */
333
+ postpone(state, events) {
334
+ const stateTag = getTag(state);
335
+ const eventList = Array.isArray(events) ? events : [events];
336
+ for (const ev of eventList) {
337
+ const eventTag = getTag(ev);
338
+ this._postponeRules.push({
339
+ stateTag,
340
+ eventTag
341
+ });
342
+ }
343
+ return this;
344
+ }
234
345
  final(state) {
235
346
  const stateTag = getTag(state);
236
347
  this._finalStates.add(stateTag);
@@ -262,6 +373,7 @@ var Machine = class Machine {
262
373
  result._finalStates = new Set(this._finalStates);
263
374
  result._spawnEffects = [...this._spawnEffects];
264
375
  result._backgroundEffects = [...this._backgroundEffects];
376
+ result._postponeRules = [...this._postponeRules];
265
377
  const anyHandlers = handlers;
266
378
  if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
267
379
  if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
@@ -277,6 +389,20 @@ var Machine = class Machine {
277
389
  return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
278
390
  }
279
391
  };
392
+ var TransitionScope = class {
393
+ constructor(machine, states) {
394
+ this.machine = machine;
395
+ this.states = states;
396
+ }
397
+ on(event, handler) {
398
+ this.machine.scopeTransition(this.states, event, handler, false);
399
+ return this;
400
+ }
401
+ reenter(event, handler) {
402
+ this.machine.scopeTransition(this.states, event, handler, true);
403
+ return this;
404
+ }
405
+ };
280
406
  const make = Machine.make;
281
407
  const spawn = Effect.fn("effect-machine.spawn")(function* (built, id) {
282
408
  const actor = yield* createActor(id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner);
@@ -1,8 +1,8 @@
1
1
  import { DuplicateActorError } from "../errors.js";
2
2
  import { PersistentActorRef } from "./persistent-actor.js";
3
- import { Effect, Option, Schema } from "effect";
3
+ import { Context, Effect, Option, Schema } from "effect";
4
4
 
5
- //#region src-v3/persistence/adapter.d.ts
5
+ //#region src/persistence/adapter.d.ts
6
6
  /**
7
7
  * Metadata for a persisted actor.
8
8
  * Used for discovery and filtering during bulk restore.
@@ -106,17 +106,30 @@ interface PersistenceAdapter {
106
106
  */
107
107
  readonly loadMetadata?: (id: string) => Effect.Effect<Option.Option<ActorMetadata>, PersistenceError>;
108
108
  }
109
- declare const PersistenceError_base: any;
109
+ declare const PersistenceError_base: Schema.TaggedErrorClass<PersistenceError, "PersistenceError", {
110
+ readonly _tag: Schema.tag<"PersistenceError">;
111
+ } & {
112
+ operation: typeof Schema.String;
113
+ actorId: typeof Schema.String;
114
+ cause: Schema.optional<typeof Schema.Unknown>;
115
+ message: Schema.optional<typeof Schema.String>;
116
+ }>;
110
117
  /**
111
118
  * Error type for persistence operations
112
119
  */
113
120
  declare class PersistenceError extends PersistenceError_base {}
114
- declare const VersionConflictError_base: any;
121
+ declare const VersionConflictError_base: Schema.TaggedErrorClass<VersionConflictError, "VersionConflictError", {
122
+ readonly _tag: Schema.tag<"VersionConflictError">;
123
+ } & {
124
+ actorId: typeof Schema.String;
125
+ expectedVersion: typeof Schema.Number;
126
+ actualVersion: typeof Schema.Number;
127
+ }>;
115
128
  /**
116
129
  * Version conflict error — snapshot version doesn't match expected
117
130
  */
118
131
  declare class VersionConflictError extends VersionConflictError_base {}
119
- declare const PersistenceAdapterTag_base: any;
132
+ declare const PersistenceAdapterTag_base: Context.TagClass<PersistenceAdapterTag, "effect-machine/src/persistence/adapter/PersistenceAdapterTag", PersistenceAdapter>;
120
133
  /**
121
134
  * PersistenceAdapter service tag
122
135
  */
@@ -1,5 +1,5 @@
1
1
  import { Context, Schema } from "effect";
2
- //#region src-v3/persistence/adapter.ts
2
+ //#region src/persistence/adapter.ts
3
3
  /**
4
4
  * Error type for persistence operations
5
5
  */
@@ -1,7 +1,7 @@
1
1
  import { PersistenceAdapter, PersistenceAdapterTag } from "../adapter.js";
2
2
  import { Effect, Layer } from "effect";
3
3
 
4
- //#region src-v3/persistence/adapters/in-memory.d.ts
4
+ //#region src/persistence/adapters/in-memory.d.ts
5
5
  /**
6
6
  * Create an in-memory persistence adapter effect.
7
7
  * Returns the adapter directly for custom layer composition.
@@ -1,6 +1,6 @@
1
1
  import { PersistenceAdapterTag, PersistenceError, VersionConflictError } from "../adapter.js";
2
2
  import { Effect, Layer, Option, Ref, Schema } from "effect";
3
- //#region src-v3/persistence/adapters/in-memory.ts
3
+ //#region src/persistence/adapters/in-memory.ts
4
4
  /**
5
5
  * Create an in-memory persistence adapter.
6
6
  * Useful for testing and development.
@@ -1,10 +1,11 @@
1
+ import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
1
2
  import { PersistentMachine } from "./persistent-machine.js";
2
- import { EffectsDef, GuardsDef } from "../slot.js";
3
- import { PersistedEvent, PersistenceError, Snapshot, VersionConflictError } from "./adapter.js";
3
+ import { PersistedEvent, PersistenceAdapterTag, PersistenceError, Snapshot, VersionConflictError } from "./adapter.js";
4
+ import { MachineRef } from "../machine.js";
4
5
  import { ActorRef } from "../actor.js";
5
- import { Effect, Option } from "effect";
6
+ import { Effect, Option, Scope } from "effect";
6
7
 
7
- //#region src-v3/persistence/persistent-actor.d.ts
8
+ //#region src/persistence/persistent-actor.d.ts
8
9
  /**
9
10
  * Extended ActorRef with persistence capabilities
10
11
  */
@@ -35,7 +36,7 @@ declare const createPersistentActor: <S extends {
35
36
  readonly _tag: string;
36
37
  }, E extends {
37
38
  readonly _tag: string;
38
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(id: string, persistentMachine: PersistentMachine<S, E, R>, initialSnapshot: Option.Option<Snapshot<S>>, initialEvents: readonly PersistedEvent<E>[]) => Effect.Effect<PersistentActorRef<S, E, R>, unknown, unknown>;
39
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(id: string, persistentMachine: PersistentMachine<S, E, R>, initialSnapshot: Option.Option<Snapshot<S>>, initialEvents: readonly PersistedEvent<E>[]) => Effect.Effect<PersistentActorRef<S, E, R>, PersistenceError, PersistenceAdapterTag | Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
39
40
  /**
40
41
  * Restore an actor from persistence.
41
42
  * Returns None if no persisted state exists.
@@ -44,6 +45,6 @@ declare const restorePersistentActor: <S extends {
44
45
  readonly _tag: string;
45
46
  }, E extends {
46
47
  readonly _tag: string;
47
- }, R>(id: string, persistentMachine: PersistentMachine<S, E, R>) => Effect.Effect<Option.None<PersistentActorRef<S, E, R>> | Option.Some<PersistentActorRef<S, E, R>>, unknown, unknown>;
48
+ }, R>(id: string, persistentMachine: PersistentMachine<S, E, R>) => Effect.Effect<Option.None<PersistentActorRef<S, E, R>> | Option.Some<PersistentActorRef<S, E, R>>, PersistenceError, PersistenceAdapterTag | Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
48
49
  //#endregion
49
50
  export { PersistentActorRef, createPersistentActor, restorePersistentActor };
@@ -1,11 +1,12 @@
1
1
  import { Inspector } from "../inspection.js";
2
2
  import { INTERNAL_INIT_EVENT, stubSystem } from "../internal/utils.js";
3
- import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler } from "../internal/transition.js";
3
+ import { NoReplyError } from "../errors.js";
4
4
  import { emitWithTimestamp } from "../internal/inspection.js";
5
+ import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone } from "../internal/transition.js";
5
6
  import { PersistenceAdapterTag } from "./adapter.js";
6
- import { ActorSystem, buildActorRefCore, notifyListeners } from "../actor.js";
7
- import { Cause, Clock, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
8
- //#region src-v3/persistence/persistent-actor.ts
7
+ import { ActorSystem, buildActorRefCore, notifyListeners, settlePendingReplies } from "../actor.js";
8
+ import { Cause, Clock, Deferred, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
9
+ //#region src/persistence/persistent-actor.ts
9
10
  /** Get current time in milliseconds using Effect Clock */
10
11
  const now = Clock.currentTimeMillis;
11
12
  /**
@@ -19,7 +20,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
19
20
  for (const persistedEvent of events) {
20
21
  if (stopVersion !== void 0 && persistedEvent.version > stopVersion) break;
21
22
  const transition = resolveTransition(machine, state, persistedEvent.event);
22
- if (transition !== void 0) state = yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem);
23
+ if (transition !== void 0) state = (yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem, "restore")).newState;
23
24
  version = persistedEvent.version;
24
25
  }
25
26
  return {
@@ -30,7 +31,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
30
31
  /**
31
32
  * Build PersistentActorRef with all methods
32
33
  */
33
- const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap) => {
34
+ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap, pendingReplies) => {
34
35
  const { machine, persistence } = persistentMachine;
35
36
  const typedMachine = machine;
36
37
  const persist = Effect.gen(function* () {
@@ -44,8 +45,10 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
44
45
  const version = Ref.get(versionRef).pipe(Effect.withSpan("effect-machine.persistentActor.version"));
45
46
  const replayTo = Effect.fn("effect-machine.persistentActor.replayTo")(function* (targetVersion) {
46
47
  if (targetVersion <= (yield* Ref.get(versionRef))) {
48
+ const dummySend = Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void);
47
49
  const dummySelf = {
48
- send: Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void),
50
+ send: dummySend,
51
+ cast: dummySend,
49
52
  spawn: () => Effect.die("spawn not supported in replay")
50
53
  };
51
54
  const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
@@ -70,7 +73,7 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
70
73
  }
71
74
  });
72
75
  return {
73
- ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap),
76
+ ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies),
74
77
  persist,
75
78
  version,
76
79
  replayTo
@@ -92,11 +95,16 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
92
95
  const eventQueue = yield* Queue.unbounded();
93
96
  const stoppedRef = yield* Ref.make(false);
94
97
  const childrenMap = /* @__PURE__ */ new Map();
98
+ const selfSend = Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
99
+ if (yield* Ref.get(stoppedRef)) return;
100
+ yield* Queue.offer(eventQueue, {
101
+ _tag: "send",
102
+ event
103
+ });
104
+ });
95
105
  const self = {
96
- send: Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
97
- if (yield* Ref.get(stoppedRef)) return;
98
- yield* Queue.offer(eventQueue, event);
99
- }),
106
+ send: selfSend,
107
+ cast: selfSend,
100
108
  spawn: (childId, childMachine) => Effect.gen(function* () {
101
109
  const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
102
110
  childrenMap.set(childId, child);
@@ -145,6 +153,7 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
145
153
  const backgroundFibers = [];
146
154
  const initEvent = { _tag: INTERNAL_INIT_EVENT };
147
155
  const initCtx = {
156
+ actorId: id,
148
157
  state: resolvedInitial,
149
158
  event: initEvent,
150
159
  self,
@@ -153,6 +162,7 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
153
162
  const { effects: effectSlots } = typedMachine._slots;
154
163
  for (const bg of typedMachine.backgroundEffects) {
155
164
  const fiber = yield* Effect.forkDaemon(bg.handler({
165
+ actorId: id,
156
166
  state: resolvedInitial,
157
167
  event: initEvent,
158
168
  self,
@@ -175,9 +185,10 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
175
185
  finalState: resolvedInitial,
176
186
  timestamp
177
187
  }));
178
- return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
188
+ return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, /* @__PURE__ */ new Set());
179
189
  }
180
- const loopFiber = yield* Effect.forkDaemon(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system));
190
+ const pendingReplies = /* @__PURE__ */ new Set();
191
+ const loopFiber = yield* Effect.forkDaemon(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies));
181
192
  return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
182
193
  const finalState = yield* SubscriptionRef.get(stateRef);
183
194
  yield* emitWithTimestamp(inspector, (timestamp) => ({
@@ -188,16 +199,17 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
188
199
  }));
189
200
  yield* Ref.set(stoppedRef, true);
190
201
  yield* Fiber.interrupt(loopFiber);
202
+ yield* settlePendingReplies(pendingReplies, id);
191
203
  yield* Scope.close(stateScopeRef.current, Exit.void);
192
204
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
193
205
  yield* Fiber.interrupt(snapshotFiber);
194
206
  yield* Fiber.interrupt(persistenceFiber);
195
- }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
207
+ }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, pendingReplies);
196
208
  });
197
209
  /**
198
210
  * Main event loop for persistent actor
199
211
  */
200
- const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system) {
212
+ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies) {
201
213
  const { machine, persistence } = persistentMachine;
202
214
  const typedMachine = machine;
203
215
  const hooks = inspector === void 0 ? void 0 : {
@@ -226,10 +238,28 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
226
238
  timestamp
227
239
  }))
228
240
  };
241
+ const postponed = [];
242
+ const pendingDrain = [];
243
+ const hasPostponeRules = machine.postponeRules.length > 0;
229
244
  while (true) {
230
- const event = yield* Queue.take(eventQueue);
245
+ const queued = pendingDrain.length > 0 ? pendingDrain.shift() : yield* Queue.take(eventQueue);
246
+ const event = queued.event;
231
247
  const currentState = yield* SubscriptionRef.get(stateRef);
232
248
  const currentVersion = yield* Ref.get(versionRef);
249
+ if (hasPostponeRules && shouldPostpone(typedMachine, currentState._tag, event._tag)) {
250
+ postponed.push(queued);
251
+ if (queued._tag === "call") yield* Deferred.succeed(queued.reply, {
252
+ newState: currentState,
253
+ previousState: currentState,
254
+ transitioned: false,
255
+ lifecycleRan: false,
256
+ isFinal: false,
257
+ hasReply: false,
258
+ reply: void 0,
259
+ postponed: true
260
+ });
261
+ continue;
262
+ }
233
263
  yield* emitWithTimestamp(inspector, (timestamp) => ({
234
264
  type: "@machine.event",
235
265
  actorId: id,
@@ -237,7 +267,13 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
237
267
  event,
238
268
  timestamp
239
269
  }));
240
- const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, hooks);
270
+ const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, id, hooks);
271
+ if (queued._tag === "call") yield* Deferred.succeed(queued.reply, result);
272
+ else if (queued._tag === "ask") if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
273
+ else yield* Deferred.fail(queued.reply, new NoReplyError({
274
+ actorId: id,
275
+ eventTag: event._tag
276
+ }));
241
277
  if (!result.transitioned) continue;
242
278
  const newVersion = currentVersion + 1;
243
279
  yield* Ref.set(versionRef, newVersion);
@@ -265,12 +301,15 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
265
301
  timestamp
266
302
  }));
267
303
  yield* Ref.set(stoppedRef, true);
304
+ postponed.length = 0;
305
+ yield* settlePendingReplies(pendingReplies, id);
268
306
  yield* Scope.close(stateScopeRef.current, Exit.void);
269
307
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
270
308
  yield* Fiber.interrupt(snapshotFiber);
271
309
  yield* Fiber.interrupt(persistenceFiber);
272
310
  return;
273
311
  }
312
+ if (result.lifecycleRan && postponed.length > 0) pendingDrain.push(...postponed.splice(0));
274
313
  }
275
314
  });
276
315
  /**
@@ -285,7 +324,7 @@ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.persistentActor.
285
324
  state,
286
325
  timestamp
287
326
  }));
288
- yield* runSpawnEffects(machine, state, event, self, stateScope, system, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
327
+ yield* runSpawnEffects(machine, state, event, self, stateScope, system, actorId, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
289
328
  type: "@machine.error",
290
329
  actorId,
291
330
  phase: info.phase,
@@ -2,7 +2,7 @@ import { EventBrand, StateBrand } from "../internal/brands.js";
2
2
  import { Machine } from "../machine.js";
3
3
  import { Schedule, Schema } from "effect";
4
4
 
5
- //#region src-v3/persistence/persistent-machine.d.ts
5
+ //#region src/persistence/persistent-machine.d.ts
6
6
  type BrandedState = {
7
7
  readonly _tag: string;
8
8
  } & StateBrand;
@@ -1,5 +1,5 @@
1
1
  import { MissingSchemaError } from "../errors.js";
2
- //#region src-v3/persistence/persistent-machine.ts
2
+ //#region src/persistence/persistent-machine.ts
3
3
  /**
4
4
  * Type guard to check if a value is a PersistentMachine
5
5
  */
@@ -1,7 +1,7 @@
1
1
  import { FullEventBrand, FullStateBrand } from "./internal/brands.js";
2
2
  import { Schema } from "effect";
3
3
 
4
- //#region src-v3/schema.d.ts
4
+ //#region src/schema.d.ts
5
5
  /**
6
6
  * Extract the TypeScript type from a TaggedStruct schema
7
7
  */
@@ -1,6 +1,6 @@
1
1
  import { InvalidSchemaError, MissingMatchHandlerError } from "./errors.js";
2
2
  import { Schema } from "effect";
3
- //#region src-v3/schema.ts
3
+ //#region src/schema.ts
4
4
  /**
5
5
  * Schema-first State/Event definitions for effect-machine.
6
6
  *
@@ -58,7 +58,10 @@ const buildMachineSchema = (definition) => {
58
58
  constructor.derive = (source, partial) => {
59
59
  const result = { _tag: tag };
60
60
  for (const key of fieldNames) if (key in source) result[key] = source[key];
61
- if (partial !== void 0) for (const [key, value] of Object.entries(partial)) result[key] = value;
61
+ if (partial !== void 0) for (const [key, value] of Object.entries(partial)) {
62
+ if (key === "_tag") continue;
63
+ result[key] = value;
64
+ }
62
65
  return result;
63
66
  };
64
67
  constructors[tag] = constructor;
@@ -1,7 +1,7 @@
1
1
  import { ActorSystem } from "./actor.js";
2
- import { Effect, Schema } from "effect";
2
+ import { Context, Effect, Schema } from "effect";
3
3
 
4
- //#region src-v3/slot.d.ts
4
+ //#region src/slot.d.ts
5
5
  /** Schema fields definition (like Schema.Struct.Fields) */
6
6
  type Fields = Record<string, Schema.Schema.All>;
7
7
  /** Extract the encoded type from schema fields (used for parameters) */
@@ -43,6 +43,7 @@ type EffectSlots<D extends EffectsDef> = { readonly [K in keyof D & string]: Eff
43
43
  * Shared across all machines via MachineContextTag.
44
44
  */
45
45
  interface MachineContext<State, Event, Self> {
46
+ readonly actorId: string;
46
47
  readonly state: State;
47
48
  readonly event: Event;
48
49
  readonly self: Self;
@@ -53,7 +54,7 @@ interface MachineContext<State, Event, Self> {
53
54
  * Single module-level tag instead of per-machine allocation.
54
55
  * @internal
55
56
  */
56
- declare const MachineContextTag: any;
57
+ declare const MachineContextTag: Context.Tag<MachineContext<any, any, any>, MachineContext<any, any, any>>;
57
58
  /**
58
59
  * Guard handler implementation.
59
60
  * Receives params and context, returns Effect<boolean>.
@@ -1,5 +1,5 @@
1
1
  import { Context } from "effect";
2
- //#region src-v3/slot.ts
2
+ //#region src/slot.ts
3
3
  /**
4
4
  * Slot module - schema-based, parameterized guards and effects.
5
5
  *
@@ -1,9 +1,9 @@
1
+ import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
1
2
  import { AssertionError } from "./errors.js";
2
- import { EffectsDef, GuardsDef } from "./slot.js";
3
- import { BuiltMachine, Machine } from "./machine.js";
3
+ import { BuiltMachine, Machine, MachineRef } from "./machine.js";
4
4
  import { Effect, SubscriptionRef } from "effect";
5
5
 
6
- //#region src-v3/testing.d.ts
6
+ //#region src/testing.d.ts
7
7
  /** Accept either Machine or BuiltMachine for testing utilities. */
8
8
  type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> = Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>;
9
9
  /**
@@ -40,7 +40,7 @@ declare const simulate: <S extends {
40
40
  }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[]) => Effect.Effect<{
41
41
  states: S[];
42
42
  finalState: S;
43
- }, never, Exclude<R, unknown>>;
43
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
44
44
  /**
45
45
  * Assert that a machine can reach a specific state given a sequence of events
46
46
  */
@@ -48,7 +48,7 @@ declare const assertReaches: <S extends {
48
48
  readonly _tag: string;
49
49
  }, E extends {
50
50
  readonly _tag: string;
51
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string) => Effect.Effect<any, unknown, unknown>;
51
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string) => Effect.Effect<S, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
52
52
  /**
53
53
  * Assert that a machine follows a specific path of state tags
54
54
  *
@@ -65,7 +65,10 @@ declare const assertPath: <S extends {
65
65
  readonly _tag: string;
66
66
  }, E extends {
67
67
  readonly _tag: string;
68
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<any, unknown, unknown>;
68
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<{
69
+ states: S[];
70
+ finalState: S;
71
+ }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
69
72
  /**
70
73
  * Assert that a machine never reaches a specific state given a sequence of events
71
74
  *
@@ -83,7 +86,10 @@ declare const assertNeverReaches: <S extends {
83
86
  readonly _tag: string;
84
87
  }, E extends {
85
88
  readonly _tag: string;
86
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string) => Effect.Effect<any, unknown, unknown>;
89
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string) => Effect.Effect<{
90
+ states: S[];
91
+ finalState: S;
92
+ }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
87
93
  /**
88
94
  * Create a controllable test harness for a machine
89
95
  */
@@ -129,7 +135,7 @@ declare const createTestHarness: <S extends {
129
135
  readonly _tag: string;
130
136
  }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, options?: TestHarnessOptions<S, E> | undefined) => Effect.Effect<{
131
137
  state: SubscriptionRef.SubscriptionRef<S>;
132
- send: (event: E) => Effect.Effect<S, never, never>;
138
+ send: (event: E) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
133
139
  getState: Effect.Effect<S, never, never>;
134
140
  }, never, never>;
135
141
  //#endregion