effect-machine 0.9.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 (62) hide show
  1. package/README.md +76 -16
  2. package/dist/actor.d.ts +55 -89
  3. package/dist/actor.js +135 -30
  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 +3 -3
  8. package/dist/index.js +2 -2
  9. package/dist/internal/transition.d.ts +26 -2
  10. package/dist/internal/transition.js +37 -8
  11. package/dist/internal/utils.d.ts +7 -2
  12. package/dist/machine.d.ts +66 -3
  13. package/dist/machine.js +65 -0
  14. package/dist/persistence/persistent-actor.js +52 -16
  15. package/dist/testing.js +57 -3
  16. package/package.json +9 -8
  17. package/{dist-v3 → v3/dist}/actor.d.ts +65 -78
  18. package/{dist-v3 → v3/dist}/actor.js +173 -35
  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 +13 -0
  26. package/v3/dist/index.js +13 -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 +1 -1
  36. package/{dist-v3 → v3/dist}/machine.d.ts +86 -9
  37. package/{dist-v3 → v3/dist}/machine.js +128 -2
  38. package/{dist-v3 → v3/dist}/persistence/adapter.d.ts +18 -5
  39. package/{dist-v3 → v3/dist}/persistence/adapter.js +1 -1
  40. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.d.ts +1 -1
  41. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.js +1 -1
  42. package/{dist-v3 → v3/dist}/persistence/persistent-actor.d.ts +7 -6
  43. package/{dist-v3 → v3/dist}/persistence/persistent-actor.js +58 -19
  44. package/{dist-v3 → v3/dist}/persistence/persistent-machine.d.ts +1 -1
  45. package/{dist-v3 → v3/dist}/persistence/persistent-machine.js +1 -1
  46. package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
  47. package/{dist-v3 → v3/dist}/schema.js +5 -2
  48. package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
  49. package/{dist-v3 → v3/dist}/slot.js +1 -1
  50. package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
  51. package/{dist-v3 → v3/dist}/testing.js +60 -6
  52. package/dist-v3/errors.d.ts +0 -27
  53. package/dist-v3/index.d.ts +0 -12
  54. package/dist-v3/index.js +0 -13
  55. package/dist-v3/inspection.js +0 -48
  56. package/dist-v3/internal/inspection.js +0 -13
  57. /package/{dist-v3 → v3/dist}/_virtual/_rolldown/runtime.js +0 -0
  58. /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
  59. /package/{dist-v3 → v3/dist}/cluster/index.js +0 -0
  60. /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
  61. /package/{dist-v3 → v3/dist}/persistence/index.d.ts +0 -0
  62. /package/{dist-v3 → v3/dist}/persistence/index.js +0 -0
package/dist/testing.js CHANGED
@@ -1,7 +1,7 @@
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
6
  //#region src/testing.ts
7
7
  /**
@@ -26,18 +26,44 @@ import { Effect, SubscriptionRef } from "effect";
26
26
  */
27
27
  const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
28
28
  const machine = input instanceof BuiltMachine ? input._inner : input;
29
+ const dummySend = Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void);
29
30
  const dummySelf = {
30
- send: Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void),
31
+ send: dummySend,
32
+ cast: dummySend,
31
33
  spawn: () => Effect.die("spawn not supported in simulation")
32
34
  };
33
35
  let currentState = machine.initial;
34
36
  const states = [currentState];
37
+ const hasPostponeRules = machine.postponeRules.length > 0;
38
+ const postponed = [];
35
39
  for (const event of events) {
40
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
41
+ postponed.push(event);
42
+ continue;
43
+ }
36
44
  const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "simulation");
37
45
  if (!result.transitioned) continue;
46
+ const prevTag = currentState._tag;
38
47
  currentState = result.newState;
39
48
  states.push(currentState);
40
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
+ }
41
67
  }
42
68
  return {
43
69
  states,
@@ -113,20 +139,48 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
113
139
  */
114
140
  const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
115
141
  const machine = input instanceof BuiltMachine ? input._inner : input;
142
+ const dummySend = Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void);
116
143
  const dummySelf = {
117
- send: Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void),
144
+ send: dummySend,
145
+ cast: dummySend,
118
146
  spawn: () => Effect.die("spawn not supported in test harness")
119
147
  };
120
148
  const stateRef = yield* SubscriptionRef.make(machine.initial);
149
+ const hasPostponeRules = machine.postponeRules.length > 0;
150
+ const postponed = [];
121
151
  return {
122
152
  state: stateRef,
123
153
  send: Effect.fn("effect-machine.testHarness.send")(function* (event) {
124
154
  const currentState = yield* SubscriptionRef.get(stateRef);
155
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
156
+ postponed.push(event);
157
+ return currentState;
158
+ }
125
159
  const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "test-harness");
126
160
  if (!result.transitioned) return currentState;
161
+ const prevTag = currentState._tag;
127
162
  const newState = result.newState;
128
163
  yield* SubscriptionRef.set(stateRef, newState);
129
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
+ }
130
184
  return newState;
131
185
  }),
132
186
  getState: SubscriptionRef.get(stateRef)
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.9.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,17 +41,18 @@
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": {
@@ -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 };