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,12 +1,12 @@
1
1
  import { Inspector } from "../inspection.js";
2
2
  import { INTERNAL_INIT_EVENT, stubSystem } from "../internal/utils.js";
3
- import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler } from "../internal/transition.js";
3
+ import { NoReplyError } from "../errors.js";
4
4
  import { emitWithTimestamp } from "../internal/inspection.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
-
9
- //#region src-v3/persistence/persistent-actor.ts
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
+ //#region src/persistence/persistent-actor.ts
10
10
  /** Get current time in milliseconds using Effect Clock */
11
11
  const now = Clock.currentTimeMillis;
12
12
  /**
@@ -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);
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);
@@ -146,6 +153,7 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
146
153
  const backgroundFibers = [];
147
154
  const initEvent = { _tag: INTERNAL_INIT_EVENT };
148
155
  const initCtx = {
156
+ actorId: id,
149
157
  state: resolvedInitial,
150
158
  event: initEvent,
151
159
  self,
@@ -154,6 +162,7 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
154
162
  const { effects: effectSlots } = typedMachine._slots;
155
163
  for (const bg of typedMachine.backgroundEffects) {
156
164
  const fiber = yield* Effect.forkDaemon(bg.handler({
165
+ actorId: id,
157
166
  state: resolvedInitial,
158
167
  event: initEvent,
159
168
  self,
@@ -176,9 +185,10 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
176
185
  finalState: resolvedInitial,
177
186
  timestamp
178
187
  }));
179
- 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());
180
189
  }
181
- const loopFiber = yield* Effect.forkDaemon(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.forkDaemon(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies));
182
192
  return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
183
193
  const finalState = yield* SubscriptionRef.get(stateRef);
184
194
  yield* emitWithTimestamp(inspector, (timestamp) => ({
@@ -189,16 +199,17 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
189
199
  }));
190
200
  yield* Ref.set(stoppedRef, true);
191
201
  yield* Fiber.interrupt(loopFiber);
202
+ yield* settlePendingReplies(pendingReplies, id);
192
203
  yield* Scope.close(stateScopeRef.current, Exit.void);
193
204
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
194
205
  yield* Fiber.interrupt(snapshotFiber);
195
206
  yield* Fiber.interrupt(persistenceFiber);
196
- }).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);
197
208
  });
198
209
  /**
199
210
  * Main event loop for persistent actor
200
211
  */
201
- 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) {
202
213
  const { machine, persistence } = persistentMachine;
203
214
  const typedMachine = machine;
204
215
  const hooks = inspector === void 0 ? void 0 : {
@@ -227,10 +238,28 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
227
238
  timestamp
228
239
  }))
229
240
  };
241
+ const postponed = [];
242
+ const pendingDrain = [];
243
+ const hasPostponeRules = machine.postponeRules.length > 0;
230
244
  while (true) {
231
- const event = yield* Queue.take(eventQueue);
245
+ const queued = pendingDrain.length > 0 ? pendingDrain.shift() : yield* Queue.take(eventQueue);
246
+ const event = queued.event;
232
247
  const currentState = yield* SubscriptionRef.get(stateRef);
233
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
+ }
234
263
  yield* emitWithTimestamp(inspector, (timestamp) => ({
235
264
  type: "@machine.event",
236
265
  actorId: id,
@@ -238,7 +267,13 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
238
267
  event,
239
268
  timestamp
240
269
  }));
241
- const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, hooks);
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
+ }));
242
277
  if (!result.transitioned) continue;
243
278
  const newVersion = currentVersion + 1;
244
279
  yield* Ref.set(versionRef, newVersion);
@@ -266,12 +301,15 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
266
301
  timestamp
267
302
  }));
268
303
  yield* Ref.set(stoppedRef, true);
304
+ postponed.length = 0;
305
+ yield* settlePendingReplies(pendingReplies, id);
269
306
  yield* Scope.close(stateScopeRef.current, Exit.void);
270
307
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
271
308
  yield* Fiber.interrupt(snapshotFiber);
272
309
  yield* Fiber.interrupt(persistenceFiber);
273
310
  return;
274
311
  }
312
+ if (result.lifecycleRan && postponed.length > 0) pendingDrain.push(...postponed.splice(0));
275
313
  }
276
314
  });
