effect-machine 0.9.0 → 0.11.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 (73) hide show
  1. package/README.md +118 -55
  2. package/dist/actor.d.ts +77 -179
  3. package/dist/actor.js +161 -113
  4. package/dist/cluster/entity-machine.js +5 -3
  5. package/dist/errors.d.ts +12 -1
  6. package/dist/errors.js +8 -1
  7. package/dist/index.d.ts +4 -8
  8. package/dist/index.js +2 -7
  9. package/dist/internal/transition.d.ts +27 -3
  10. package/dist/internal/transition.js +38 -9
  11. package/dist/internal/utils.d.ts +7 -2
  12. package/dist/internal/utils.js +1 -5
  13. package/dist/machine.d.ts +94 -35
  14. package/dist/machine.js +128 -13
  15. package/dist/testing.js +57 -3
  16. package/package.json +10 -9
  17. package/v3/dist/actor.d.ts +210 -0
  18. package/{dist-v3 → v3/dist}/actor.js +198 -117
  19. package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -1
  20. package/{dist-v3 → v3/dist}/cluster/entity-machine.js +8 -6
  21. package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
  22. package/{dist-v3 → v3/dist}/cluster/to-entity.js +1 -1
  23. package/v3/dist/errors.d.ts +76 -0
  24. package/{dist-v3 → v3/dist}/errors.js +9 -2
  25. package/v3/dist/index.d.ts +9 -0
  26. package/v3/dist/index.js +8 -0
  27. package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
  28. package/v3/dist/inspection.js +156 -0
  29. package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
  30. package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
  31. package/v3/dist/internal/inspection.js +20 -0
  32. package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
  33. package/{dist-v3 → v3/dist}/internal/transition.js +47 -15
  34. package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
  35. package/{dist-v3 → v3/dist}/internal/utils.js +2 -6
  36. package/{dist-v3 → v3/dist}/machine.d.ts +113 -40
  37. package/{dist-v3 → v3/dist}/machine.js +191 -15
  38. package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
  39. package/{dist-v3 → v3/dist}/schema.js +5 -2
  40. package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
  41. package/{dist-v3 → v3/dist}/slot.js +1 -1
  42. package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
  43. package/{dist-v3 → v3/dist}/testing.js +60 -6
  44. package/dist/persistence/adapter.d.ts +0 -135
  45. package/dist/persistence/adapter.js +0 -25
  46. package/dist/persistence/adapters/in-memory.d.ts +0 -32
  47. package/dist/persistence/adapters/in-memory.js +0 -174
  48. package/dist/persistence/index.d.ts +0 -5
  49. package/dist/persistence/index.js +0 -5
  50. package/dist/persistence/persistent-actor.d.ts +0 -50
  51. package/dist/persistence/persistent-actor.js +0 -368
  52. package/dist/persistence/persistent-machine.d.ts +0 -105
  53. package/dist/persistence/persistent-machine.js +0 -22
  54. package/dist-v3/actor.d.ts +0 -291
  55. package/dist-v3/errors.d.ts +0 -27
  56. package/dist-v3/index.d.ts +0 -12
  57. package/dist-v3/index.js +0 -13
  58. package/dist-v3/inspection.js +0 -48
  59. package/dist-v3/internal/inspection.js +0 -13
  60. package/dist-v3/persistence/adapter.d.ts +0 -125
  61. package/dist-v3/persistence/adapter.js +0 -25
  62. package/dist-v3/persistence/adapters/in-memory.d.ts +0 -32
  63. package/dist-v3/persistence/adapters/in-memory.js +0 -174
  64. package/dist-v3/persistence/index.d.ts +0 -5
  65. package/dist-v3/persistence/index.js +0 -5
  66. package/dist-v3/persistence/persistent-actor.d.ts +0 -49
  67. package/dist-v3/persistence/persistent-actor.js +0 -365
  68. package/dist-v3/persistence/persistent-machine.d.ts +0 -105
  69. package/dist-v3/persistence/persistent-machine.js +0 -22
  70. /package/{dist-v3 → v3/dist}/_virtual/_rolldown/runtime.js +0 -0
  71. /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
  72. /package/{dist-v3 → v3/dist}/cluster/index.js +0 -0
  73. /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
