effect-machine 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +76 -16
  2. package/dist/_virtual/_rolldown/runtime.js +6 -11
  3. package/dist/actor.d.ts +58 -72
  4. package/dist/actor.js +166 -32
  5. package/dist/cluster/entity-machine.d.ts +0 -1
  6. package/dist/cluster/entity-machine.js +6 -6
  7. package/dist/cluster/index.js +1 -2
  8. package/dist/cluster/to-entity.js +1 -3
  9. package/dist/errors.d.ts +12 -1
  10. package/dist/errors.js +8 -3
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +2 -3
  13. package/dist/inspection.js +1 -3
  14. package/dist/internal/inspection.js +1 -3
  15. package/dist/internal/transition.d.ts +26 -2
  16. package/dist/internal/transition.js +37 -10
  17. package/dist/internal/utils.d.ts +7 -2
  18. package/dist/internal/utils.js +1 -3
  19. package/dist/machine.d.ts +66 -4
  20. package/dist/machine.js +67 -31
  21. package/dist/persistence/adapter.js +1 -3
  22. package/dist/persistence/adapters/in-memory.js +1 -3
  23. package/dist/persistence/index.js +1 -2
  24. package/dist/persistence/persistent-actor.js +54 -19
  25. package/dist/persistence/persistent-machine.js +1 -3
  26. package/dist/schema.js +1 -3
  27. package/dist/slot.js +1 -3
  28. package/dist/testing.js +58 -6
  29. package/package.json +19 -18
  30. package/v3/dist/_virtual/_rolldown/runtime.js +13 -0
  31. package/{dist-v3 → v3/dist}/actor.d.ts +65 -78
  32. package/{dist-v3 → v3/dist}/actor.js +173 -37
  33. package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -2
  34. package/{dist-v3 → v3/dist}/cluster/entity-machine.js +9 -9
  35. package/{dist-v3 → v3/dist}/cluster/index.js +1 -2
  36. package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
  37. package/{dist-v3 → v3/dist}/cluster/to-entity.js +2 -4
  38. package/v3/dist/errors.d.ts +76 -0
  39. package/{dist-v3 → v3/dist}/errors.js +9 -4
  40. package/v3/dist/index.d.ts +13 -0
  41. package/v3/dist/index.js +13 -0
  42. package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
  43. package/v3/dist/inspection.js +156 -0
  44. package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
  45. package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
  46. package/v3/dist/internal/inspection.js +20 -0
  47. package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
  48. package/{dist-v3 → v3/dist}/internal/transition.js +47 -17
  49. package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
  50. package/{dist-v3 → v3/dist}/internal/utils.js +2 -4
  51. package/{dist-v3 → v3/dist}/machine.d.ts +86 -10
  52. package/{dist-v3 → v3/dist}/machine.js +130 -33
  53. package/{dist-v3 → v3/dist}/persistence/adapter.d.ts +18 -5
  54. package/{dist-v3 → v3/dist}/persistence/adapter.js +2 -4
  55. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.d.ts +1 -1
  56. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.js +2 -4
  57. package/{dist-v3 → v3/dist}/persistence/index.js +1 -2
  58. package/{dist-v3 → v3/dist}/persistence/persistent-actor.d.ts +7 -6
  59. package/{dist-v3 → v3/dist}/persistence/persistent-actor.js +59 -22
  60. package/{dist-v3 → v3/dist}/persistence/persistent-machine.d.ts +1 -1
  61. package/{dist-v3 → v3/dist}/persistence/persistent-machine.js +2 -4
  62. package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
  63. package/{dist-v3 → v3/dist}/schema.js +6 -5
  64. package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
  65. package/{dist-v3 → v3/dist}/slot.js +2 -4
  66. package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
  67. package/{dist-v3 → v3/dist}/testing.js +61 -9
  68. package/dist-v3/_virtual/_rolldown/runtime.js +0 -18
  69. package/dist-v3/errors.d.ts +0 -27
  70. package/dist-v3/index.d.ts +0 -13
  71. package/dist-v3/index.js +0 -14
  72. package/dist-v3/inspection.js +0 -50
  73. package/dist-v3/internal/inspection.js +0 -15
  74. /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
  75. /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
  76. /package/{dist-v3 → v3/dist}/persistence/index.d.ts +0 -0