277
315
  /**
@@ -286,7 +324,7 @@ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.persistentActor.
286
324
  state,
287
325
  timestamp
288
326
  }));
289
- yield* runSpawnEffects(machine, state, event, self, stateScope, system, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
327
+ yield* runSpawnEffects(machine, state, event, self, stateScope, system, actorId, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
290
328
  type: "@machine.error",
291
329
  actorId,
292
330
  phase: info.phase,
@@ -362,6 +400,5 @@ const restorePersistentActor = Effect.fn("effect-machine.persistentActor.restore
362
400
  const actor = yield* createPersistentActor(id, persistentMachine, maybeSnapshot, events);
363
401
  return Option.some(actor);
364
402
  });
365
-
366
403
  //#endregion
367
- export { createPersistentActor, restorePersistentActor };
404
+ export { createPersistentActor, restorePersistentActor };
@@ -2,7 +2,7 @@ import { EventBrand, StateBrand } from "../internal/brands.js";
2
2
  import { Machine } from "../machine.js";
3
3
  import { Schedule, Schema } from "effect";
4
4
 
5
- //#region src-v3/persistence/persistent-machine.d.ts
5
+ //#region src/persistence/persistent-machine.d.ts
6
6
  type BrandedState = {
7
7
  readonly _tag: string;
8
8
  } & StateBrand;
@@ -1,6 +1,5 @@
1
1
  import { MissingSchemaError } from "../errors.js";
2
-
3
- //#region src-v3/persistence/persistent-machine.ts
2
+ //#region src/persistence/persistent-machine.ts
4
3
  /**
5
4
  * Type guard to check if a value is a PersistentMachine
6
5
  */
@@ -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 };
@@ -1,7 +1,7 @@
1
1
  import { FullEventBrand, FullStateBrand } from "./internal/brands.js";
2
2
  import { Schema } from "effect";
3
3
 
4
- //#region src-v3/schema.d.ts
4
+ //#region src/schema.d.ts
5
5
  /**
6
6
  * Extract the TypeScript type from a TaggedStruct schema
7
7
  */
@@ -1,7 +1,6 @@
1
1
  import { InvalidSchemaError, MissingMatchHandlerError } from "./errors.js";
2
2
  import { Schema } from "effect";
3
-
4
- //#region src-v3/schema.ts
3
+ //#region src/schema.ts
5
4
  /**
6
5
  * Schema-first State/Event definitions for effect-machine.
7
6
  *
@@ -59,7 +58,10 @@ const buildMachineSchema = (definition) => {
59
58
  constructor.derive = (source, partial) => {
60
59
  const result = { _tag: tag };
61
60
  for (const key of fieldNames) if (key in source) result[key] = source[key];
62
- if (partial !== void 0) for (const [key, value] of Object.entries(partial)) result[key] = value;
61
+ if (partial !== void 0) for (const [key, value] of Object.entries(partial)) {
62
+ if (key === "_tag") continue;
63
+ result[key] = value;
64
+ }
63
65
  return result;
64
66
  };
65
67
  constructors[tag] = constructor;
@@ -160,6 +162,5 @@ const State = (definition) => createMachineSchema(definition);
160
162
  * ```
161
163
  */
162
164
  const Event = (definition) => createMachineSchema(definition);
163
-
164
165
  //#endregion
165
- export { Event, State };
166
+ export { Event, State };
@@ -1,7 +1,7 @@
1
1
  import { ActorSystem } from "./actor.js";
2
- import { Effect, Schema } from "effect";
2
+ import { Context, Effect, Schema } from "effect";
3
3
 
4
- //#region src-v3/slot.d.ts
4
+ //#region src/slot.d.ts
5
5
  /** Schema fields definition (like Schema.Struct.Fields) */
6
6
  type Fields = Record<string, Schema.Schema.All>;
7
7
  /** Extract the encoded type from schema fields (used for parameters) */