package/dist/actor.js CHANGED
@@ -1,11 +1,8 @@
1
1
  import { Inspector } from "./inspection.js";
2
2
  import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
3
- import { DuplicateActorError } from "./errors.js";
4
- import { isPersistentMachine } from "./persistence/persistent-machine.js";
3
+ import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
5
4
  import { emitWithTimestamp } from "./internal/inspection.js";
6
- import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
7
- import { PersistenceAdapterTag, PersistenceError } from "./persistence/adapter.js";
8
- import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
5
+ import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
9
6
  import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
10
7
  //#region src/actor.ts
11
8
  /**
@@ -29,14 +26,17 @@ const notifyListeners = (listeners, state) => {
29
26
  } catch {}
30
27
  };
31
28
  /**
32
- * Build core ActorRef methods shared between regular and persistent actors.
29
+ * Build core ActorRef methods.
33
30
  */
34
- const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap) => {
31
+ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub) => {
35
32
  const send = Effect.fn("effect-machine.actor.send")(function* (event) {
36
33
  if (yield* Ref.get(stoppedRef)) return;
37
- yield* Queue.offer(eventQueue, { event });
34
+ yield* Queue.offer(eventQueue, {
35
+ _tag: "send",
36
+ event
37
+ });
38
38
  });
39
- const dispatch = Effect.fn("effect-machine.actor.dispatch")(function* (event) {
39
+ const call = Effect.fn("effect-machine.actor.call")(function* (event) {
40
40
  if (yield* Ref.get(stoppedRef)) {
41
41
  const currentState = yield* SubscriptionRef.get(stateRef);
42
42
  return {
@@ -48,11 +48,30 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
48
48
  };
49
49
  }
50
50
  const reply = yield* Deferred.make();
51
+ pendingReplies.add(reply);
51
52
  yield* Queue.offer(eventQueue, {
53
+ _tag: "call",
52
54
  event,
53
55
  reply
54
56
  });
55
- return yield* Deferred.await(reply);
57
+ return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))), Effect.catchTag("ActorStoppedError", () => SubscriptionRef.get(stateRef).pipe(Effect.map((currentState) => ({
58
+ newState: currentState,
59
+ previousState: currentState,
60
+ transitioned: false,
61
+ lifecycleRan: false,
62
+ isFinal: machine.finalStates.has(currentState._tag)
63
+ })))));
64
+ });
65
+ const ask = Effect.fn("effect-machine.actor.ask")(function* (event) {
66
+ if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
67
+ const reply = yield* Deferred.make();
68
+ pendingReplies.add(reply);
69
+ yield* Queue.offer(eventQueue, {
70
+ _tag: "ask",
71
+ event,
72
+ reply
73
+ });
74
+ return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))));
56
75
  });
57
76
  const snapshot = SubscriptionRef.get(stateRef).pipe(Effect.withSpan("effect-machine.actor.snapshot"));
58
77
  const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
@@ -88,32 +107,39 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
88
107
  return {
89
108
  id,
90
109
  send,
110
+ cast: send,
111
+ call,
112
+ ask,
91
113
  state: stateRef,
92
114
  stop,
93
- stopSync: () => Effect.runFork(stop),
94
115
  snapshot,
95
- snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
96
116
  matches,
97
- matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
98
117
  can,
99
- canSync: (event) => {
100
- return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
101
- },
102
118
  changes: SubscriptionRef.changes(stateRef),
119
+ transitions: transitionsPubSub !== void 0 ? Stream.fromPubSub(transitionsPubSub) : Stream.empty,
103
120
  waitFor,
104
121
  awaitFinal,
105
122
  sendAndWait,
106
- sendSync: (event) => {
107
- if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, { event }));
108
- },
109
- dispatch,
110
- dispatchPromise: (event) => Effect.runPromise(dispatch(event)),
111
123
  subscribe: (fn) => {
112
124
  listeners.add(fn);
113
125
  return () => {
114
126
  listeners.delete(fn);
115
127
  };
116
128
  },
129
+ sync: {
130
+ send: (event) => {
131
+ if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, {
132
+ _tag: "send",
133
+ event
134
+ }));
135
+ },
136
+ stop: () => Effect.runFork(stop),
137
+ snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
138
+ matches: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
139
+ can: (event) => {
140
+ return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
141
+ }
142
+ },
117
143
  system,
118
144
  children: childrenMap
119
145
  };
@@ -121,7 +147,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
121
147
  /**
122
148
  * Create and start an actor for a machine
123
149
  */
124
- const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
150
+ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
151
+ const initial = options?.initialState ?? machine.initial;
125
152
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
126
153
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
127
154
  let system;
@@ -136,11 +163,16 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
136
163
  const eventQueue = yield* Queue.unbounded();
137
164
  const stoppedRef = yield* Ref.make(false);
138
165
  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
+ });
139
173
  const self = {
140
- send: Effect.fn("effect-machine.actor.self.send")(function* (event) {
141
- if (yield* Ref.get(stoppedRef)) return;
142
- yield* Queue.offer(eventQueue, { event });
143
- }),
174
+ send: selfSend,
175
+ cast: selfSend,
144
176
  spawn: (childId, childMachine) => Effect.gen(function* () {
145
177
  const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
146
178
  childrenMap.set(childId, child);
@@ -151,20 +183,20 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
151
183
  return child;
152
184
  })
153
185
  };
154
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
186
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
155
187
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
156
188
  type: "@machine.spawn",
157
189
  actorId: id,
158
- initialState: machine.initial,
190
+ initialState: initial,
159
191
  timestamp
160
192
  }));
161
- const stateRef = yield* SubscriptionRef.make(machine.initial);
193
+ const stateRef = yield* SubscriptionRef.make(initial);
162
194
  const listeners = /* @__PURE__ */ new Set();
163
195
  const backgroundFibers = [];
164
196
  const initEvent = { _tag: INTERNAL_INIT_EVENT };
165
197
  const ctx = {
166
198
  actorId: id,
167
- state: machine.initial,
199
+ state: initial,
168
200
  event: initEvent,
169
201
  self,
170
202
  system
@@ -173,7 +205,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
173
205
  for (const bg of machine.backgroundEffects) {
174
206
  const fiber = yield* Effect.forkDetach(bg.handler({
175
207
  actorId: id,
176
- state: machine.initial,
208
+ state: initial,
177
209
  event: initEvent,
178
210
  self,
179
211
  effects: effectSlots,
@@ -182,21 +214,23 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
182
214
  backgroundFibers.push(fiber);
183
215
  }
184
216
  const stateScopeRef = { current: yield* Scope.make() };
185
- yield* runSpawnEffectsWithInspection(machine, machine.initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
186
- if (machine.finalStates.has(machine.initial._tag)) {
217
+ yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
218
+ if (machine.finalStates.has(initial._tag)) {
187
219
  yield* Scope.close(stateScopeRef.current, Exit.void);
188
220
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
189
221
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
190
222
  type: "@machine.stop",
191
223
  actorId: id,
192
- finalState: machine.initial,
224
+ finalState: initial,
193
225
  timestamp
194
226
  }));
195
227
  yield* Ref.set(stoppedRef, true);
196
228
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
197
- return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
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());
198
230
  }
199
- const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system));
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));
200
234
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
201
235
  const finalState = yield* SubscriptionRef.get(stateRef);
202
236
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
@@ -207,33 +241,115 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
207
241
  }));
208
242
  yield* Ref.set(stoppedRef, true);
209
243
  yield* Fiber.interrupt(loopFiber);
244
+ yield* settlePendingReplies(pendingReplies, id);
210
245
  yield* Scope.close(stateScopeRef.current, Exit.void);
211
246
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
212
247
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
213
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
248
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
249
+ });
250
+ /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
251
+ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
252
+ const error = new ActorStoppedError({ actorId });
253
+ for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
254
+ pendingReplies.clear();
214
255
  });
215
256
  /**
216
- * Main event loop for the actor
257
+ * Main event loop for the actor.
258
+ * Includes postpone buffer — events matching postpone rules are buffered
259
+ * and drained after state tag changes (gen_statem semantics).
217
260
  */
218
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system) {
219
- while (true) {
220
- const { event, reply } = yield* Queue.take(eventQueue);
261
+ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, 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;
221
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
+ }
222
287
  const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
223
288
  "effect_machine.actor.id": actorId,
224
289
  "effect_machine.state.current": currentState._tag,
225
290
  "effect_machine.event.type": event._tag
226
291
  } })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
227
- if (reply !== void 0) yield* Deferred.succeed(reply, result);
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));
228
316
  if (shouldStop) {
229
317
  yield* Ref.set(stoppedRef, true);
318
+ settlePostponedBuffer(postponed, pendingReplies, actorId);
319
+ yield* settlePendingReplies(pendingReplies, actorId);
230
320
  yield* Scope.close(stateScopeRef.current, Exit.void);
231
321
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
232
322
  return;
233
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
+ }
234
341
  }
235
342
  });
236
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
+ /**
237
353
  * Process a single event, returning true if the actor should stop.
238
354
  * Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
239
355
  */
@@ -375,25 +491,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
375
491
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
376
492
  return yield* registerActor(id, yield* createActor(id, built._inner));
377
493
  });
378
- const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* (id, persistentMachine) {
379
- if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
380
- const adapter = yield* PersistenceAdapterTag;
381
- const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
382
- return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
383
- });
384
- const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
385
- if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
386
- return yield* spawnRegular(id, machine);
387
- });
388
- function spawn(id, machine) {
389
- return withSpawnGate(spawnImpl(id, machine));
390
- }
391
- const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
392
- const maybeActor = yield* restorePersistentActor(id, persistentMachine);
393
- if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
394
- return maybeActor;
395
- });
396
- const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
494
+ const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
397
495
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
398
496
  return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
399
497
  });
@@ -410,55 +508,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
410
508
  yield* actor.stop;
411
509
  return true;
412
510
  });
413
- const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
414
- const adapter = yield* PersistenceAdapterTag;
415
- if (adapter.listActors === void 0) return [];
416
- return yield* adapter.listActors();
417
- });
418
- const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
419
- const restored = [];
420
- const failed = [];
421
- for (const id of ids) {
422
- if (MutableHashMap.has(actorsMap, id)) continue;
423
- const result = yield* Effect.result(restore(id, persistentMachine));
424
- if (result._tag === "Failure") failed.push({
425
- id,
426
- error: result.failure
427
- });
428
- else if (Option.isSome(result.success)) restored.push(result.success.value);
429
- else failed.push({
430
- id,
431
- error: new PersistenceError({
432
- operation: "restore",
433
- actorId: id,
434
- message: "No persisted state found"
435
- })
436
- });
437
- }
438
- return {
439
- restored,
440
- failed
441
- };
442
- });
443
- const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
444
- const adapter = yield* PersistenceAdapterTag;
445
- if (adapter.listActors === void 0) return {
446
- restored: [],
447
- failed: []
448
- };
449
- const machineType = persistentMachine.persistence.machineType;
450
- if (machineType === void 0) return yield* new PersistenceError({
451
- operation: "restoreAll",
452
- actorId: "*",
453
- message: "restoreAll requires explicit machineType in persistence config"
454
- });
455
- let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
456
- if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
457
- return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
458
- });
459
511
  return ActorSystem.of({
460
512
  spawn,
461
- restore,
462
513
  get,
463
514
  stop,
464
515
  events: Stream.fromPubSub(eventPubSub),
@@ -474,10 +525,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
474
525
  return () => {
475
526
  eventListeners.delete(fn);
476
527
  };
477
- },
478
- listPersisted,
479
- restoreMany,
480
- restoreAll
528
+ }
481
529
  });
482
530
  });
483
531
  /**
@@ -485,4 +533,4 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
485
533
  */
486
534
  const Default = Layer.effect(ActorSystem, make());
487
535
  //#endregion
488
- export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
536
+ export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
@@ -54,10 +54,12 @@ const EntityMachine = { layer: (entity, machine, options) => {
54
54
  if (Option.isNone(existingSystem)) return yield* Effect.die("EntityMachine requires ActorSystem in context");
55
55
  const system = existingSystem.value;
56
56
  const internalQueue = yield* Queue.unbounded();
57
+ const clusterSend = Effect.fn("effect-machine.cluster.self.send")(function* (event) {
58
+ yield* Queue.offer(internalQueue, event);
59
+ });
57
60
  const self = {
58
- send: Effect.fn("effect-machine.cluster.self.send")(function* (event) {
59
- yield* Queue.offer(internalQueue, event);
60
- }),
61
+ send: clusterSend,
62
+ cast: clusterSend,
61
63
  spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
62
64
  };
63
65
  const stateRef = yield* Ref.make(initialState);
package/dist/errors.d.ts CHANGED
@@ -42,5 +42,16 @@ declare const AssertionError_base: Schema.ErrorClass<AssertionError, Schema.Tagg
42
42
  }>, effect_Cause0.YieldableError>;
43
43
  /** Assertion failed in testing utilities */
44
44
  declare class AssertionError extends AssertionError_base {}
45
+ declare const ActorStoppedError_base: Schema.ErrorClass<ActorStoppedError, Schema.TaggedStruct<"ActorStoppedError", {
46
+ readonly actorId: Schema.String;
47
+ }>, effect_Cause0.YieldableError>;
48
+ /** Actor was stopped while a call/ask was pending */
49
+ declare class ActorStoppedError extends ActorStoppedError_base {}
50
+ declare const NoReplyError_base: Schema.ErrorClass<NoReplyError, Schema.TaggedStruct<"NoReplyError", {
51
+ readonly actorId: Schema.String;
52
+ readonly eventTag: Schema.String;
53
+ }>, effect_Cause0.YieldableError>;
54
+ /** ask() was used but the transition handler did not call reply */
55
+ declare class NoReplyError extends NoReplyError_base {}
45
56
  //#endregion
46
- export { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
57
+ export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
package/dist/errors.js CHANGED
@@ -32,5 +32,12 @@ var ProvisionValidationError = class extends Schema.TaggedErrorClass()("Provisio
32
32
  }) {};
33
33
  /** Assertion failed in testing utilities */
34
34
  var AssertionError = class extends Schema.TaggedErrorClass()("AssertionError", { message: Schema.String }) {};
35
+ /** Actor was stopped while a call/ask was pending */
36
+ var ActorStoppedError = class extends Schema.TaggedErrorClass()("ActorStoppedError", { actorId: Schema.String }) {};
37
+ /** ask() was used but the transition handler did not call reply */
38
+ var NoReplyError = class extends Schema.TaggedErrorClass()("NoReplyError", {
39
+ actorId: Schema.String,
40
+ eventTag: Schema.String
41
+ }) {};
35
42
  //#endregion
36
- export { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
43
+ export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,9 @@
1
1
  import { EffectHandlers, EffectSlot, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlot, GuardSlots, GuardsDef, GuardsSchema, MachineContext, Slot } from "./slot.js";
2
2
  import { Event, MachineEventSchema, MachineStateSchema, State } from "./schema.js";
3
- import { PersistenceConfig, PersistentMachine, isPersistentMachine } from "./persistence/persistent-machine.js";
4
- import { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
3
+ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
5
4
  import { ProcessEventResult } from "./internal/transition.js";
6
- import { PersistentActorRef, createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
7
- import { ActorMetadata, PersistedEvent, PersistenceAdapter, PersistenceAdapterTag, PersistenceError, RestoreFailure, RestoreResult, Snapshot, VersionConflictError } from "./persistence/adapter.js";
8
- import { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter } from "./persistence/adapters/in-memory.js";
9
- import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, ProvideHandlers, SpawnEffect, StateHandlerContext, TaskOptions, Transition, machine_d_exports } from "./machine.js";
10
- import { ActorRef, ActorSystem, Default, SystemEvent, SystemEventListener } from "./actor.js";
5
+ import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SpawnEffect, StateHandlerContext, TaskOptions, Transition, machine_d_exports } from "./machine.js";
6
+ import { ActorRef, ActorRefSync, ActorSystem, Default, SystemEvent, SystemEventListener, TransitionInfo } from "./actor.js";
11
7
  import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
12
8
  import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
13
- export { type ActorMetadata, type ActorRef, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, 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, InMemoryPersistenceAdapter, 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, type PersistOptions, type PersistedEvent, type PersistenceAdapter, PersistenceAdapterTag, type PersistenceConfig, PersistenceError, type PersistentActorRef, type PersistentMachine, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type RestoreFailure, type RestoreResult, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type Snapshot, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, makeInspectorEffect, restorePersistentActor, simulate, tracingInspector };
9
+ export { type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, 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, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, UnprovidedSlotsError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
package/dist/index.js CHANGED
@@ -1,13 +1,8 @@
1
1
  import { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
2
- import { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
3
- import { isPersistentMachine } from "./persistence/persistent-machine.js";
2
+ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
4
3
  import { Slot } from "./slot.js";
5
4
  import { machine_exports } from "./machine.js";
6
- import { PersistenceAdapterTag, PersistenceError, VersionConflictError } from "./persistence/adapter.js";
7
- import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
8
5
  import { ActorSystem, Default } from "./actor.js";
9
6
  import { Event, State } from "./schema.js";
10
7
  import { assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
11
- import { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter } from "./persistence/adapters/in-memory.js";
12
- import "./persistence/index.js";
13
- export { Default as ActorSystemDefault, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, InMemoryPersistenceAdapter, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, MissingSchemaError, PersistenceAdapterTag, PersistenceError, ProvisionValidationError, Slot, SlotProvisionError, State, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, makeInspectorEffect, restorePersistentActor, simulate, tracingInspector };
8
+ export { ActorStoppedError, Default as ActorSystemDefault, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, Slot, SlotProvisionError, State, UnprovidedSlotsError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
@@ -21,7 +21,7 @@ interface TransitionExecutionResult<S> {
21
21
  *
22
22
  * Used by:
23
23
  * - executeTransition (actor event loop, testing)
24
- * - persistent-actor replay (restore, replayTo)
24
+ * - Machine.replay (event sourcing restore)
25
25
  *
26
26
  * @internal
27
27
  */
@@ -29,7 +29,11 @@ declare const runTransitionHandler: <S extends {
29
29
  readonly _tag: string;
30
30
  }, E extends {
31
31
  readonly _tag: string;
32
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
32
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
33
+ newState: S;
34
+ hasReply: boolean;
35
+ reply: unknown;
36
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
33
37
  /**
34
38
  * Execute a transition for a given state and event.
35
39
  * Handles transition resolution, handler invocation, and guard/effect slot creation.
@@ -49,6 +53,8 @@ declare const executeTransition: <S extends {
49
53
  newState: S;
50
54
  transitioned: boolean;
51
55
  reenter: boolean;
56
+ hasReply: boolean;
57
+ reply: unknown;
52
58
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
53
59
  /**
54
60
  * Optional hooks for event processing inspection/tracing.
@@ -84,7 +90,22 @@ interface ProcessEventResult<S> {
84
90
  readonly lifecycleRan: boolean;
85
91
  /** Whether new state is final */
86
92
  readonly isFinal: boolean;
93
+ /** Whether the handler provided a reply (structural, not value-based) */
94
+ readonly hasReply: boolean;
95
+ /** Domain reply value from handler (used by ask). Only meaningful when hasReply is true. */
96
+ readonly reply?: unknown;
97
+ /** Whether the event was postponed (buffered for retry after next state change) */
98
+ readonly postponed: boolean;
87
99
  }
100
+ /**
101
+ * Check if an event should be postponed in the current state.
102
+ * @internal
103
+ */
104
+ declare const shouldPostpone: <S extends {
105
+ readonly _tag: string;
106
+ }, E extends {
107
+ readonly _tag: string;
108
+ }, R>(machine: Machine<S, E, R, any, any, any, any>, stateTag: string, eventTag: string) => boolean;
88
109
  /**
89
110
  * Process a single event through the machine.
90
111
  *
@@ -109,6 +130,9 @@ declare const processEventCore: <S extends {
109
130
  transitioned: boolean;
110
131
  lifecycleRan: boolean;
111
132
  isFinal: boolean;
133
+ hasReply: boolean;
134
+ reply: unknown;
135
+ postponed: boolean;
112
136
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
113
137
  /**
114
138
  * Run spawn effects for a state (forked into state scope, auto-cancelled on state exit).
@@ -157,4 +181,4 @@ declare const findSpawnEffects: <S extends {
157
181
  readonly _tag: string;
158
182
  }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, GD, EFD>, stateTag: string) => ReadonlyArray<SpawnEffect<S, E, EFD, R>>;
159
183
  //#endregion
160
- export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler };
184
+ export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };