effect-machine 0.13.0 → 0.15.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 (49) hide show
  1. package/README.md +19 -11
  2. package/dist/actor.d.ts +18 -6
  3. package/dist/actor.js +151 -61
  4. package/dist/cluster/entity-machine.d.ts +1 -1
  5. package/dist/cluster/entity-machine.js +2 -1
  6. package/dist/cluster/to-entity.d.ts +1 -1
  7. package/dist/errors.d.ts +9 -2
  8. package/dist/errors.js +8 -2
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.js +4 -4
  11. package/dist/internal/runtime.d.ts +21 -2
  12. package/dist/internal/runtime.js +64 -56
  13. package/dist/internal/transition.d.ts +9 -9
  14. package/dist/internal/transition.js +3 -5
  15. package/dist/machine.d.ts +129 -135
  16. package/dist/machine.js +97 -112
  17. package/dist/schema.d.ts +17 -1
  18. package/dist/schema.js +10 -0
  19. package/dist/slot.d.ts +112 -86
  20. package/dist/slot.js +92 -59
  21. package/dist/testing.d.ts +16 -16
  22. package/dist/testing.js +3 -3
  23. package/package.json +3 -3
  24. package/v3/dist/actor.d.ts +25 -12
  25. package/v3/dist/actor.js +174 -89
  26. package/v3/dist/cluster/entity-machine.d.ts +1 -1
  27. package/v3/dist/cluster/entity-machine.js +1 -0
  28. package/v3/dist/cluster/to-entity.d.ts +1 -1
  29. package/v3/dist/errors.d.ts +12 -3
  30. package/v3/dist/errors.js +10 -4
  31. package/v3/dist/index.d.ts +6 -6
  32. package/v3/dist/index.js +2 -2
  33. package/v3/dist/inspection.d.ts +3 -22
  34. package/v3/dist/inspection.js +1 -15
  35. package/v3/dist/internal/brands.d.ts +4 -8
  36. package/v3/dist/internal/inspection.js +1 -1
  37. package/v3/dist/internal/runtime.d.ts +27 -8
  38. package/v3/dist/internal/runtime.js +91 -61
  39. package/v3/dist/internal/transition.d.ts +10 -10
  40. package/v3/dist/internal/transition.js +8 -10
  41. package/v3/dist/internal/utils.js +5 -1
  42. package/v3/dist/machine.d.ts +160 -120
  43. package/v3/dist/machine.js +118 -115
  44. package/v3/dist/schema.d.ts +25 -11
  45. package/v3/dist/schema.js +18 -5
  46. package/v3/dist/slot.d.ts +112 -86
  47. package/v3/dist/slot.js +92 -59
  48. package/v3/dist/testing.d.ts +16 -16
  49. package/v3/dist/testing.js +7 -7
package/README.md CHANGED
@@ -39,14 +39,14 @@ const CheckoutEvent = Event({
39
39
  Cancel: {},
40
40
  });
41
41
 
42
- const CheckoutEffects = Slot.Effects({
43
- chargeCard: { cartId: Schema.String, totalCents: Schema.Number },
42
+ const CheckoutSlots = Slot.define({
43
+ chargeCard: Slot.fn({ cartId: Schema.String, totalCents: Schema.Number }),
44
44
  });
45
45
 
46
46
  const checkoutMachine = Machine.make({
47
47
  state: CheckoutState,
48
48
  event: CheckoutEvent,
49
- effects: CheckoutEffects,
49
+ slots: CheckoutSlots,
50
50
  initial: CheckoutState.ReviewingCart({ cartId: "cart_123", totalCents: 4200 }),
51
51
  })
52
52
  .on(CheckoutState.ReviewingCart, CheckoutEvent.Submit, ({ state }) =>
@@ -61,8 +61,8 @@ const checkoutMachine = Machine.make({
61
61
  .onAny(CheckoutEvent.Cancel, ({ state }) =>
62
62
  CheckoutState.Failed.derive(state, { reason: "cancelled" }),
63
63
  )
64
- .spawn(CheckoutState.ChargingCard, ({ effects, state }) =>
65
- effects.chargeCard({ cartId: state.cartId, totalCents: state.totalCents }),
64
+ .spawn(CheckoutState.ChargingCard, ({ slots, state }) =>
65
+ slots.chargeCard({ cartId: state.cartId, totalCents: state.totalCents }),
66
66
  )
67
67
  .final(CheckoutState.Confirmed)
68
68
  .final(CheckoutState.Failed);
@@ -86,10 +86,11 @@ const actor =
86
86
  yield *
87
87
  Machine.spawn(checkoutMachine, {
88
88
  slots: {
89
- chargeCard: ({ cartId, totalCents }, { self }) =>
89
+ chargeCard: ({ cartId, totalCents }) =>
90
90
  Effect.gen(function* () {
91
+ const ctx = yield* checkoutMachine.Context;
91
92
  const result = yield* PaymentService.charge(cartId, totalCents);
92
- yield* self.send(
93
+ yield* ctx.self.send(
93
94
  result.ok
94
95
  ? CheckoutEvent.Charged({ receiptId: result.receiptId })
95
96
  : CheckoutEvent.Declined({ reason: result.error }),
@@ -97,6 +98,7 @@ const actor =
97
98
  }),
98
99
  },
99
100
  });
101
+ yield * actor.start;
100
102
  ```
101
103
 
102
104
  The same machine can run with different slot implementations in tests, local apps, or production. Slots are accepted everywhere the machine runs:
@@ -108,16 +110,21 @@ The same machine can run with different slot implementations in tests, local app
108
110
 
109
111
  ## Running Actors
110
112
 
111
- `Machine.spawn` gives you a live actor with a queue and lifecycle management.
113
+ `Machine.spawn` allocates an actor but does not start it. Call `actor.start` to fork the event loop, background effects, and spawn effects. Events sent before `start()` are queued.
112
114
 
113
115
  ```ts
114
116
  const program = Effect.gen(function* () {
115
117
  const actor = yield* Machine.spawn(checkoutMachine, {
116
118
  slots: {
117
- chargeCard: ({ cartId }, { self }) =>
118
- self.send(CheckoutEvent.Charged({ receiptId: `rcpt_${cartId}` })),
119
+ chargeCard: ({ cartId }) =>
120
+ checkoutMachine.Context.pipe(
121
+ Effect.flatMap((ctx) =>
122
+ ctx.self.send(CheckoutEvent.Charged({ receiptId: `rcpt_${cartId}` })),
123
+ ),
124
+ ),
119
125
  },
120
126
  });
127
+ yield* actor.start;
121
128
 
122
129
  yield* actor.send(CheckoutEvent.Submit);
123
130
  const finalState = yield* actor.awaitFinal;
@@ -128,6 +135,7 @@ Effect.runPromise(Effect.scoped(program));
128
135
 
129
136
  Key actor operations:
130
137
 
138
+ - `start` forks the event loop (idempotent, required after `Machine.spawn`)
131
139
  - `send(event)` queues and returns immediately
132
140
  - `call(event)` returns full transition info
133
141
  - `ask(event)` returns a typed domain reply (requires `Event.reply(...)`)
@@ -135,7 +143,7 @@ Key actor operations:
135
143
  - `stop` interrupts now; `drain` processes the remaining queue first
136
144
  - `watch(other)` completes when another actor stops
137
145
 
138
- For named actors or shared lookup, use an actor system:
146
+ For named actors or shared lookup, use an actor system. `system.spawn` auto-starts — no `actor.start` needed:
139
147
 
140
148
  ```ts
141
149
  import { ActorSystemDefault, ActorSystemService } from "effect-machine";
package/dist/actor.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
2
2
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
3
- import { EffectsDef, GuardsDef } from "./slot.js";
3
+ import { ProvideSlots, SlotsDef } from "./slot.js";
4
4
  import { ActorExit, Supervision } from "./supervision.js";
5
5
  import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
6
- import { Machine } from "./machine.js";
6
+ import { Lifecycle, Machine } from "./machine.js";
7
7
  import { RuntimeQueuedEvent } from "./internal/runtime.js";
8
8
  import { Deferred, Effect, Layer, Option, PubSub, Queue, Ref, Scope, ServiceMap, Stream, SubscriptionRef } from "effect";
9
9
 
@@ -54,6 +54,15 @@ interface ActorRef<State extends {
54
54
  readonly state: SubscriptionRef.SubscriptionRef<State>;
55
55
  /** Stop the actor gracefully. */
56
56
  readonly stop: Effect.Effect<void>;
57
+ /**
58
+ * Start the actor — fork event loop, background effects, spawn effects.
59
+ * Idempotent: first caller runs initialization, subsequent callers await completion.
60
+ * Events sent before start() are queued and processed when start() runs.
61
+ *
62
+ * Called automatically by `system.spawn`. For `Machine.spawn`, the caller
63
+ * must call `start` explicitly.
64
+ */
65
+ readonly start: Effect.Effect<void>;
57
66
  /** Get current state snapshot. */
58
67
  readonly snapshot: Effect.Effect<State>;
59
68
  /** Check if current state matches tag. */
@@ -158,8 +167,10 @@ interface ActorSystem {
158
167
  readonly _tag: string;
159
168
  }, E extends {
160
169
  readonly _tag: string;
161
- }, R>(id: string, machine: Machine<S, E, R, any, any, any, any>, options?: {
170
+ }, R, SD extends SlotsDef = Record<string, never>>(id: string, machine: Machine<S, E, R, any, any, SD>, options?: {
162
171
  readonly supervision?: Supervision.Policy;
172
+ readonly slots?: ProvideSlots<SD, any>;
173
+ readonly lifecycle?: Lifecycle<S, E>;
163
174
  }) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
164
175
  /**
165
176
  * Get an existing actor by ID
@@ -202,7 +213,7 @@ declare const buildActorRefCore: <S extends {
202
213
  readonly _tag: string;
203
214
  }, E extends {
204
215
  readonly _tag: string;
205
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueueRef: Ref.Ref<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>> | undefined, exitDeferred: Deferred.Deferred<ActorExit<S>, never>) => ActorRef<S, E>;
216
+ }, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueueRef: Ref.Ref<Queue.Queue<QueuedEvent<E>>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, start: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub: PubSub.PubSub<TransitionInfo<S, E>> | undefined, exitDeferred: Deferred.Deferred<ActorExit<S>, never>) => ActorRef<S, E>;
206
217
  /**
207
218
  * Create and start an actor for a machine.
208
219
  * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
@@ -211,9 +222,10 @@ declare const createActor: <S extends {
211
222
  readonly _tag: string;
212
223
  }, E extends {
213
224
  readonly _tag: string;
214
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, options?: {
225
+ }, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, options?: {
215
226
  initialState?: S;
216
- supervision?: Supervision.Policy; /** @internal Called by system after each restart — emits ActorRestarted system event */
227
+ supervision?: Supervision.Policy;
228
+ lifecycle?: Lifecycle<S, E>; /** @internal Called by system after each restart — emits ActorRestarted system event */
217
229
  onRestart?: (generation: number, exit: ActorExit<unknown>) => Effect.Effect<void>;
218
230
  } | undefined) => Effect.Effect<ActorRef<S, E>, never, never>;
219
231
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
package/dist/actor.js CHANGED
@@ -1,7 +1,8 @@
1
- import { Inspector } from "./inspection.js";
1
+ import { ActorStoppedError, DuplicateActorError } from "./errors.js";
2
2
  import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
3
3
  import { emitWithTimestamp } from "./internal/inspection.js";
4
- import { ActorStoppedError, DuplicateActorError } from "./errors.js";
4
+ import { Inspector } from "./inspection.js";
5
+ import { materializeMachine } from "./machine.js";
5
6
  import { createRuntime } from "./internal/runtime.js";
6
7
  import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
7
8
  //#region src/actor.ts
@@ -28,7 +29,7 @@ const notifyListeners = (listeners, state) => {
28
29
  /**
29
30
  * Build core ActorRef methods.
30
31
  */
31
- const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub, exitDeferred) => {
32
+ const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, start, system, childrenMap, pendingReplies, transitionsPubSub, exitDeferred) => {
32
33
  const send = Effect.fn("effect-machine.actor.send")(function* (event) {
33
34
  if (yield* Ref.get(stoppedRef)) return;
34
35
  const q = yield* Ref.get(eventQueueRef);
@@ -115,6 +116,7 @@ const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, lis
115
116
  ask,
116
117
  state: stateRef,
117
118
  stop,
119
+ start,
118
120
  snapshot,
119
121
  matches,
120
122
  can,
@@ -190,33 +192,87 @@ const buildInspectionHooks = (actorId, inspector) => ({
190
192
  }))
191
193
  });
192
194
  /**
195
+ * Resolve actor system from context, creating an implicit one if none exists.
196
+ * @internal
197
+ */
198
+ const resolveActorSystem = Effect.fn("effect-machine.resolveActorSystem")(function* () {
199
+ const existingSystem = yield* Effect.serviceOption(ActorSystem);
200
+ if (Option.isSome(existingSystem)) return {
201
+ system: existingSystem.value,
202
+ implicitSystemScope: void 0
203
+ };
204
+ const scope = yield* Scope.make();
205
+ return {
206
+ system: yield* make().pipe(Effect.provideService(Scope.Scope, scope)),
207
+ implicitSystemScope: scope
208
+ };
209
+ });
210
+ /**
211
+ * Run the supervision loop for a supervised actor.
212
+ * Observes exit deferred, applies restart policy, resets cell resources on restart.
213
+ * @internal
214
+ */
215
+ const runSupervisionLoop = (params) => Effect.gen(function* () {
216
+ const step = yield* Schedule.toStepWithSleep(params.supervision.schedule);
217
+ while (true) {
218
+ const currentRuntime = params.runtimeRef.current;
219
+ if (currentRuntime === void 0) return;
220
+ const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
221
+ if (generationExit._tag !== "Defect") {
222
+ yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
223
+ return;
224
+ }
225
+ if (params.supervision.shouldRestart !== void 0 && !params.supervision.shouldRestart(generationExit)) {
226
+ yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
227
+ return;
228
+ }
229
+ if ((yield* step(generationExit).pipe(Effect.exit))._tag === "Failure") {
230
+ yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
231
+ return;
232
+ }
233
+ const nextGeneration = params.generationRef.get() + 1;
234
+ params.generationRef.set(nextGeneration);
235
+ let restartState = params.machine.initial;
236
+ if (params.lifecycle?.recovery !== void 0) {
237
+ const resolved = yield* params.lifecycle.recovery.resolve({
238
+ actorId: params.id,
239
+ generation: nextGeneration,
240
+ machineInitial: params.machine.initial
241
+ });
242
+ if (Option.isSome(resolved)) restartState = resolved.value;
243
+ }
244
+ yield* settlePendingReplies(params.pendingReplies, params.id);
245
+ const freshQueue = yield* Queue.unbounded();
246
+ yield* Ref.set(params.eventQueueRef, freshQueue);
247
+ yield* SubscriptionRef.set(params.stateRef, restartState);
248
+ yield* Ref.set(params.stoppedRef, false);
249
+ params.childrenMap.clear();
250
+ const machineForRestart = restartState !== params.machine.initial ? Object.create(params.machine, { initial: {
251
+ value: restartState,
252
+ enumerable: true
253
+ } }) : params.machine;
254
+ const newRuntime = yield* params.spawnGeneration(machineForRestart);
255
+ params.runtimeRef.current = newRuntime;
256
+ yield* newRuntime.start;
257
+ if (params.onRestart !== void 0) yield* params.onRestart(nextGeneration, generationExit);
258
+ notifyListeners(params.listeners, restartState);
259
+ }
260
+ });
261
+ /**
193
262
  * Create and start an actor for a machine.
194
263
  * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
195
264
  */
196
265
  const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
266
+ const lifecycle = options?.lifecycle;
197
267
  const initial = options?.initialState ?? machine.initial;
198
268
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
199
269
  yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
200
- const existingSystem = yield* Effect.serviceOption(ActorSystem);
201
- let system;
202
- let implicitSystemScope;
203
- if (Option.isSome(existingSystem)) system = existingSystem.value;
204
- else {
205
- const scope = yield* Scope.make();
206
- system = yield* make().pipe(Effect.provideService(Scope.Scope, scope));
207
- implicitSystemScope = scope;
208
- }
270
+ const { system, implicitSystemScope } = yield* resolveActorSystem();
209
271
  const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
210
272
  const childrenMap = /* @__PURE__ */ new Map();
211
273
  const pendingReplies = /* @__PURE__ */ new Set();
212
274
  const listeners = /* @__PURE__ */ new Set();
213
275
  const transitionsPubSub = yield* PubSub.unbounded();
214
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
215
- type: "@machine.spawn",
216
- actorId: id,
217
- initialState: initial,
218
- timestamp
219
- }));
220
276
  const hooks = inspectorValue !== void 0 ? buildInspectionHooks(id, inspectorValue) : void 0;
221
277
  const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
222
278
  value: initial,
@@ -228,9 +284,11 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
228
284
  const eventQueueRef = yield* Ref.make(initialQueue);
229
285
  const terminalExitDeferred = yield* Deferred.make();
230
286
  let stopEmitted = false;
287
+ let generation = 0;
231
288
  const runtimeRef = { current: void 0 };
289
+ const supervisorFiberRef = { current: void 0 };
232
290
  /** Build lifecycle hooks for a generation */
233
- const buildLifecycle = () => {
291
+ const buildRuntimeLifecycle = () => {
234
292
  stopEmitted = false;
235
293
  return {
236
294
  onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
@@ -240,8 +298,18 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
240
298
  event,
241
299
  timestamp
242
300
  })) : void 0,
243
- onStateChange: (result, _event) => Effect.gen(function* () {
301
+ onStateChange: (result, event) => Effect.gen(function* () {
244
302
  notifyListeners(listeners, result.newState);
303
+ if (lifecycle?.durability !== void 0 && result.transitioned) {
304
+ const durability = lifecycle.durability;
305
+ if (durability.shouldSave === void 0 || durability.shouldSave(result.newState, result.previousState)) yield* durability.save({
306
+ actorId: id,
307
+ generation,
308
+ previousState: result.previousState,
309
+ nextState: result.newState,
310
+ event
311
+ });
312
+ }
245
313
  yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
246
314
  if (result.lifecycleRan) {
247
315
  yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
@@ -293,7 +361,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
293
361
  stoppedRef,
294
362
  eventQueue: currentQueue
295
363
  },
296
- lifecycle: buildLifecycle(),
364
+ lifecycle: buildRuntimeLifecycle(),
297
365
  wrapProcess: (state, event, inner) => Effect.withSpan("effect-machine.event.process", { attributes: {
298
366
  "effect_machine.actor.id": id,
299
367
  "effect_machine.state.current": state._tag,
@@ -307,49 +375,67 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
307
375
  }));
308
376
  })
309
377
  })));
310
- const runtime = yield* spawnGeneration(machineWithState);
311
- runtimeRef.current = runtime;
378
+ runtimeRef.current = yield* spawnGeneration(machineWithState);
312
379
  const supervision = options?.supervision;
313
- let supervisorFiber;
314
- if (supervision !== void 0) supervisorFiber = yield* Effect.forkDetach(Effect.gen(function* () {
315
- const step = yield* Schedule.toStepWithSleep(supervision.schedule);
316
- let generation = 0;
317
- while (true) {
318
- const currentRuntime = runtimeRef.current;
319
- if (currentRuntime === void 0) return;
320
- const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
321
- if (generationExit._tag !== "Defect") {
322
- yield* Deferred.succeed(terminalExitDeferred, generationExit);
323
- return;
324
- }
325
- if (supervision.shouldRestart !== void 0 && !supervision.shouldRestart(generationExit)) {
326
- yield* Deferred.succeed(terminalExitDeferred, generationExit);
327
- return;
328
- }
329
- if ((yield* step(generationExit).pipe(Effect.exit))._tag === "Failure") {
330
- yield* Deferred.succeed(terminalExitDeferred, generationExit);
331
- return;
332
- }
333
- yield* settlePendingReplies(pendingReplies, id);
334
- const freshQueue = yield* Queue.unbounded();
335
- yield* Ref.set(eventQueueRef, freshQueue);
336
- yield* SubscriptionRef.set(stateRef, machine.initial);
337
- yield* Ref.set(stoppedRef, false);
338
- childrenMap.clear();
339
- runtimeRef.current = yield* spawnGeneration(machine);
340
- generation++;
341
- if (options?.onRestart !== void 0) yield* options.onRestart(generation, generationExit);
342
- notifyListeners(listeners, machine.initial);
343
- }
344
- }));
345
- else yield* Effect.forkDetach(Deferred.await(runtime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
346
- return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, Effect.gen(function* () {
347
- if (supervisorFiber !== void 0) yield* Fiber.interrupt(supervisorFiber);
380
+ const stop = Effect.gen(function* () {
381
+ if (supervisorFiberRef.current !== void 0) yield* Fiber.interrupt(supervisorFiberRef.current);
348
382
  const currentRuntime = runtimeRef.current;
349
383
  if (currentRuntime !== void 0) yield* currentRuntime.stop;
350
384
  yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
351
385
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
352
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
386
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid);
387
+ const isHydrated = options?.initialState !== void 0;
388
+ return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, Effect.gen(function* () {
389
+ if (lifecycle?.recovery !== void 0 && !isHydrated) {
390
+ const resolved = yield* lifecycle.recovery.resolve({
391
+ actorId: id,
392
+ generation,
393
+ machineInitial: machine.initial
394
+ });
395
+ if (Option.isSome(resolved)) {
396
+ yield* SubscriptionRef.set(stateRef, resolved.value);
397
+ runtimeRef.current = yield* spawnGeneration(Object.create(machine, { initial: {
398
+ value: resolved.value,
399
+ enumerable: true
400
+ } }));
401
+ }
402
+ }
403
+ const currentState = yield* SubscriptionRef.get(stateRef);
404
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
405
+ type: "@machine.spawn",
406
+ actorId: id,
407
+ initialState: currentState,
408
+ timestamp
409
+ }));
410
+ if (supervision !== void 0) supervisorFiberRef.current = yield* Effect.forkDetach(runSupervisionLoop({
411
+ supervision,
412
+ machine,
413
+ id,
414
+ runtimeRef,
415
+ terminalExitDeferred,
416
+ pendingReplies,
417
+ eventQueueRef,
418
+ stateRef,
419
+ stoppedRef,
420
+ childrenMap,
421
+ listeners,
422
+ spawnGeneration,
423
+ lifecycle,
424
+ generationRef: {
425
+ get: () => generation,
426
+ set: (g) => {
427
+ generation = g;
428
+ }
429
+ },
430
+ onRestart: options?.onRestart
431
+ }));
432
+ else {
433
+ const currentRuntime = runtimeRef.current;
434
+ if (currentRuntime !== void 0) yield* Effect.forkDetach(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
435
+ }
436
+ const currentRuntime = runtimeRef.current;
437
+ if (currentRuntime !== void 0) yield* currentRuntime.start;
438
+ }).pipe(Effect.withSpan("effect-machine.actor.start"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
353
439
  });
354
440
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
355
441
  const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
@@ -406,9 +492,11 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
406
492
  });
407
493
  const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
408
494
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
495
+ const materialized = spawnOptions?.slots !== void 0 ? materializeMachine(machine, spawnOptions.slots) : machine;
409
496
  let actorRef;
410
- const actor = yield* createActor(id, machine, {
497
+ const actor = yield* createActor(id, materialized, {
411
498
  supervision: spawnOptions?.supervision,
499
+ lifecycle: spawnOptions?.lifecycle,
412
500
  onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
413
501
  _tag: "ActorRestarted",
414
502
  id,
@@ -418,7 +506,9 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
418
506
  }) : Effect.void : void 0
419
507
  });
420
508
  actorRef = actor;
421
- return yield* registerActor(id, actor);
509
+ yield* registerActor(id, actor);
510
+ yield* actor.start.pipe(Effect.catchCause((cause) => actor.stop.pipe(Effect.andThen(Effect.failCause(cause)))));
511
+ return actor;
422
512
  });
423
513
  const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
424
514
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
@@ -65,7 +65,7 @@ declare const EntityMachine: {
65
65
  readonly _tag: string;
66
66
  }, E extends {
67
67
  readonly _tag: string;
68
- }, R, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, any, any, any, any>, options?: EntityMachineOptions<S, E>) => Layer.Layer<never, never, R>;
68
+ }, R, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, any, any, any>, options?: EntityMachineOptions<S, E>) => Layer.Layer<never, never, R>;
69
69
  };
70
70
  //#endregion
71
71
  export { EntityMachine, EntityMachineOptions };
@@ -1,6 +1,6 @@
1
+ import { replay } from "../machine.js";
1
2
  import { createRuntime } from "../internal/runtime.js";
2
3
  import { ActorSystem, makeSystem } from "../actor.js";
3
- import { replay } from "../machine.js";
4
4
  import { PersistenceAdapter } from "./persistence.js";
5
5
  import { Clock, Effect, Option, Queue, Ref, Stream, SubscriptionRef } from "effect";
6
6
  import { Entity } from "effect/unstable/cluster";
@@ -60,6 +60,7 @@ const EntityMachine = { layer: (entity, machine, options) => {
60
60
  eventQueue
61
61
  }
62
62
  });
63
+ yield* runtime.start;
63
64
  if (persistCtx.adapter !== void 0) {
64
65
  const { adapter: pAdapter, key } = persistCtx;
65
66
  const strategy = persistence?.strategy ?? "snapshot";
@@ -61,7 +61,7 @@ declare const toEntity: <S extends {
61
61
  readonly _tag: string;
62
62
  }, E extends {
63
63
  readonly _tag: string;
64
- }, R>(machine: Machine<S, E, R, any, any, any, any>, options: ToEntityOptions) => Entity.Entity<string, Rpc.Rpc<"Send", Schema.Struct<{
64
+ }, R>(machine: Machine<S, E, R, any, any, any>, options: ToEntityOptions) => Entity.Entity<string, Rpc.Rpc<"Send", Schema.Struct<{
65
65
  event: Schema.Schema<E>;
66
66
  }>, Schema.Schema<S>, Schema.Never, never, never> | Rpc.Rpc<"Ask", Schema.Struct<{
67
67
  event: Schema.Schema<E>;
package/dist/errors.d.ts CHANGED
@@ -24,7 +24,7 @@ declare const MissingMatchHandlerError_base: Schema.ErrorClass<MissingMatchHandl
24
24
  declare class MissingMatchHandlerError extends MissingMatchHandlerError_base {}
25
25
  declare const SlotProvisionError_base: Schema.ErrorClass<SlotProvisionError, Schema.TaggedStruct<"SlotProvisionError", {
26
26
  readonly slotName: Schema.String;
27
- readonly slotType: Schema.Literals<readonly ["guard", "effect"]>;
27
+ readonly slotType: Schema.Literal<"slot">;
28
28
  }>, _$effect_Cause0.YieldableError>;
29
29
  /** Slot handler not found at runtime (internal error) */
30
30
  declare class SlotProvisionError extends SlotProvisionError_base {}
@@ -55,6 +55,13 @@ declare const PersistenceError_base: Schema.ErrorClass<PersistenceError, Schema.
55
55
  }>, _$effect_Cause0.YieldableError>;
56
56
  /** Persistence adapter operation failed */
57
57
  declare class PersistenceError extends PersistenceError_base {}
58
+ declare const SlotCodecError_base: Schema.ErrorClass<SlotCodecError, Schema.TaggedStruct<"SlotCodecError", {
59
+ readonly slotName: Schema.String;
60
+ readonly phase: Schema.Literals<readonly ["input", "output"]>;
61
+ readonly message: Schema.String;
62
+ }>, _$effect_Cause0.YieldableError>;
63
+ /** Slot input/output schema validation failed */
64
+ declare class SlotCodecError extends SlotCodecError_base {}
58
65
  declare const VersionConflictError_base: Schema.ErrorClass<VersionConflictError, Schema.TaggedStruct<"VersionConflictError", {
59
66
  readonly expected: Schema.Number;
60
67
  readonly actual: Schema.Number;
@@ -62,4 +69,4 @@ declare const VersionConflictError_base: Schema.ErrorClass<VersionConflictError,
62
69
  /** Optimistic locking failure — stored version doesn't match expected */
63
70
  declare class VersionConflictError extends VersionConflictError_base {}
64
71
  //#endregion
65
- export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, VersionConflictError };
72
+ export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotCodecError, SlotProvisionError, VersionConflictError };
package/dist/errors.js CHANGED
@@ -21,7 +21,7 @@ var MissingMatchHandlerError = class extends Schema.TaggedErrorClass()("MissingM
21
21
  /** Slot handler not found at runtime (internal error) */
22
22
  var SlotProvisionError = class extends Schema.TaggedErrorClass()("SlotProvisionError", {
23
23
  slotName: Schema.String,
24
- slotType: Schema.Literals(["guard", "effect"])
24
+ slotType: Schema.Literal("slot")
25
25
  }) {};
26
26
  /** Slot provision validation failed — missing or extra handlers */
27
27
  var ProvisionValidationError = class extends Schema.TaggedErrorClass()("ProvisionValidationError", {
@@ -39,10 +39,16 @@ var NoReplyError = class extends Schema.TaggedErrorClass()("NoReplyError", {
39
39
  }) {};
40
40
  /** Persistence adapter operation failed */
41
41
  var PersistenceError = class extends Schema.TaggedErrorClass()("PersistenceError", { message: Schema.String }) {};
42
+ /** Slot input/output schema validation failed */
43
+ var SlotCodecError = class extends Schema.TaggedErrorClass()("SlotCodecError", {
44
+ slotName: Schema.String,
45
+ phase: Schema.Literals(["input", "output"]),
46
+ message: Schema.String
47
+ }) {};
42
48
  /** Optimistic locking failure — stored version doesn't match expected */
43
49
  var VersionConflictError = class extends Schema.TaggedErrorClass()("VersionConflictError", {
44
50
  expected: Schema.Number,
45
51
  actual: Schema.Number
46
52
  }) {};
47
53
  //#endregion
48
- export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, VersionConflictError };
54
+ export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotCodecError, SlotProvisionError, VersionConflictError };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { DeferReplyResult, ReplyResult } from "./internal/utils.js";
2
2
  import { Event, MachineEventSchema, MachineStateSchema, ReplyFields, State } from "./schema.js";
3
- import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, VersionConflictError } from "./errors.js";
4
- import { EffectHandlers, EffectSlot, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlot, GuardSlots, GuardsDef, GuardsSchema, MachineContext, Slot } from "./slot.js";
3
+ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotCodecError, SlotProvisionError, VersionConflictError } from "./errors.js";
4
+ import { HasSlotKeys, MachineContext, ProvideSlots, Slot, SlotCall, SlotCalls, SlotFnDef, SlotHandler, SlotInvocation, SlotRequest, SlotResult, SlotsDef, SlotsSchema } from "./slot.js";
5
5
  import { ActorExit, CellPhase, DefectPhase, Supervision } from "./supervision.js";
6
6
  import { ProcessEventResult } from "./internal/transition.js";
7
- import { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, machine_d_exports } from "./machine.js";
7
+ import { BackgroundEffect, Durability, DurabilityCommit, HandlerContext, Lifecycle, Machine, MachineRef, MakeConfig, Recovery, RecoveryContext, SpawnEffect, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, machine_d_exports } from "./machine.js";
8
8
  import { ActorRef, ActorRefSync, ActorSystem, Default, SystemEvent, SystemEventListener, TransitionInfo } from "./actor.js";
9
9
  import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
10
10
  import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
11
- export { ActorExit, type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type CellPhase, type DefectPhase, type DeferReplyResult, 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, PersistenceError, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type ReplyFields, type ReplyResult, type SimulationResult, Slot, type SlotContext, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, Supervision, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TimeoutConfig, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
11
+ export { ActorExit, type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type CellPhase, type DefectPhase, type DeferReplyResult, DuplicateActorError, type Durability, type DurabilityCommit, type EffectEvent, type ErrorEvent, Event, type EventReceivedEvent, type HandlerContext, type HasSlotKeys, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, type Lifecycle, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, type ProcessEventResult, type ProvideSlots, ProvisionValidationError, type Recovery, type RecoveryContext, type ReplyFields, type ReplyResult, type SimulationResult, Slot, type SlotCall, type SlotCalls, SlotCodecError, type SlotFnDef, type SlotHandler, type SlotInvocation, SlotProvisionError, type SlotRequest, type SlotResult, type SlotsDef, type SlotsSchema, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, Supervision, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TimeoutConfig, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
+ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotCodecError, SlotProvisionError, VersionConflictError } from "./errors.js";
1
2
  import { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
2
- import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, VersionConflictError } from "./errors.js";
3
- import { ActorExit, Supervision } from "./supervision.js";
4
- import { ActorSystem, Default } from "./actor.js";
5
3
  import { Slot } from "./slot.js";
6
4
  import { machine_exports } from "./machine.js";
5
+ import { ActorExit, Supervision } from "./supervision.js";
6
+ import { ActorSystem, Default } from "./actor.js";
7
7
  import { Event, State } from "./schema.js";
8
8
  import { assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
9
- export { ActorExit, ActorStoppedError, Default as ActorSystemDefault, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, Slot, SlotProvisionError, State, Supervision, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
9
+ export { ActorExit, ActorStoppedError, Default as ActorSystemDefault, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, Slot, SlotCodecError, SlotProvisionError, State, Supervision, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
@@ -1,5 +1,5 @@
1
1
  import { NoReplyError } from "../errors.js";
2
- import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
2
+ import { MachineContext, SlotsDef } from "../slot.js";
3
3
  import { ActorExit } from "../supervision.js";
4
4
  import { ProcessEventHooks, ProcessEventResult } from "./transition.js";
5
5
  import { Machine, MachineRef } from "../machine.js";
@@ -55,6 +55,12 @@ interface RuntimeHandle<S, E> {
55
55
  readonly isStopped: Effect.Effect<boolean>;
56
56
  /** Stop the runtime (interrupt event loop, clean up) */
57
57
  readonly stop: Effect.Effect<void>;
58
+ /**
59
+ * Start the runtime — fork event loop, background effects, spawn effects.
60
+ * Idempotent: first caller runs initialization, subsequent callers await completion.
61
+ * Events sent before start() are queued and processed when start() runs.
62
+ */
63
+ readonly start: Effect.Effect<void>;
58
64
  /** @internal — raw event queue for direct enqueue (actor.ts uses this for pendingReplies tracking) */
59
65
  readonly _queue: Queue.Queue<RuntimeQueuedEvent<E>>;
60
66
  /** @internal — stopped ref for direct access */
@@ -137,6 +143,19 @@ declare const createRuntime: <S extends {
137
143
  readonly _tag: string;
138
144
  }, E extends {
139
145
  readonly _tag: string;
140
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, any, any, GD, EFD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<RuntimeHandle<S, E>, never, Scope.Scope | Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
146
+ }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<{
147
+ stop: Effect.Effect<void, never, never>;
148
+ start: Effect.Effect<void, unknown, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
149
+ send: (event: E) => Effect.Effect<void>;
150
+ sendWait: (event: E) => Effect.Effect<void, unknown>;
151
+ ask: (event: E) => Effect.Effect<unknown, NoReplyError>;
152
+ getState: Effect.Effect<S, never, never>;
153
+ stateRef: SubscriptionRef.SubscriptionRef<S>;
154
+ isStopped: Effect.Effect<boolean>;
155
+ _queue: Queue.Queue<RuntimeQueuedEvent<E>, never>;
156
+ _stoppedRef: Ref.Ref<boolean>;
157
+ exitDeferred: Deferred.Deferred<ActorExit<S>, never>;
158
+ actorScope: Scope.Closeable;
159
+ }, never, Scope.Scope>;
141
160
  //#endregion
142
161
  export { ProcessQueuedResult, RuntimeCellResources, RuntimeConfig, RuntimeHandle, RuntimeLifecycleHooks, RuntimeQueuedEvent, createRuntime };