effect-machine 0.11.0 → 0.13.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 (68) hide show
  1. package/README.md +128 -324
  2. package/dist/actor.d.ts +52 -31
  3. package/dist/actor.js +218 -283
  4. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  5. package/dist/cluster/adapters/in-memory.js +79 -0
  6. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  7. package/dist/cluster/entity-actor-ref.js +33 -0
  8. package/dist/cluster/entity-machine.d.ts +31 -49
  9. package/dist/cluster/entity-machine.js +178 -52
  10. package/dist/cluster/index.d.ts +5 -2
  11. package/dist/cluster/index.js +4 -1
  12. package/dist/cluster/persistence.d.ts +49 -0
  13. package/dist/cluster/persistence.js +18 -0
  14. package/dist/cluster/to-entity.d.ts +9 -3
  15. package/dist/cluster/to-entity.js +16 -4
  16. package/dist/errors.d.ts +25 -17
  17. package/dist/errors.js +10 -5
  18. package/dist/index.d.ts +6 -4
  19. package/dist/index.js +4 -3
  20. package/dist/internal/brands.d.ts +14 -1
  21. package/dist/internal/runtime.d.ts +142 -0
  22. package/dist/internal/runtime.js +357 -0
  23. package/dist/internal/transition.d.ts +10 -4
  24. package/dist/internal/transition.js +24 -12
  25. package/dist/internal/utils.d.ts +42 -6
  26. package/dist/internal/utils.js +27 -1
  27. package/dist/machine.d.ts +89 -55
  28. package/dist/machine.js +80 -68
  29. package/dist/schema.d.ts +35 -34
  30. package/dist/schema.js +33 -4
  31. package/dist/supervision.d.ts +97 -0
  32. package/dist/supervision.js +42 -0
  33. package/dist/testing.d.ts +17 -8
  34. package/dist/testing.js +22 -23
  35. package/package.json +7 -7
  36. package/v3/dist/actor.d.ts +54 -37
  37. package/v3/dist/actor.js +209 -277
  38. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  39. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  40. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  41. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  42. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  43. package/v3/dist/cluster/entity-machine.js +134 -50
  44. package/v3/dist/cluster/index.d.ts +5 -2
  45. package/v3/dist/cluster/index.js +4 -1
  46. package/v3/dist/cluster/persistence.d.ts +48 -0
  47. package/v3/dist/cluster/persistence.js +14 -0
  48. package/v3/dist/cluster/to-entity.d.ts +5 -2
  49. package/v3/dist/cluster/to-entity.js +12 -4
  50. package/v3/dist/errors.d.ts +18 -8
  51. package/v3/dist/errors.js +9 -4
  52. package/v3/dist/index.d.ts +6 -4
  53. package/v3/dist/index.js +3 -2
  54. package/v3/dist/internal/brands.d.ts +15 -1
  55. package/v3/dist/internal/runtime.d.ts +142 -0
  56. package/v3/dist/internal/runtime.js +335 -0
  57. package/v3/dist/internal/transition.d.ts +10 -4
  58. package/v3/dist/internal/transition.js +23 -11
  59. package/v3/dist/internal/utils.d.ts +42 -6
  60. package/v3/dist/internal/utils.js +27 -1
  61. package/v3/dist/machine.d.ts +35 -47
  62. package/v3/dist/machine.js +62 -64
  63. package/v3/dist/schema.d.ts +35 -34
  64. package/v3/dist/schema.js +29 -3
  65. package/v3/dist/supervision.d.ts +97 -0
  66. package/v3/dist/supervision.js +42 -0
  67. package/v3/dist/testing.d.ts +18 -9
  68. package/v3/dist/testing.js +21 -22
package/v3/dist/actor.js CHANGED
@@ -1,9 +1,10 @@
1
- import { Inspector } from "./inspection.js";
2
- import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
3
- import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
1
+ import { ActorStoppedError, DuplicateActorError } from "./errors.js";
2
+ import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
4
3
  import { emitWithTimestamp } from "./internal/inspection.js";