@@ -1,6 +1,5 @@
1
1
  import { PersistenceAdapterTag, PersistenceError, VersionConflictError } from "../adapter.js";
2
2
  import { Effect, Layer, Option, Ref, Schema } from "effect";
3
-
4
3
  //#region src/persistence/adapters/in-memory.ts
5
4
  /**
6
5
  * Create an in-memory persistence adapter.
@@ -171,6 +170,5 @@ const makeInMemoryPersistenceAdapter = make;
171
170
  * ```
172
171
  */
173
172
  const InMemoryPersistenceAdapter = Layer.effect(PersistenceAdapterTag, make);
174
-
175
173
  //#endregion
176
- export { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter };
174
+ export { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter };
@@ -2,5 +2,4 @@ import { isPersistentMachine, persist } from "./persistent-machine.js";
2
2
  import { PersistenceAdapterTag, PersistenceError, VersionConflictError } from "./adapter.js";
3
3
  import { createPersistentActor, restorePersistentActor } from "./persistent-actor.js";
4
4
  import { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter } from "./adapters/in-memory.js";
5
-
6
- export { InMemoryPersistenceAdapter, PersistenceAdapterTag, PersistenceError, VersionConflictError, createPersistentActor, isPersistentMachine, makeInMemoryPersistenceAdapter, persist, restorePersistentActor };
5
+ export { InMemoryPersistenceAdapter, PersistenceAdapterTag, PersistenceError, VersionConflictError, createPersistentActor, isPersistentMachine, makeInMemoryPersistenceAdapter, persist, restorePersistentActor };
@@ -1,11 +1,11 @@
1
1
  import { Inspector } from "../inspection.js";
2
2
  import { INTERNAL_INIT_EVENT, stubSystem } from "../internal/utils.js";
3
+ import { NoReplyError } from "../errors.js";
3
4
  import { emitWithTimestamp } from "../internal/inspection.js";
4
- import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler } from "../internal/transition.js";
5
+ import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone } from "../internal/transition.js";
5
6
  import { PersistenceAdapterTag } from "./adapter.js";
6
- import { ActorSystem, buildActorRefCore, notifyListeners } from "../actor.js";
7
- import { Cause, Clock, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
8
-
7
+ import { ActorSystem, buildActorRefCore, notifyListeners, settlePendingReplies } from "../actor.js";
8
+ import { Cause, Clock, Deferred, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
9
9
  //#region src/persistence/persistent-actor.ts
10
10
  /** Get current time in milliseconds using Effect Clock */
11
11
  const now = Clock.currentTimeMillis;
@@ -20,7 +20,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
20
20
  for (const persistedEvent of events) {
21
21
  if (stopVersion !== void 0 && persistedEvent.version > stopVersion) break;
22
22
  const transition = resolveTransition(machine, state, persistedEvent.event);
23
- if (transition !== void 0) state = yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem, "restore");
23
+ if (transition !== void 0) state = (yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem, "restore")).newState;
24
24
  version = persistedEvent.version;
25
25
  }
26
26
  return {
@@ -31,7 +31,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
31
31
  /**
32
32
  * Build PersistentActorRef with all methods
33
33
  */
34
- const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap) => {
34
+ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap, pendingReplies) => {
35
35
  const { machine, persistence } = persistentMachine;
36
36
  const typedMachine = machine;
37
37
  const persist = Effect.gen(function* () {
@@ -45,8 +45,10 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
45
45
  const version = Ref.get(versionRef).pipe(Effect.withSpan("effect-machine.persistentActor.version"));
46
46
  const replayTo = Effect.fn("effect-machine.persistentActor.replayTo")(function* (targetVersion) {
47
47
  if (targetVersion <= (yield* Ref.get(versionRef))) {
48
+ const dummySend = Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void);
48
49
  const dummySelf = {
49
- send: Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void),
50
+ send: dummySend,
51
+ cast: dummySend,
50
52
  spawn: () => Effect.die("spawn not supported in replay")
51
53
  };
52
54
  const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
@@ -71,7 +73,7 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
71
73
  }
72
74
  });
73
75
  return {
74
- ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap),
76
+ ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies),
75
77
  persist,
