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