5
- import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
6
- import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect";
4
+ import { Inspector } from "./inspection.js";
5
+ import { materializeMachine } from "./machine.js";
6
+ import { createRuntime } from "./internal/runtime.js";
7
+ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Stream, SubscriptionRef } from "effect";
7
8
  //#region src/actor.ts
8
9
  /**
9
10
  * Actor system: spawning, lifecycle, and event processing.
@@ -11,7 +12,7 @@ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, O
11
12
  * Combines:
12
13
  * - ActorRef interface (running actor handle)
13
14
  * - ActorSystem service (spawn/stop/get actors)
14
- * - Actor creation and event loop
15
+ * - Actor creation (delegates to runtime kernel)
15
16
  */
16
17
  /**
17
18
  * ActorSystem service tag
@@ -26,12 +27,13 @@ const notifyListeners = (listeners, state) => {
26
27
  } catch {}
27
28
  };
28
29
  /**
29
- * Build core ActorRef methods shared between regular and persistent actors.
30
+ * Build core ActorRef methods.
30
31
  */
31
- const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub) => {
32
+ const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, 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
- yield* Queue.offer(eventQueue, {
35
+ const q = yield* Ref.get(eventQueueRef);
36
+ yield* Queue.offer(q, {
35
37
  _tag: "send",
36
38
  event
37
39
  });
@@ -49,7 +51,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
49
51
  }
50
52
  const reply = yield* Deferred.make();
51
53
  pendingReplies.add(reply);
52
- yield* Queue.offer(eventQueue, {
54
+ const q = yield* Ref.get(eventQueueRef);
55
+ yield* Queue.offer(q, {
53
56
  _tag: "call",
54
57
  event,
55
58
  reply
@@ -66,7 +69,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
66
69
  if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
67
70
  const reply = yield* Deferred.make();
68
71
  pendingReplies.add(reply);
69
- yield* Queue.offer(eventQueue, {
72
+ const q = yield* Ref.get(eventQueueRef);
73
+ yield* Queue.offer(q, {
70
74
  _tag: "ask",
71
75
  event,
72
76
  reply
@@ -85,10 +89,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
85
89
  const current = yield* SubscriptionRef.get(stateRef);
86
90
  if (predicate(current)) return current;
87
91
  const done = yield* Deferred.make();
88
- const rt = yield* Effect.runtime();
89
- const runFork = Runtime.runFork(rt);
90
92
  const listener = (state) => {
91
- if (predicate(state)) runFork(Deferred.succeed(done, state));
93
+ if (predicate(state)) Effect.runFork(Deferred.succeed(done, state));
92
94
  };
93
95
  listeners.add(listener);
94
96
  const afterSubscribe = yield* SubscriptionRef.get(stateRef);
@@ -128,12 +130,27 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
128
130
  listeners.delete(fn);
129
131
  };
130
132
  },
133
+ awaitExit: Deferred.await(exitDeferred),
134
+ watch: (other) => other.awaitExit,
135
+ drain: Effect.gen(function* () {
136
+ if (yield* Ref.get(stoppedRef)) return;
137
+ const q = yield* Ref.get(eventQueueRef);
138
+ const done = yield* Deferred.make();
139
+ yield* Queue.offer(q, {
140
+ _tag: "drain",
141
+ done
142
+ });
143
+ yield* Deferred.await(done);
144
+ }).pipe(Effect.asVoid),
131
145
  sync: {
132
146
  send: (event) => {
133
- if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, {
134
- _tag: "send",
135
- event
136
- }));
147
+ if (!Effect.runSync(Ref.get(stoppedRef))) {
148
+ const q = Effect.runSync(Ref.get(eventQueueRef));
149
+ Effect.runSync(Queue.offer(q, {
150
+ _tag: "send",
151
+ event
152
+ }));
153
+ }
137
154
  },
138
155
  stop: () => Effect.runFork(stop),
139
156
  snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
@@ -147,11 +164,13 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
147
164
  };
148
165
  };
149
166
  /**
150
- * Create and start an actor for a machine
167
+ * Create and start an actor for a machine.
168
+ * Uses the shared runtime kernel with lifecycle hooks for actor-specific concerns.
151
169
  */
152
170
  const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
153
171
  const initial = options?.initialState ?? machine.initial;
154
172
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
173
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
155
174
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
156
175
  let system;
157
176
  let implicitSystemScope;
@@ -162,286 +181,180 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
162
181
  implicitSystemScope = scope;
163
182
  }
164
183
  const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
165
- const eventQueue = yield* Queue.unbounded();
166
- const stoppedRef = yield* Ref.make(false);
167
184
  const childrenMap = /* @__PURE__ */ new Map();
168
- const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
169
- if (yield* Ref.get(stoppedRef)) return;
170
- yield* Queue.offer(eventQueue, {
171
- _tag: "send",
172
- event
173
- });
174
- });
175
- const self = {
176
- send: selfSend,
177
- cast: selfSend,
178
- spawn: (childId, childMachine) => Effect.gen(function* () {
179
- const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
180
- childrenMap.set(childId, child);
181
- const maybeScope = yield* Effect.serviceOption(Scope.Scope);
182
- if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
183
- childrenMap.delete(childId);
184
- }));
185
- return child;
186
- })
187
- };
188
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
185
+ const pendingReplies = /* @__PURE__ */ new Set();
186
+ const listeners = /* @__PURE__ */ new Set();
187
+ const transitionsPubSub = yield* PubSub.unbounded();
189
188
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
190
189
  type: "@machine.spawn",