76
78
  version,
77
79
  replayTo
@@ -93,11 +95,16 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
93
95
  const eventQueue = yield* Queue.unbounded();
94
96
  const stoppedRef = yield* Ref.make(false);
95
97
  const childrenMap = /* @__PURE__ */ new Map();
98
+ const selfSend = Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
99
+ if (yield* Ref.get(stoppedRef)) return;
100
+ yield* Queue.offer(eventQueue, {
101
+ _tag: "send",
102
+ event
103
+ });
104
+ });
96
105
  const self = {
97
- send: Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
98
- if (yield* Ref.get(stoppedRef)) return;
99
- yield* Queue.offer(eventQueue, event);
100
- }),
106
+ send: selfSend,
107
+ cast: selfSend,
101
108
  spawn: (childId, childMachine) => Effect.gen(function* () {
102
109
  const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
103
110
  childrenMap.set(childId, child);
@@ -178,9 +185,10 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
178
185
  finalState: resolvedInitial,
179
186
  timestamp
180
187
  }));
181
- return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
188
+ return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, /* @__PURE__ */ new Set());
182
189
  }
183
- const loopFiber = yield* Effect.forkDetach(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system));
190
+ const pendingReplies = /* @__PURE__ */ new Set();
191
+ const loopFiber = yield* Effect.forkDetach(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies));
184
192
  return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
185
193
  const finalState = yield* SubscriptionRef.get(stateRef);
186
194
  yield* emitWithTimestamp(inspector, (timestamp) => ({
@@ -191,16 +199,17 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
191
199
  }));
192
200
  yield* Ref.set(stoppedRef, true);
193
201
  yield* Fiber.interrupt(loopFiber);
202
+ yield* settlePendingReplies(pendingReplies, id);
194
203
  yield* Scope.close(stateScopeRef.current, Exit.void);
195
204
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
196
205
  yield* Fiber.interrupt(snapshotFiber);
197
206
  yield* Fiber.interrupt(persistenceFiber);
198
- }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
207
+ }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, pendingReplies);
199
208
  });
200
209
  /**
201
210
  * Main event loop for persistent actor
202
211
  */
203
- const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system) {
212
+ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies) {
204
213
  const { machine, persistence } = persistentMachine;
205
214
  const typedMachine = machine;
206
215
  const hooks = inspector === void 0 ? void 0 : {
@@ -229,10 +238,28 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
229
238
  timestamp
230
239
  }))
231
240
  };
241
+ const postponed = [];
242
+ const pendingDrain = [];
243
+ const hasPostponeRules = machine.postponeRules.length > 0;
232
244
  while (true) {
233
- const event = yield* Queue.take(eventQueue);
245
+ const queued = pendingDrain.length > 0 ? pendingDrain.shift() : yield* Queue.take(eventQueue);
246
+ const event = queued.event;
234
247
  const currentState = yield* SubscriptionRef.get(stateRef);
235
248
  const currentVersion = yield* Ref.get(versionRef);
249
+ if (hasPostponeRules && shouldPostpone(typedMachine, currentState._tag, event._tag)) {
250
+ postponed.push(queued);
251
+ if (queued._tag === "call") yield* Deferred.succeed(queued.reply, {
252
+ newState: currentState,
253
+ previousState: currentState,
254
+ transitioned: false,
255
+ lifecycleRan: false,
256
+ isFinal: false,
257
+ hasReply: false,
258
+ reply: void 0,
259
+ postponed: true
260
+ });
261
+ continue;
262
+ }
236
263
  yield* emitWithTimestamp(inspector, (timestamp) => ({
237
264
  type: "@machine.event",
238
265
  actorId: id,
@@ -241,6 +268,12 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
241
268
  timestamp
242
269
  }));
243
270
  const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, id, hooks);
