effect-machine 0.3.1 → 0.4.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 +24 -0
  2. package/dist/_virtual/_rolldown/runtime.js +18 -0
  3. package/dist/actor.d.ts +256 -0
  4. package/dist/actor.js +402 -0
  5. package/dist/cluster/entity-machine.d.ts +90 -0
  6. package/dist/cluster/entity-machine.js +80 -0
  7. package/dist/cluster/index.d.ts +3 -0
  8. package/dist/cluster/index.js +4 -0
  9. package/dist/cluster/to-entity.d.ts +64 -0
  10. package/dist/cluster/to-entity.js +53 -0
  11. package/dist/errors.d.ts +61 -0
  12. package/dist/errors.js +38 -0
  13. package/dist/index.d.ts +13 -0
  14. package/dist/index.js +14 -0
  15. package/dist/inspection.d.ts +125 -0
  16. package/dist/inspection.js +50 -0
  17. package/dist/internal/brands.d.ts +40 -0
  18. package/dist/internal/brands.js +0 -0
  19. package/dist/internal/inspection.d.ts +11 -0
  20. package/dist/internal/inspection.js +15 -0
  21. package/dist/internal/transition.d.ts +160 -0
  22. package/dist/internal/transition.js +238 -0
  23. package/dist/internal/utils.d.ts +60 -0
  24. package/dist/internal/utils.js +46 -0
  25. package/dist/machine.d.ts +278 -0
  26. package/dist/machine.js +317 -0
  27. package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
  28. package/dist/persistence/adapter.js +27 -0
  29. package/dist/persistence/adapters/in-memory.d.ts +32 -0
  30. package/dist/persistence/adapters/in-memory.js +176 -0
  31. package/dist/persistence/index.d.ts +5 -0
  32. package/dist/persistence/index.js +6 -0
  33. package/dist/persistence/persistent-actor.d.ts +50 -0
  34. package/dist/persistence/persistent-actor.js +358 -0
  35. package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
  36. package/dist/persistence/persistent-machine.js +24 -0
  37. package/dist/schema.d.ts +141 -0
  38. package/dist/schema.js +165 -0
  39. package/dist/slot.d.ts +130 -0
  40. package/dist/slot.js +99 -0
  41. package/dist/testing.d.ts +142 -0
  42. package/dist/testing.js +138 -0
  43. package/package.json +28 -14
  44. package/src/actor.ts +0 -1058
  45. package/src/cluster/entity-machine.ts +0 -201
  46. package/src/cluster/index.ts +0 -43
  47. package/src/cluster/to-entity.ts +0 -99
  48. package/src/errors.ts +0 -64
  49. package/src/index.ts +0 -105
  50. package/src/inspection.ts +0 -178
  51. package/src/internal/brands.ts +0 -51
  52. package/src/internal/inspection.ts +0 -18
  53. package/src/internal/transition.ts +0 -489
  54. package/src/internal/utils.ts +0 -80
  55. package/src/machine.ts +0 -836
  56. package/src/persistence/adapters/in-memory.ts +0 -294
  57. package/src/persistence/index.ts +0 -24
  58. package/src/persistence/persistent-actor.ts +0 -791
  59. package/src/schema.ts +0 -362
  60. package/src/slot.ts +0 -281
  61. package/src/testing.ts +0 -284
  62. package/tsconfig.json +0 -65