191
190
  actorId: id,
192
191
  initialState: initial,
193
192
  timestamp
194
193
  }));
195
- const stateRef = yield* SubscriptionRef.make(initial);
196
- const listeners = /* @__PURE__ */ new Set();
197
- const backgroundFibers = [];
198
- const initEvent = { _tag: INTERNAL_INIT_EVENT };
199
- const ctx = {
200
- actorId: id,
201
- state: initial,
202
- event: initEvent,
203
- self,
204
- system
205
- };
206
- const { effects: effectSlots } = machine._slots;
207
- for (const bg of machine.backgroundEffects) {
208
- const fiber = yield* Effect.forkDaemon(bg.handler({
209
- actorId: id,
210
- state: initial,
211
- event: initEvent,
212
- self,
213
- effects: effectSlots,
214
- system
215
- }).pipe(Effect.provideService(machine.Context, ctx)));
216
- backgroundFibers.push(fiber);
217
- }
218
- const stateScopeRef = { current: yield* Scope.make() };
219
- yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
220
- if (machine.finalStates.has(initial._tag)) {
221
- yield* Scope.close(stateScopeRef.current, Exit.void);
222
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
223
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
224
- type: "@machine.stop",
225
- actorId: id,
226
- finalState: initial,
227
- timestamp
228
- }));
229
- yield* Ref.set(stoppedRef, true);
230
- if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
231
- 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());
232
- }
233
- const pendingReplies = /* @__PURE__ */ new Set();
234
- const transitionsPubSub = yield* PubSub.unbounded();
235
- const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub));
236
- return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
237
- const finalState = yield* SubscriptionRef.get(stateRef);
238
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
239
- type: "@machine.stop",
240
- actorId: id,
241
- finalState,
242
- timestamp
243
- }));
244
- yield* Ref.set(stoppedRef, true);
245
- yield* Fiber.interrupt(loopFiber);
246
- yield* settlePendingReplies(pendingReplies, id);
247
- yield* Scope.close(stateScopeRef.current, Exit.void);
248
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
249
- if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
250
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
251
- });
252
- /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
253
- const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
254
- const error = new ActorStoppedError({ actorId });
255
- for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
256
- pendingReplies.clear();
257
- });
258
- /**
259
- * Main event loop for the actor.
260
- * Includes postpone buffer — events matching postpone rules are buffered
261
- * and drained after state tag changes (gen_statem semantics).
262
- */
263
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub) {
264
- const postponed = [];
265
- const hasPostponeRules = machine.postponeRules.length > 0;
266
- const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
267
- const event = queued.event;
268
- const currentState = yield* SubscriptionRef.get(stateRef);
269
- if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
270
- postponed.push(queued);
271
- if (queued._tag === "call") {
272
- const postponedResult = {
273
- newState: currentState,
274
- previousState: currentState,
275
- transitioned: false,
276
- lifecycleRan: false,
277
- isFinal: false,
278
- hasReply: false,
279
- reply: void 0,
280
- postponed: true
281
- };
282
- yield* Deferred.succeed(queued.reply, postponedResult);
283
- }
284
- return {
285
- shouldStop: false,
286
- stateChanged: false
287
- };
288
- }
289
- const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
290
- "effect_machine.actor.id": actorId,
291
- "effect_machine.state.current": currentState._tag,
292
- "effect_machine.event.type": event._tag
293
- } })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
294
- switch (queued._tag) {
295
- case "call":
296
- yield* Deferred.succeed(queued.reply, result);
297
- break;
298
- case "ask":
299
- if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
300
- else yield* Deferred.fail(queued.reply, new NoReplyError({
301
- actorId,
302
- eventTag: event._tag
303
- }));
304
- break;
305
- }
306
- if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
307
- fromState: result.previousState,
308
- toState: result.newState,
309
- event
310
- });
311
- return {
312
- shouldStop,
313
- stateChanged: result.lifecycleRan
314
- };
315
- });
316
- while (true) {
317
- const { shouldStop, stateChanged } = yield* processQueued(yield* Queue.take(eventQueue));
318
- if (shouldStop) {
319
- yield* Ref.set(stoppedRef, true);
320
- settlePostponedBuffer(postponed, pendingReplies, actorId);
321
- yield* settlePendingReplies(pendingReplies, actorId);
322
- yield* Scope.close(stateScopeRef.current, Exit.void);
323
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
324
- return;
325
- }
326
- let drainTriggered = stateChanged;
327
- while (drainTriggered && postponed.length > 0) {
328
- drainTriggered = false;
329
- const drained = postponed.splice(0);
330
- for (const entry of drained) {
331
- const drain = yield* processQueued(entry);
332
- if (drain.shouldStop) {
333
- yield* Ref.set(stoppedRef, true);
334
- settlePostponedBuffer(postponed, pendingReplies, actorId);
335
- yield* settlePendingReplies(pendingReplies, actorId);
336
- yield* Scope.close(stateScopeRef.current, Exit.void);
337
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
338
- return;
339
- }
340
- if (drain.stateChanged) drainTriggered = true;
341
- }
342
- }
343
- }
344
- });
345
- /**
346
- * Settle all reply-bearing entries in the postpone buffer on shutdown.
347
- * Call entries already had their Deferred settled with the postponed result
348
- * (so their pendingReplies entry is already removed). Ask/send entries
349
- * with Deferreds are settled via the pendingReplies registry.
350
- */
351
- const settlePostponedBuffer = (postponed, _pendingReplies, _actorId) => {
352
- postponed.length = 0;
353
- };
354
- /**
355
- * Process a single event, returning true if the actor should stop.
356
- * Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
357
- */
358
- const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system) {
359
- yield* emitWithTimestamp(inspector, (timestamp) => ({
360
- type: "@machine.event",
361
- actorId,
362
- state: currentState,
363
- event,
364
- timestamp
365
- }));
366
- const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, inspector === void 0 ? void 0 : {
367
- onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
194
+ const hooks = inspectorValue === void 0 ? void 0 : {
195
+ onSpawnEffect: (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
368
196
  type: "@machine.effect",
369
- actorId,
197
+ actorId: id,
370
198
  effectType: "spawn",
371
199
  state,
372
200
  timestamp
373
201
  })),
374
- onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
202
+ onTransition: (from, to, ev) => emitWithTimestamp(inspectorValue, (timestamp) => ({
375
203
  type: "@machine.transition",
376
- actorId,
204
+ actorId: id,
377
205
  fromState: from,
378
206
  toState: to,
379
207
  event: ev,
380
208
  timestamp
381
209
  })),
382
- onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
210
+ onError: (info) => emitWithTimestamp(inspectorValue, (timestamp) => ({
383
211
  type: "@machine.error",
384
- actorId,
212
+ actorId: id,
385
213
  phase: info.phase,
386
214
  state: info.state,
387
215
  event: info.event,
388
216
  error: Cause.pretty(info.cause),
389
217
  timestamp
390
218
  }))
391
- });
392
- if (!result.transitioned) {
393
- yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
219
+ };
220
+ const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
221
+ value: initial,
222
+ enumerable: true
223
+ } }) : machine;
224
+ const stateRef = yield* SubscriptionRef.make(initial);
225
+ const stoppedRef = yield* Ref.make(false);
226
+ const initialQueue = yield* Queue.unbounded();
227
+ const eventQueueRef = yield* Ref.make(initialQueue);
228
+ const terminalExitDeferred = yield* Deferred.make();
229
+ let stopEmitted = false;
230
+ const runtimeRef = { current: void 0 };
231
+ /** Build lifecycle hooks for a generation */
232
+ const buildLifecycle = () => {
233
+ stopEmitted = false;
394
234
  return {
395
- shouldStop: false,
396
- result
397
- };
398
- }
399
- yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
400
- yield* SubscriptionRef.set(stateRef, result.newState);
401
- notifyListeners(listeners, result.newState);
402
- if (result.lifecycleRan) {
403
- yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
404
- yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
405
- if (result.isFinal) {
406
- yield* emitWithTimestamp(inspector, (timestamp) => ({
407
- type: "@machine.stop",
408
- actorId,
409
- finalState: result.newState,
235
+ onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
236
+ type: "@machine.event",
237
+ actorId: id,
238
+ state,
239
+ event,
410
240
  timestamp
241
+ })) : void 0,
242
+ onStateChange: (result, _event) => Effect.gen(function* () {
243
+ notifyListeners(listeners, result.newState);
244
+ yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
245
+ if (result.lifecycleRan) {
246
+ yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
247
+ yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
248
+ }
249
+ }),
250
+ onProcessed: (result, event) => result.transitioned ? PubSub.publish(transitionsPubSub, {
251
+ fromState: result.previousState,
252
+ toState: result.newState,
253
+ event
254
+ }).pipe(Effect.asVoid) : Effect.void,
255
+ onFinal: inspectorValue !== void 0 ? (state) => Effect.gen(function* () {
256
+ stopEmitted = true;
257
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
258
+ type: "@machine.stop",
259
+ actorId: id,
260
+ finalState: state,
261
+ timestamp
262
+ }));
263
+ }) : void 0,
264
+ onShutdown: () => Effect.gen(function* () {
265
+ if (!stopEmitted) {
266
+ const finalState = yield* SubscriptionRef.get(stateRef);
267
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
268
+ type: "@machine.stop",
269
+ actorId: id,
270
+ finalState,
271
+ timestamp
272
+ }));
273
+ }
274
+ yield* settlePendingReplies(pendingReplies, id);
275
+ }),
276
+ onInitialSpawnEffects: inspectorValue !== void 0 ? (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
277
+ type: "@machine.effect",
278
+ actorId: id,
279
+ effectType: "spawn",
280
+ state,
281
+ timestamp
282
+ })) : void 0
283
+ };
284
+ };
285
+ /** Create a single runtime generation */
286
+ const spawnGeneration = (machineForGen) => Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
287
+ actorId: id,
288
+ hooks,
289
+ skipFinalizer: true,
290
+ cellResources: {
291
+ stateRef,
292
+ stoppedRef,
293
+ eventQueue: currentQueue
294
+ },
295
+ lifecycle: buildLifecycle(),
296
+ wrapProcess: (state, event, inner) => Effect.withSpan("effect-machine.event.process", { attributes: {
297
+ "effect_machine.actor.id": id,
298
+ "effect_machine.state.current": state._tag,
299
+ "effect_machine.event.type": event._tag
300
+ } })(inner.pipe(Effect.tap((r) => Effect.annotateCurrentSpan("effect_machine.transition.matched", r.result.transitioned)))),
301
+ onChildSpawned: (childId, child) => Effect.gen(function* () {
302
+ childrenMap.set(childId, child);
303
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
304
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
305
+ childrenMap.delete(childId);
411
306
  }));
412
- return {
413
- shouldStop: true,
414
- result
415
- };
307
+ })
308
+ })));
309
+ const supervision = options?.supervision;
310
+ const runtime = yield* spawnGeneration(machineWithState);
311
+ runtimeRef.current = runtime;
312
+ let supervisorFiber;
313
+ if (supervision !== void 0) supervisorFiber = yield* Effect.forkDaemon(Effect.gen(function* () {
314
+ const driver = yield* Schedule.driver(supervision.schedule);
315
+ let generation = 0;
316
+ while (true) {
317
+ const currentRuntime = runtimeRef.current;
318
+ if (currentRuntime === void 0) return;
319
+ const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
320
+ if (generationExit._tag !== "Defect") {
321
+ yield* Deferred.succeed(terminalExitDeferred, generationExit);
322
+ return;
323
+ }
324
+ if (supervision.shouldRestart !== void 0 && !supervision.shouldRestart(generationExit)) {
325
+ yield* Deferred.succeed(terminalExitDeferred, generationExit);
326
+ return;
327
+ }
328
+ if ((yield* driver.next(generationExit).pipe(Effect.exit))._tag === "Failure") {
329
+ yield* Deferred.succeed(terminalExitDeferred, generationExit);
330
+ return;
331
+ }
332
+ yield* settlePendingReplies(pendingReplies, id);
333
+ const freshQueue = yield* Queue.unbounded();
334
+ yield* Ref.set(eventQueueRef, freshQueue);
335
+ yield* SubscriptionRef.set(stateRef, machine.initial);
336
+ yield* Ref.set(stoppedRef, false);
337
+ childrenMap.clear();
338
+ runtimeRef.current = yield* spawnGeneration(machine);
339
+ generation++;
340
+ if (options?.onRestart !== void 0) yield* options.onRestart(generation, generationExit);
341
+ notifyListeners(listeners, machine.initial);
416
342
  }
417
- }
418
- return {
419
- shouldStop: false,
420
- result
421
- };
422
- });
423
- /**
424
- * Run spawn effects with actor-specific inspection and tracing.
425
- * Wraps the core runSpawnEffects with inspection events and spans.
426
- * @internal
427
- */
428
- const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
429
- yield* emitWithTimestamp(inspector, (timestamp) => ({
430
- type: "@machine.effect",
431
- actorId,
432
- effectType: "spawn",
433
- state,
434
- timestamp
435
343
  }));
436
- yield* runSpawnEffects(machine, state, event, self, stateScope, system, actorId, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
437
- type: "@machine.error",
438
- actorId,
439
- phase: info.phase,
440
- state: info.state,
441
- event: info.event,
442
- error: Cause.pretty(info.cause),
443
- timestamp
444
- })));
344
+ else yield* Deferred.await(runtime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit)), Effect.catchAllCause(() => Effect.void), Effect.fork);
345
+ return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, Effect.gen(function* () {
346
+ if (supervisorFiber !== void 0) yield* Fiber.interrupt(supervisorFiber);
347
+ const currentRuntime = runtimeRef.current;
348
+ if (currentRuntime !== void 0) yield* currentRuntime.stop;
349
+ yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
350
+ if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
351
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
352
+ });
353
+ /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
354
+ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
355
+ const error = new ActorStoppedError({ actorId });
356
+ for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
357
+ pendingReplies.clear();
445
358
  });
446
359
  /** Notify all system event listeners (sync). */
447
360
  const notifySystemListeners = (listeners, event) => {
@@ -462,7 +375,6 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
462
375
  });
463
376
  return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.zipRight(PubSub.shutdown(eventPubSub)), Effect.asVoid);
464
377
  });
465
- /** Check for duplicate ID, register actor, attach scope cleanup if available */
466
378
  const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
467
379
  if (MutableHashMap.has(actorsMap, id)) {
468
380
  yield* actor.stop;
@@ -481,7 +393,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
481
393
  yield* emitSystemEvent({
482
394
  _tag: "ActorStopped",
483
395
  id,
484
- actor: actorRef
396
+ actor: actorRef,
397
+ exit: { _tag: "Stopped" }
485
398
  });
486
399
  MutableHashMap.remove(actorsMap, id);
487
400
  }
@@ -489,11 +402,24 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
489
402
  }));
490
403
  return actor;
491
404
  });
492
- const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, built) {
405
+ const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
493
406
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
494
- return yield* registerActor(id, yield* createActor(id, built._inner));
407
+ const materialized = materializeMachine(machine, spawnOptions?.slots);
408
+ let actorRef;
409
+ const actor = yield* createActor(id, materialized, {
410
+ supervision: spawnOptions?.supervision,
411
+ onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
412
+ _tag: "ActorRestarted",
413
+ id,
414
+ actor: actorRef,
415
+ generation,
416
+ exit
417
+ }) : Effect.void : void 0
418
+ });
419
+ actorRef = actor;
420
+ return yield* registerActor(id, actor);
495
421
  });
496
- const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
422
+ const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
497
423
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
498
424
  return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
499
425
  });
@@ -505,7 +431,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
505
431
  yield* emitSystemEvent({
506
432
  _tag: "ActorStopped",
507
433
  id,
508
- actor
434
+ actor,
435
+ exit: { _tag: "Stopped" }
509
436
  });
510
437
  yield* actor.stop;
511
438
  return true;
@@ -531,8 +458,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
531
458
  });
532
459
  });
533
460
  /**
461
+ * Create an ActorSystem instance. Must be run in a Scope.
462
+ * @internal — use Default layer for normal usage
463
+ */
464
+ const makeSystem = make;
465
+ /**
534
466
  * Default ActorSystem layer
535
467
  */
536
- const Default = Layer.scoped(ActorSystem, make());
468
+ const Default = Layer.effect(ActorSystem, make());
537
469
  //#endregion
538
- export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
470
+ export { ActorSystem, Default, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
@@ -0,0 +1,15 @@
1
+ import { PersistedEvent, PersistenceAdapter, Snapshot } from "../persistence.js";
2
+ import { Effect, Layer, Ref } from "effect";
3
+
4
+ //#region src/cluster/adapters/in-memory.d.ts
5
+ interface EntityStore {
6
+ snapshot: Snapshot<unknown> | undefined;
7
+ events: Array<PersistedEvent<unknown>>;
8
+ }
9
+ declare const makeInMemoryPersistenceAdapter: Effect.Effect<{
10
+ adapter: PersistenceAdapter;
11
+ storeRef: Ref.Ref<Map<string, EntityStore>>;
12
+ layer: Layer.Layer<PersistenceAdapter, never, never>;
13
+ }, never, never>;
14
+ //#endregion
15
+ export { makeInMemoryPersistenceAdapter };