271
+ if (queued._tag === "call") yield* Deferred.succeed(queued.reply, result);
272
+ else if (queued._tag === "ask") if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
273
+ else yield* Deferred.fail(queued.reply, new NoReplyError({
274
+ actorId: id,
275
+ eventTag: event._tag
276
+ }));
244
277
  if (!result.transitioned) continue;
245
278
  const newVersion = currentVersion + 1;
246
279
  yield* Ref.set(versionRef, newVersion);
@@ -268,12 +301,15 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
268
301
  timestamp
269
302
  }));
270
303
  yield* Ref.set(stoppedRef, true);
304
+ postponed.length = 0;
305
+ yield* settlePendingReplies(pendingReplies, id);
271
306
  yield* Scope.close(stateScopeRef.current, Exit.void);
272
307
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
273
308
  yield* Fiber.interrupt(snapshotFiber);
274
309
  yield* Fiber.interrupt(persistenceFiber);
275
310
  return;
276
311
  }
312
+ if (result.lifecycleRan && postponed.length > 0) pendingDrain.push(...postponed.splice(0));
277
313
  }
278
314
  });
279
315
  /**
@@ -364,6 +400,5 @@ const restorePersistentActor = Effect.fn("effect-machine.persistentActor.restore
364
400
  const actor = yield* createPersistentActor(id, persistentMachine, maybeSnapshot, events);
365
401
  return Option.some(actor);
366
402
  });
367
-
368
403
  //#endregion
369
- export { createPersistentActor, restorePersistentActor };
404
+ export { createPersistentActor, restorePersistentActor };
@@ -1,5 +1,4 @@
1
1
  import { MissingSchemaError } from "../errors.js";
2
-
3
2
  //#region src/persistence/persistent-machine.ts
4
3
  /**
5
4
  * Type guard to check if a value is a PersistentMachine
@@ -19,6 +18,5 @@ const persist = (config) => (machine) => {
19
18
  }
20
19
  };
21
20
  };
22
-
23
21
  //#endregion
24
- export { isPersistentMachine, persist };
22
+ export { isPersistentMachine, persist };
package/dist/schema.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { InvalidSchemaError, MissingMatchHandlerError } from "./errors.js";
2
2
  import { Schema } from "effect";
3
-
4
3
  //#region src/schema.ts
5
4
  /**
6
5
  * Schema-first State/Event definitions for effect-machine.
@@ -164,6 +163,5 @@ const State = (definition) => createMachineSchema(definition);
164
163
  * ```
165
164
  */
166
165
  const Event = (definition) => createMachineSchema(definition);
167
-
168
166
  //#endregion
169
- export { Event, State };
167
+ export { Event, State };
package/dist/slot.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { ServiceMap } from "effect";
2
-
3
2
  //#region src/slot.ts
4
3
  /**
5
4
  * Slot module - schema-based, parameterized guards and effects.
@@ -94,6 +93,5 @@ const Slot = {
94
93
  Guards,
95
94
  Effects
96
95
  };
97
-
98
96
  //#endregion
99
- export { Effects, Guards, MachineContextTag, Slot };
97
+ export { Effects, Guards, MachineContextTag, Slot };
package/dist/testing.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { stubSystem } from "./internal/utils.js";
2
2
  import { AssertionError } from "./errors.js";
3
3
  import { BuiltMachine } from "./machine.js";
4
- import { executeTransition } from "./internal/transition.js";
4
+ import { executeTransition, shouldPostpone } from "./internal/transition.js";
5
5
  import { Effect, SubscriptionRef } from "effect";
6
-
7
6
  //#region src/testing.ts
8
7
  /**
9
8
  * Simulate a sequence of events through a machine without running an actor.
@@ -27,18 +26,44 @@ import { Effect, SubscriptionRef } from "effect";
27
26
  */
28
27
  const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
29
28
  const machine = input instanceof BuiltMachine ? input._inner : input;
29
+ const dummySend = Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void);
30
30
  const dummySelf = {
31
- send: Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void),
31
+ send: dummySend,
32
+ cast: dummySend,
32
33
  spawn: () => Effect.die("spawn not supported in simulation")
33
34
  };
34
35
  let currentState = machine.initial;
35
36
  const states = [currentState];