@@ -0,0 +1,142 @@
1
+ import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
2
+ import { AssertionError } from "./errors.js";
3
+ import { BuiltMachine, Machine, MachineRef } from "./machine.js";
4
+ import { Effect, SubscriptionRef } from "effect";
5
+
6
+ //#region src/testing.d.ts
7
+ /** Accept either Machine or BuiltMachine for testing utilities. */
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
+ /**
10
+ * Result of simulating events through a machine
11
+ */
12
+ interface SimulationResult<S> {
13
+ readonly states: ReadonlyArray<S>;
14
+ readonly finalState: S;
15
+ }
16
+ /**
17
+ * Simulate a sequence of events through a machine without running an actor.
18
+ * Useful for testing state transitions in isolation.
19
+ * Does not run onEnter/spawn/background effects, but does run guard/effect slots
20
+ * within transition handlers.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const result = yield* simulate(
25
+ * fetcherMachine,
26
+ * [
27
+ * Event.Fetch({ url: "https://example.com" }),
28
+ * Event._Done({ data: { foo: "bar" } })
29
+ * ]
30
+ * )
31
+ *
32
+ * expect(result.finalState._tag).toBe("Success")
33
+ * expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
34
+ * ```
35
+ */
36
+ declare const simulate: <S extends {
37
+ readonly _tag: string;
38
+ }, E extends {
39
+ readonly _tag: string;
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
+ states: S[];
42
+ finalState: S;
43
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
44
+ /**
45
+ * Assert that a machine can reach a specific state given a sequence of events
46
+ */
47
+ declare const assertReaches: <S extends {
48
+ readonly _tag: string;
49
+ }, E extends {
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<S, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
52
+ /**
53
+ * Assert that a machine follows a specific path of state tags
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * yield* assertPath(
58
+ * machine,
59
+ * [Event.Start(), Event.Increment(), Event.Stop()],
60
+ * ["Idle", "Counting", "Counting", "Done"]
61
+ * )
62
+ * ```
63
+ */
64
+ declare const assertPath: <S extends {
65
+ readonly _tag: string;
66
+ }, E extends {
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<{
69
+ states: S[];
70
+ finalState: S;
71
+ }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
72
+ /**
73
+ * Assert that a machine never reaches a specific state given a sequence of events
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // Verify error handling doesn't reach crash state
78
+ * yield* assertNeverReaches(
79
+ * machine,
80
+ * [Event.Error(), Event.Retry(), Event.Success()],
81
+ * "Crashed"
82
+ * )
83
+ * ```
84
+ */
85
+ declare const assertNeverReaches: <S extends {
86
+ readonly _tag: string;
87
+ }, E extends {
88
+ readonly _tag: string;
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>>>>;
93
+ /**
94
+ * Create a controllable test harness for a machine
95
+ */
96
+ interface TestHarness<S, E, R> {
97
+ readonly state: SubscriptionRef.SubscriptionRef<S>;
98
+ readonly send: (event: E) => Effect.Effect<S, never, R>;
99
+ readonly getState: Effect.Effect<S>;
100
+ }
101
+ /**
102
+ * Options for creating a test harness
103
+ */
104
+ interface TestHarnessOptions<S, E> {
105
+ /**
106
+ * Called after each transition with the previous state, event, and new state.
107
+ * Useful for logging or spying on transitions.
108
+ */
109
+ readonly onTransition?: (from: S, event: E, to: S) => void;
110
+ }
111
+ /**
112
+ * Create a test harness for step-by-step testing.
113
+ * Does not run onEnter/spawn/background effects, but does run guard/effect slots
114
+ * within transition handlers.
115
+ *
116
+ * @example Basic usage
117
+ * ```ts
118
+ * const harness = yield* createTestHarness(machine)
119
+ * yield* harness.send(Event.Start())
120
+ * const state = yield* harness.getState
121
+ * ```
122
+ *
123
+ * @example With transition observer
124
+ * ```ts
125
+ * const transitions: Array<{ from: string; event: string; to: string }> = []
126
+ * const harness = yield* createTestHarness(machine, {
127
+ * onTransition: (from, event, to) =>
128
+ * transitions.push({ from: from._tag, event: event._tag, to: to._tag })
129
+ * })
130
+ * ```
131
+ */
132
+ declare const createTestHarness: <S extends {
133
+ readonly _tag: string;
134
+ }, E extends {
135
+ readonly _tag: string;
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<{
137
+ state: SubscriptionRef.SubscriptionRef<S>;
138
+ send: (event: E) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
139
+ getState: Effect.Effect<S, never, never>;
140
+ }, never, never>;
141
+ //#endregion
142
+ export { AssertionError, SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate };
@@ -0,0 +1,138 @@
1
+ import { stubSystem } from "./internal/utils.js";
2
+ import { AssertionError } from "./errors.js";
3
+ import { BuiltMachine } from "./machine.js";
4
+ import { executeTransition } from "./internal/transition.js";
5
+ import { Effect, SubscriptionRef } from "effect";
6
+
7
+ //#region src/testing.ts
8
+ /**
9
+ * Simulate a sequence of events through a machine without running an actor.
10
+ * Useful for testing state transitions in isolation.
11
+ * Does not run onEnter/spawn/background effects, but does run guard/effect slots
12
+ * within transition handlers.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const result = yield* simulate(
17
+ * fetcherMachine,
18
+ * [
19
+ * Event.Fetch({ url: "https://example.com" }),
20
+ * Event._Done({ data: { foo: "bar" } })
21
+ * ]
22
+ * )
23
+ *
24
+ * expect(result.finalState._tag).toBe("Success")
25
+ * expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
26
+ * ```
27
+ */
28
+ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
29
+ const machine = input instanceof BuiltMachine ? input._inner : input;
30
+ const dummySelf = {
31
+ send: Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void),
32
+ spawn: () => Effect.die("spawn not supported in simulation")
33
+ };
34
+ let currentState = machine.initial;
35
+ const states = [currentState];
36
+ for (const event of events) {
37
+ const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem);
38
+ if (!result.transitioned) continue;
39
+ currentState = result.newState;
40
+ states.push(currentState);
41
+ if (machine.finalStates.has(currentState._tag)) break;
42
+ }
43
+ return {
44
+ states,
45
+ finalState: currentState
46
+ };
47
+ });
48
+ /**
49
+ * Assert that a machine can reach a specific state given a sequence of events
50
+ */
51
+ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag) {
52
+ const result = yield* simulate(input, events);
53
+ if (result.finalState._tag !== expectedTag) return yield* new AssertionError({ message: `Expected final state "${expectedTag}" but got "${result.finalState._tag}". States visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
54
+ return result.finalState;
55
+ });
56
+ /**
57
+ * Assert that a machine follows a specific path of state tags
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * yield* assertPath(
62
+ * machine,
63
+ * [Event.Start(), Event.Increment(), Event.Stop()],
64
+ * ["Idle", "Counting", "Counting", "Done"]
65
+ * )
66
+ * ```
67
+ */
68
+ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath) {
69
+ const result = yield* simulate(input, events);
70
+ const actualPath = result.states.map((s) => s._tag);
71
+ if (actualPath.length !== expectedPath.length) return yield* new AssertionError({ message: `Path length mismatch. Expected ${expectedPath.length} states but got ${actualPath.length}.\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
72
+ for (let i = 0; i < expectedPath.length; i++) if (actualPath[i] !== expectedPath[i]) return yield* new AssertionError({ message: `Path mismatch at position ${i}. Expected "${expectedPath[i]}" but got "${actualPath[i]}".\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
73
+ return result;
74
+ });
75
+ /**
76
+ * Assert that a machine never reaches a specific state given a sequence of events
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // Verify error handling doesn't reach crash state
81
+ * yield* assertNeverReaches(
82
+ * machine,
83
+ * [Event.Error(), Event.Retry(), Event.Success()],
84
+ * "Crashed"
85
+ * )
86
+ * ```
87
+ */
88
+ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag) {
89
+ const result = yield* simulate(input, events);
90
+ const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
91
+ if (visitedIndex !== -1) return yield* new AssertionError({ message: `Machine reached forbidden state "${forbiddenTag}" at position ${visitedIndex}.\nStates visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
92
+ return result;
93
+ });
94
+ /**
95
+ * Create a test harness for step-by-step testing.
96
+ * Does not run onEnter/spawn/background effects, but does run guard/effect slots
97
+ * within transition handlers.
98
+ *
99
+ * @example Basic usage
100
+ * ```ts
101
+ * const harness = yield* createTestHarness(machine)
102
+ * yield* harness.send(Event.Start())
103
+ * const state = yield* harness.getState
104
+ * ```
105
+ *
106
+ * @example With transition observer
107
+ * ```ts
108
+ * const transitions: Array<{ from: string; event: string; to: string }> = []
109
+ * const harness = yield* createTestHarness(machine, {
110
+ * onTransition: (from, event, to) =>
111
+ * transitions.push({ from: from._tag, event: event._tag, to: to._tag })
112
+ * })
113
+ * ```
114
+ */
115
+ const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
116
+ const machine = input instanceof BuiltMachine ? input._inner : input;
117
+ const dummySelf = {
118
+ send: Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void),
119
+ spawn: () => Effect.die("spawn not supported in test harness")
120
+ };
121
+ const stateRef = yield* SubscriptionRef.make(machine.initial);
122
+ return {
123
+ state: stateRef,
124
+ send: Effect.fn("effect-machine.testHarness.send")(function* (event) {
125
+ const currentState = yield* SubscriptionRef.get(stateRef);
126
+ const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem);
127
+ if (!result.transitioned) return currentState;
128
+ const newState = result.newState;
129
+ yield* SubscriptionRef.set(stateRef, newState);
130
+ if (options?.onTransition !== void 0) options.onTransition(currentState, event, newState);
131
+ return newState;
132
+ }),
133
+ getState: SubscriptionRef.get(stateRef)
134
+ };
135
+ });
136
+
137
+ //#endregion
138
+ export { AssertionError, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate };
package/package.json CHANGED
@@ -1,18 +1,27 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"
7
7
  },
8
8
  "files": [
9
- "src",
10
- "tsconfig.json"
9
+ "dist"
11
10
  ],
12
11
  "type": "module",
13
12
  "exports": {
14
- ".": "./src/index.ts",
15
- "./cluster": "./src/cluster/index.ts"
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "./cluster": {
20
+ "import": {
21
+ "types": "./dist/cluster/index.d.ts",
22
+ "default": "./dist/cluster/index.js"
23
+ }
24
+ }
16
25
  },
17
26
  "publishConfig": {
18
27
  "access": "public"
@@ -25,27 +34,29 @@
25
34
  "fmt:check": "oxfmt --check",
26
35
  "test": "bun test",
27
36
  "test:watch": "bun test --watch",
28
- "gate": "concurrently -n type,lint,fmt,test -c blue,yellow,magenta,green \"bun run typecheck\" \"bun run lint:fix\" \"bun run fmt\" \"bun run test\"",
37
+ "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\"",
29
38
  "prepare": "lefthook install || true && effect-language-service patch",
30
39
  "version": "changeset version",
31
- "release": "changeset publish"
40
+ "build": "tsdown",
41
+ "release": "bun run build && changeset publish"
32
42
  },
33
43
  "dependencies": {
34
- "effect": "^3.19.15"
44
+ "effect": "^3.19.16"
35
45
  },
36
46
  "devDependencies": {
37
47
  "@changesets/changelog-github": "^0.5.2",
38
48
  "@changesets/cli": "^2.29.8",
39
- "@effect/cluster": "^0.56.1",
49
+ "@effect/cluster": "^0.56.2",
40
50
  "@effect/experimental": "^0.58.0",
41
- "@effect/language-service": "^0.72.0",
51
+ "@effect/language-service": "^0.73.0",
42
52
  "@effect/rpc": "^0.73.0",
43
- "@types/bun": "latest",
53
+ "@types/bun": "1.3.8",
44
54
  "concurrently": "^9.2.1",
45
55
  "effect-bun-test": "^0.1.0",
46
- "lefthook": "^2.0.15",
47
- "oxfmt": "^0.26.0",
48
- "oxlint": "^1.41.0",
56
+ "lefthook": "^2.1.0",
57
+ "oxfmt": "^0.28.0",
58
+ "oxlint": "^1.43.0",
59
+ "tsdown": "^0.20.3",
49
60
  "typescript": "^5.9.3"
50
61
  },
51
62
  "peerDependencies": {
@@ -59,5 +70,8 @@
59
70
  "@effect/rpc": {
60
71
  "optional": true
61
72
  }
73
+ },
74
+ "overrides": {
75
+ "effect": "^3.19.16"
62
76
  }
63
77
  }