@@ -43,6 +43,7 @@ type EffectSlots<D extends EffectsDef> = { readonly [K in keyof D & string]: Eff
43
43
  * Shared across all machines via MachineContextTag.
44
44
  */
45
45
  interface MachineContext<State, Event, Self> {
46
+ readonly actorId: string;
46
47
  readonly state: State;
47
48
  readonly event: Event;
48
49
  readonly self: Self;
@@ -53,7 +54,7 @@ interface MachineContext<State, Event, Self> {
53
54
  * Single module-level tag instead of per-machine allocation.
54
55
  * @internal
55
56
  */
56
- declare const MachineContextTag: any;
57
+ declare const MachineContextTag: Context.Tag<MachineContext<any, any, any>, MachineContext<any, any, any>>;
57
58
  /**
58
59
  * Guard handler implementation.
59
60
  * Receives params and context, returns Effect<boolean>.
@@ -1,6 +1,5 @@
1
1
  import { Context } from "effect";
2
-
3
- //#region src-v3/slot.ts
2
+ //#region src/slot.ts
4
3
  /**
5
4
  * Slot module - schema-based, parameterized guards and effects.
6
5
  *
@@ -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 };
@@ -1,9 +1,9 @@
1
+ import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
1
2
  import { AssertionError } from "./errors.js";
2
- import { EffectsDef, GuardsDef } from "./slot.js";
3
- import { BuiltMachine, Machine } from "./machine.js";
3
+ import { BuiltMachine, Machine, MachineRef } from "./machine.js";
4
4
  import { Effect, SubscriptionRef } from "effect";
5
5
 
6
- //#region src-v3/testing.d.ts
6
+ //#region src/testing.d.ts
7
7
  /** Accept either Machine or BuiltMachine for testing utilities. */
8
8
  type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> = Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>;
9
9
  /**
@@ -40,7 +40,7 @@ declare const simulate: <S extends {
40
40
  }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[]) => Effect.Effect<{
41
41
  states: S[];
42
42
  finalState: S;
43
- }, never, Exclude<R, unknown>>;
43
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
44
44
  /**
45
45
  * Assert that a machine can reach a specific state given a sequence of events
46
46
  */
@@ -48,7 +48,7 @@ declare const assertReaches: <S extends {
48
48
  readonly _tag: string;
49
49
  }, E extends {
50
50
  readonly _tag: string;
51
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string) => Effect.Effect<any, unknown, unknown>;
51
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string) => Effect.Effect<S, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
52
52
  /**
53
53
  * Assert that a machine follows a specific path of state tags
54
54
  *
@@ -65,7 +65,10 @@ declare const assertPath: <S extends {
65
65
  readonly _tag: string;
66
66
  }, E extends {
67
67
  readonly _tag: string;
68
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<any, unknown, unknown>;
68
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<{
69
+ states: S[];
70
+ finalState: S;
71
+ }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
69
72
  /**
70
73
  * Assert that a machine never reaches a specific state given a sequence of events
71
74
  *
@@ -83,7 +86,10 @@ declare const assertNeverReaches: <S extends {
83
86
  readonly _tag: string;
84
87
  }, E extends {
85
88
  readonly _tag: string;
86
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string) => Effect.Effect<any, unknown, unknown>;
89
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string) => Effect.Effect<{
90
+ states: S[];
91
+ finalState: S;
92
+ }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
87
93
  /**
88
94
  * Create a controllable test harness for a machine
89
95
  */
@@ -129,7 +135,7 @@ declare const createTestHarness: <S extends {
129
135
  readonly _tag: string;
130
136
  }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, options?: TestHarnessOptions<S, E> | undefined) => Effect.Effect<{
131
137
  state: SubscriptionRef.SubscriptionRef<S>;
132
- send: (event: E) => Effect.Effect<S, never, never>;
138
+ send: (event: E) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
133
139
  getState: Effect.Effect<S, never, never>;
134
140
  }, never, never>;
135
141
  //#endregion
@@ -1,10 +1,9 @@
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
- //#region src-v3/testing.ts
6
+ //#region src/testing.ts
8
7
  /**
9
8
  * Simulate a sequence of events through a machine without running an actor.
10
9
  * Useful for testing state transitions in isolation.
@@ -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) {
37
- const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem);
40
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
41
+ postponed.push(event);
42
+ continue;
43
+ }
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);
126
- const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem);
155
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
156
+ postponed.push(event);
157
+ return currentState;
158
+ }
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 };
@@ -1,18 +0,0 @@
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) {
6
- __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true
9
- });
10
- }
11
- if (!no_symbols) {
12
- __defProp(target, Symbol.toStringTag, { value: "Module" });
13
- }
14
- return target;
15
- };
16
-
17
- //#endregion
18
- export { __exportAll };
@@ -1,27 +0,0 @@
1
- //#region src-v3/errors.d.ts
2
- declare const DuplicateActorError_base: any;
3
- /** Attempted to spawn/restore actor with ID already in use */
4
- declare class DuplicateActorError extends DuplicateActorError_base {}
5
- declare const UnprovidedSlotsError_base: any;
6
- /** Machine has unprovided effect slots */
7
- declare class UnprovidedSlotsError extends UnprovidedSlotsError_base {}
8
- declare const MissingSchemaError_base: any;
9
- /** Operation requires schemas attached to machine */
10
- declare class MissingSchemaError extends MissingSchemaError_base {}
11
- declare const InvalidSchemaError_base: any;
12
- /** State/Event schema has no variants */
13
- declare class InvalidSchemaError extends InvalidSchemaError_base {}
14
- declare const MissingMatchHandlerError_base: any;
15
- /** $match called with missing handler for tag */
16
- declare class MissingMatchHandlerError extends MissingMatchHandlerError_base {}
17
- declare const SlotProvisionError_base: any;
18
- /** Slot handler not found at runtime (internal error) */
19
- declare class SlotProvisionError extends SlotProvisionError_base {}
20
- declare const ProvisionValidationError_base: any;
21
- /** Machine.build() validation failed - missing or extra handlers */
22
- declare class ProvisionValidationError extends ProvisionValidationError_base {}
23
- declare const AssertionError_base: any;
24
- /** Assertion failed in testing utilities */
25
- declare class AssertionError extends AssertionError_base {}
26
- //#endregion
27
- export { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
@@ -1,13 +0,0 @@
1
- import { Event, MachineEventSchema, MachineStateSchema, State } from "./schema.js";
2
- import { PersistenceConfig, PersistentMachine, isPersistentMachine } from "./persistence/persistent-machine.js";
3
- import { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
4
- import { EffectHandlers, EffectSlot, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlot, GuardSlots, GuardsDef, GuardsSchema, MachineContext, Slot } from "./slot.js";
5
- import { PersistentActorRef, createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
6
- import { ActorMetadata, PersistedEvent, PersistenceAdapter, PersistenceAdapterTag, PersistenceError, RestoreFailure, RestoreResult, Snapshot, VersionConflictError } from "./persistence/adapter.js";
7
- import { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter } from "./persistence/adapters/in-memory.js";
8
- import "./persistence/index.js";
9
- import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, ProvideHandlers, SpawnEffect, StateHandlerContext, Transition, machine_d_exports } from "./machine.js";
10
- import { ActorRef, ActorSystem, Default, SystemEvent, SystemEventListener } from "./actor.js";
11
- import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
12
- import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, SpawnEvent, StopEvent, TransitionEvent, collectingInspector, consoleInspector, makeInspector } 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, 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 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 TestHarness, type TestHarnessOptions, type Transition, type TransitionEvent, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, restorePersistentActor, simulate };
package/dist-v3/index.js DELETED
@@ -1,14 +0,0 @@
1
- import { Inspector, collectingInspector, consoleInspector, makeInspector } from "./inspection.js";
2
- import { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
3
- import { isPersistentMachine } from "./persistence/persistent-machine.js";
4
- import { Slot } from "./slot.js";
5
- 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
- import { ActorSystem, Default } from "./actor.js";
9
- import { Event, State } from "./schema.js";
10
- 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
-
14
- 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, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, restorePersistentActor, simulate };
@@ -1,50 +0,0 @@
1
- import { Context } from "effect";
2
-
3
- //#region src-v3/inspection.ts
4
- /**
5
- * Inspector service tag - optional service for machine introspection
6
- * Uses `any` types to allow variance flexibility when providing the service
7
- */
8
- const Inspector = Context.GenericTag("@effect/machine/Inspector");
9
- /**
10
- * Create an inspector from a callback function.
11
- *
12
- * Type params accept either raw tagged types or Schema constructors:
13
- * - `makeInspector(cb)` — defaults to `AnyInspectionEvent`
14
- * - `makeInspector<MyState, MyEvent>(cb)` — explicit tagged types
15
- * - `makeInspector<typeof MyState, typeof MyEvent>(cb)` — schema constructors (auto-extracts `.Type`)
16
- */
17
- const makeInspector = (onInspect) => ({ onInspect });
18
- /**
19
- * Console inspector that logs events in a readable format
20
- */
21
- const consoleInspector = () => makeInspector((event) => {
22
- const prefix = `[${event.actorId}]`;
23
- switch (event.type) {
24
- case "@machine.spawn":
25
- console.log(prefix, "spawned →", event.initialState._tag);
26
- break;
27
- case "@machine.event":
28
- console.log(prefix, "received", event.event._tag, "in", event.state._tag);
29
- break;
30
- case "@machine.transition":
31
- console.log(prefix, event.fromState._tag, "→", event.toState._tag);
32
- break;
33
- case "@machine.effect":
34
- console.log(prefix, event.effectType, "effect in", event.state._tag);
35
- break;
36
- case "@machine.error":
37
- console.log(prefix, "error in", event.phase, event.state._tag, "-", event.error);
38
- break;
39
- case "@machine.stop":
40
- console.log(prefix, "stopped in", event.finalState._tag);
41
- break;
42
- }
43
- });
44
- /**
45
- * Collecting inspector that stores events in an array for testing
46
- */
47
- const collectingInspector = (events) => ({ onInspect: (event) => events.push(event) });
48
-
49
- //#endregion
50
- export { Inspector, collectingInspector, consoleInspector, makeInspector };
@@ -1,15 +0,0 @@
1
- import { Clock, Effect } from "effect";
2
-
3
- //#region src-v3/internal/inspection.ts
4
- /**
5
- * Emit an inspection event with timestamp from Clock.
6
- * @internal
7
- */
8
- const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function* (inspector, makeEvent) {
9
- if (inspector === void 0) return;
10
- const timestamp = yield* Clock.currentTimeMillis;
11
- yield* Effect.try(() => inspector.onInspect(makeEvent(timestamp))).pipe(Effect.ignore);
12
- });
13
-
14
- //#endregion
15
- export { emitWithTimestamp };
File without changes
File without changes
File without changes