37
+ const hasPostponeRules = machine.postponeRules.length > 0;
38
+ const postponed = [];
36
39
  for (const event of events) {
40
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
41
+ postponed.push(event);
42
+ continue;
43
+ }
37
44
  const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "simulation");
38
45
  if (!result.transitioned) continue;
46
+ const prevTag = currentState._tag;
39
47
  currentState = result.newState;
40
48
  states.push(currentState);
41
49
  if (machine.finalStates.has(currentState._tag)) break;
50
+ let drainTag = prevTag;
51
+ while (currentState._tag !== drainTag && postponed.length > 0) {
52
+ drainTag = currentState._tag;
53
+ const drained = postponed.splice(0);
54
+ for (const postponedEvent of drained) {
55
+ if (shouldPostpone(machine, currentState._tag, postponedEvent._tag)) {
56
+ postponed.push(postponedEvent);
57
+ continue;
58
+ }
59
+ const drainResult = yield* executeTransition(machine, currentState, postponedEvent, dummySelf, stubSystem, "simulation");
60
+ if (drainResult.transitioned) {
61
+ currentState = drainResult.newState;
62
+ states.push(currentState);
63
+ if (machine.finalStates.has(currentState._tag)) break;
64
+ }
65
+ }
66
+ }
42
67
  }
43
68
  return {
44
69
  states,
@@ -114,25 +139,52 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
114
139
  */
115
140
  const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
116
141
  const machine = input instanceof BuiltMachine ? input._inner : input;
142
+ const dummySend = Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void);
117
143
  const dummySelf = {
118
- send: Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void),
144
+ send: dummySend,
145
+ cast: dummySend,
119
146
  spawn: () => Effect.die("spawn not supported in test harness")
120
147
  };
121
148
  const stateRef = yield* SubscriptionRef.make(machine.initial);
149
+ const hasPostponeRules = machine.postponeRules.length > 0;
150
+ const postponed = [];
122
151
  return {
123
152
  state: stateRef,
124
153
  send: Effect.fn("effect-machine.testHarness.send")(function* (event) {
125
154
  const currentState = yield* SubscriptionRef.get(stateRef);
155
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
156
+ postponed.push(event);
157
+ return currentState;
158
+ }
126
159
  const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "test-harness");
127
160
  if (!result.transitioned) return currentState;
161
+ const prevTag = currentState._tag;
128
162
  const newState = result.newState;
129
163
  yield* SubscriptionRef.set(stateRef, newState);
130
164
  if (options?.onTransition !== void 0) options.onTransition(currentState, event, newState);
165
+ let drainTag = prevTag;
166
+ let currentTag = newState._tag;
167
+ while (currentTag !== drainTag && postponed.length > 0) {
168
+ drainTag = currentTag;
169
+ const drained = postponed.splice(0);
170
+ for (const postponedEvent of drained) {
171
+ const state = yield* SubscriptionRef.get(stateRef);
172
+ if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
173
+ postponed.push(postponedEvent);
174
+ continue;
175
+ }
176
+ const drainResult = yield* executeTransition(machine, state, postponedEvent, dummySelf, stubSystem, "test-harness");
177
+ if (drainResult.transitioned) {
178
+ yield* SubscriptionRef.set(stateRef, drainResult.newState);
179
+ currentTag = drainResult.newState._tag;
180
+ if (options?.onTransition !== void 0) options.onTransition(state, postponedEvent, drainResult.newState);
181
+ }
182
+ }
183
+ }
131
184
  return newState;
132
185
  }),
133
186
  getState: SubscriptionRef.get(stateRef)
134
187
  };
135
188
  });
136
-
137
189
  //#endregion
138
- export { AssertionError, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate };
190
+ export { AssertionError, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"
7
7
  },
8
8
  "files": [
9
9
  "dist",
10
- "dist-v3"
10
+ "v3/dist"
11
11
  ],
12
12
  "type": "module",
13
13
  "exports": {
@@ -25,14 +25,14 @@
25
25
  },
26
26
  "./v3": {
27
27
  "import": {
28
- "types": "./dist-v3/index.d.ts",
29
- "default": "./dist-v3/index.js"
28
+ "types": "./v3/dist/index.d.ts",
29
+ "default": "./v3/dist/index.js"
30
30
  }
31
31
  },
32
32
  "./v3/cluster": {
33
33
  "import": {
34
- "types": "./dist-v3/cluster/index.d.ts",
35
- "default": "./dist-v3/cluster/index.js"
34
+ "types": "./v3/dist/cluster/index.d.ts",
35
+ "default": "./v3/dist/cluster/index.js"
36
36
  }
37
37
  }
38
38
  },
@@ -41,34 +41,35 @@
41
41
  },
42
42
  "scripts": {
43
43
  "typecheck": "tsc --noEmit",
44
- "typecheck:v3": "tsc --noEmit -p tsconfig.v3.json",
44
+ "typecheck:v3": "tsc --noEmit -p v3/tsconfig.json",
45
45
  "lint": "oxlint",
46
46
  "lint:fix": "oxlint --fix",
47
47
  "fmt": "oxfmt",
48
48
  "fmt:check": "oxfmt --check",
49
49
  "test": "bun test",
50
+ "test:v3": "bun test --tsconfig-override=v3/tsconfig.json v3/test/",
50
51
  "test:watch": "bun test --watch",
51
52
  "gate": "concurrently -n type,lint,fmt,test,build -c blue,yellow,magenta,green,cyan \"bun run typecheck\" \"bun run lint:fix\" \"bun run fmt\" \"bun run test\" \"bun run build\"",
52
53
  "prepare": "lefthook install || true && effect-language-service patch",
53
54
  "version": "changeset version",
54
- "build": "tsdown && tsdown --config tsdown.v3.config.ts",
55
+ "build": "tsdown && tsdown --config v3/tsdown.config.ts",
55
56
  "release": "bun run build && changeset publish"
56
57
  },
57
58
  "dependencies": {
58
59
  "effect": "4.0.0-beta.35"
59
60
  },
60
61
  "devDependencies": {
61
- "@changesets/changelog-github": "^0.5.2",
62
- "@changesets/cli": "^2.29.8",
63
- "@effect/language-service": "^0.75.1",
64
- "@types/bun": "1.3.9",
62
+ "@changesets/changelog-github": "^0.6.0",
63
+ "@changesets/cli": "^2.30.0",
64
+ "@effect/language-service": "^0.82.0",
65
+ "@types/bun": "1.3.11",
65
66
  "concurrently": "^9.2.1",
66
- "effect-bun-test": "0.2.0",
67
- "effect-v3": "npm:effect@^3.19.18",
68
- "lefthook": "^2.1.1",
69
- "oxfmt": "^0.33.0",
70
- "oxlint": "^1.48.0",
71
- "tsdown": "^0.20.3",
67
+ "effect-bun-test": "0.2.1",
68
+ "effect-v3": "npm:effect@^3.21.0",
69
+ "lefthook": "^2.1.4",
70
+ "oxfmt": "^0.41.0",
71
+ "oxlint": "^1.56.0",
72
+ "tsdown": "^0.21.4",
72
73
  "typescript": "^5.9.3"
73
74
  },
74
75
  "peerDependencies": {
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll };
@@ -1,87 +1,85 @@
1
+ import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
1
2
  import { PersistentMachine } from "./persistence/persistent-machine.js";
2
- import { DuplicateActorError } from "./errors.js";
3
- import { EffectsDef, GuardsDef } from "./slot.js";
3
+ import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
4
  import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
5
5
  import { PersistentActorRef } from "./persistence/persistent-actor.js";
6
6
  import { ActorMetadata, PersistenceAdapterTag, PersistenceError, RestoreResult, VersionConflictError } from "./persistence/adapter.js";
7
- import { BuiltMachine, Machine } from "./machine.js";
8
- import { Effect, Option, Queue, Ref, Stream, SubscriptionRef } from "effect";
7
+ import { BuiltMachine, Machine, MachineRef } from "./machine.js";
8
+ import { Context, Deferred, Effect, Layer, Option, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
9
+ import * as effect_dist_dts_Tracer_js0 from "effect/dist/dts/Tracer.js";
9
10
 
10
- //#region src-v3/actor.d.ts
11
+ //#region src/actor.d.ts
12
+ /** Discriminated mailbox request */
13
+ type QueuedEvent<E> = {
14
+ readonly _tag: "send";
15
+ readonly event: E;
16
+ } | {
17
+ readonly _tag: "call";
18
+ readonly event: E;
19
+ readonly reply: Deferred.Deferred<ProcessEventResult<{
20
+ readonly _tag: string;
21
+ }>, ActorStoppedError>;
22
+ } | {
23
+ readonly _tag: "ask";
24
+ readonly event: E;
25
+ readonly reply: Deferred.Deferred<unknown, NoReplyError | ActorStoppedError>;
26
+ };
11
27
  /**
12
28
  * Reference to a running actor.
13
29
  */
30
+ /**
31
+ * Sync projection of ActorRef for non-Effect boundaries (React hooks, framework callbacks).
32
+ */
33
+ interface ActorRefSync<State extends {
34
+ readonly _tag: string;
35
+ }, Event> {
36
+ readonly send: (event: Event) => void;
37
+ readonly stop: () => void;
38
+ readonly snapshot: () => State;
39
+ readonly matches: (tag: State["_tag"]) => boolean;
40
+ readonly can: (event: Event) => boolean;
41
+ }
14
42
  interface ActorRef<State extends {
15
43
  readonly _tag: string;
16
44
  }, Event> {
17
- /**
18
- * Unique identifier for this actor
19
- */
20
45
  readonly id: string;
21
- /**
22
- * Send an event to the actor
23
- */
46
+ /** Send an event (fire-and-forget). */
24
47
  readonly send: (event: Event) => Effect.Effect<void>;
48
+ /** Fire-and-forget alias for send (OTP gen_server:cast). */
49
+ readonly cast: (event: Event) => Effect.Effect<void>;
25
50
  /**
26
- * Observable state of the actor
51
+ * Serialized request-reply (OTP gen_server:call).
52
+ * Event is processed through the queue; caller gets ProcessEventResult back.
27
53
  */
28
- readonly state: SubscriptionRef.SubscriptionRef<State>;
54
+ readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
29
55
  /**
30
- * Stop the actor gracefully
56
+ * Typed request-reply. Event is processed through the queue; caller gets
57
+ * the domain value returned by the handler's `reply` field.
58
+ * Fails with NoReplyError if the handler doesn't provide a reply.
31
59
  */
60
+ readonly ask: <R>(event: Event) => Effect.Effect<R, NoReplyError | ActorStoppedError>;
61
+ /** Observable state. */
62
+ readonly state: SubscriptionRef.SubscriptionRef<State>;
63
+ /** Stop the actor gracefully. */
32
64
  readonly stop: Effect.Effect<void>;
33
- /**
34
- * Stop the actor (fire-and-forget).
35
- * Signals graceful shutdown without waiting for completion.
36
- * Use when stopping from sync contexts (e.g. framework cleanup hooks).
37
- */
38
- readonly stopSync: () => void;
39
- /**
40
- * Get current state snapshot (Effect)
41
- */
65
+ /** Get current state snapshot. */
42
66
  readonly snapshot: Effect.Effect<State>;
43
- /**
44
- * Get current state snapshot (sync)
45
- */
46
- readonly snapshotSync: () => State;
47
- /**
48
- * Check if current state matches tag (Effect)
49
- */
67
+ /** Check if current state matches tag. */
50
68
  readonly matches: (tag: State["_tag"]) => Effect.Effect<boolean>;
51
- /**
52
- * Check if current state matches tag (sync)
53
- */
54
- readonly matchesSync: (tag: State["_tag"]) => boolean;
55
- /**
56
- * Check if event can be handled in current state (Effect)
57
- */
69
+ /** Check if event can be handled in current state. */
58
70
  readonly can: (event: Event) => Effect.Effect<boolean>;
59
- /**
60
- * Check if event can be handled in current state (sync)
61
- */
62
- readonly canSync: (event: Event) => boolean;
63
- /**
64
- * Stream of state changes
65
- */
71
+ /** Stream of state changes. */
66
72
  readonly changes: Stream.Stream<State>;
67
- /**
68
- * Wait for a state that matches predicate or state variant (includes current snapshot).
69
- * Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
70
- */
73
+ /** Wait for a state matching predicate or variant (includes current snapshot). */
71
74
  readonly waitFor: {
72
75
  (predicate: (state: State) => boolean): Effect.Effect<State>;
73
76
  (state: {
74
77
  readonly _tag: State["_tag"];
75
78
  }): Effect.Effect<State>;
76
79
  };
77
- /**
78
- * Wait for a final state (includes current snapshot)
79
- */
80
+ /** Wait for a final state (includes current snapshot). */
80
81
  readonly awaitFinal: Effect.Effect<State>;
81
- /**
82
- * Send event and wait for predicate, state variant, or final state.
83
- * Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
84
- */
82
+ /** Send event and wait for predicate, state variant, or final state. */
85
83
  readonly sendAndWait: {
86
84
  (event: Event, predicate: (state: State) => boolean): Effect.Effect<State>;
87
85
  (event: Event, state: {
@@ -89,26 +87,13 @@ interface ActorRef<State extends {
89
87
  }): Effect.Effect<State>;
90
88
  (event: Event): Effect.Effect<State>;
91
89
  };
92
- /**
93
- * Send event synchronously (fire-and-forget).
94
- * No-op on stopped actors. Use when you need to send from sync contexts
95
- * (e.g. framework hooks, event handlers).
96
- */
97
- readonly sendSync: (event: Event) => void;
98
- /**
99
- * Subscribe to state changes (sync callback)
100
- * Returns unsubscribe function
101
- */
90
+ /** Subscribe to state changes (sync callback). Returns unsubscribe function. */
102
91
  readonly subscribe: (fn: (state: State) => void) => () => void;
103
- /**
104
- * The actor system this actor belongs to.
105
- * Every actor always has a system either inherited from context or implicitly created.
106
- */
92
+ /** Sync helpers for non-Effect boundaries. */
93
+ readonly sync: ActorRefSync<State, Event>;
94
+ /** The actor system this actor belongs to. */
107
95
  readonly system: ActorSystem;
108
- /**
109
- * Child actors spawned via `self.spawn` in this actor's handlers.
110
- * State-scoped children are auto-removed on state exit.
111
- */
96
+ /** Child actors spawned via `self.spawn` in this actor's handlers. */
112
97
  readonly children: ReadonlyMap<string, ActorRef<AnyState, unknown>>;
113
98
  }
114
99
  /** Base type for stored actors (internal) */
@@ -260,7 +245,7 @@ interface ActorSystem {
260
245
  /**
261
246
  * ActorSystem service tag
262
247
  */
263
- declare const ActorSystem: any;
248
+ declare const ActorSystem: Context.Tag<ActorSystem, ActorSystem>;
264
249
  /** Listener set for sync subscriptions */
265
250
  type Listeners<S> = Set<(state: S) => void>;
266
251
  /**
@@ -274,7 +259,7 @@ declare const buildActorRefCore: <S extends {
274
259
  readonly _tag: string;
275
260
  }, E extends {
276
261
  readonly _tag: string;
277
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<E>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>) => ActorRef<S, E>;
262
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<QueuedEvent<E>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>) => ActorRef<S, E>;
278
263
  /**
279
264
  * Create and start an actor for a machine
280
265
  */
@@ -282,10 +267,12 @@ declare const createActor: <S extends {
282
267
  readonly _tag: string;
283
268
  }, E extends {
284
269
  readonly _tag: string;
285
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) => Effect.Effect<ActorRef<S, E>, unknown, unknown>;
270
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) => Effect.Effect<ActorRef<S, E>, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, effect_dist_dts_Tracer_js0.ParentSpan> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope> | Exclude<Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>, effect_dist_dts_Tracer_js0.ParentSpan>>;
271
+ /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
272
+ declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
286
273
  /**
287
274
  * Default ActorSystem layer
288
275
  */
289
- declare const Default: any;
276
+ declare const Default: Layer.Layer<ActorSystem, never, never>;
290
277
  //#endregion
291
- export { ActorRef, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
278
+ export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };