pure-effect 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  It implements the "Functional Core, Imperative Shell" pattern, allowing you to decouple your business logic from external side effects like database calls or API requests. Instead of executing side effects immediately, your functions return Commands which are executed later by an interpreter.
6
6
 
7
+ **Pure Effect** comes with JSDoc type annotations, so it can be used with TypeScript as well.
8
+
7
9
  ## Installation
8
10
 
9
11
  ```bash
@@ -28,13 +30,13 @@ const validateRegistration = (input) => {
28
30
  const findUser = (email) => {
29
31
  const cmdFindUser = () => db.findUser(email); // The work to do later
30
32
  const next = (user) => Success(user); // Wrap result in Success
31
- return Command(cmd, next);
33
+ return Command(cmdFindUser, next);
32
34
  };
33
35
 
34
36
  const saveUser = (input) => {
35
37
  const cmdSaveUser = () => db.saveUser(input);
36
38
  const next = (saved) => Success(saved);
37
- return Command(cmd, next);
39
+ return Command(cmdSaveUser, next);
38
40
  };
39
41
 
40
42
  const ensureEmailAvailable = (user) => {
@@ -52,8 +54,6 @@ const registerUserFlow = (input) =>
52
54
 
53
55
  // The Imperative Shell
54
56
  async function registerUser() {
55
- const input = { email: 'new@test.com', password: 'password123' };
56
-
57
57
  // logic is just a data structure until we pass it to runEffect
58
58
  const logic = registerUserFlow(input);
59
59
 
package/index.js CHANGED
@@ -1,7 +1,50 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {{ type: 'Success', value: any }} SuccessState */
4
+ /** @typedef {{ type: 'Failure', error: any }} FailureState */
5
+ /**
6
+ * @typedef {{
7
+ * type: 'Command',
8
+ * cmd: () => Promise<any>|any,
9
+ * next: (result: any) => Effect
10
+ * }} CommandState
11
+ */
12
+
13
+ /**
14
+ * The Union type for all possible states
15
+ * @typedef {SuccessState | FailureState | CommandState} Effect
16
+ */
17
+
18
+ /**
19
+ * Represents a successful computation
20
+ * @param {any} value - The result value
21
+ * @returns {SuccessState}
22
+ */
1
23
  const Success = (value) => ({ type: 'Success', value });
24
+
25
+ /**
26
+ * Represents a failed computation. Stops the pipeline execution
27
+ * @param {any} error - The error reason (string, Error object, etc).
28
+ * @returns {FailureState}
29
+ */
2
30
  const Failure = (error) => ({ type: 'Failure', error });
31
+
32
+ /**
33
+ * Represents a side effect to be executed later
34
+ * @param {() => Promise<any>|any} cmd - The side-effect function to execute
35
+ * @param {(result: any) => Effect} next - A function that receives the result of `cmd` and returns the next Effect
36
+ * @returns {CommandState}
37
+ */
3
38
  const Command = (cmd, next) => ({ type: 'Command', cmd, next });
4
39
 
40
+ /**
41
+ * Connects an Effect to the next function in the pipeline.
42
+ * Handles the branching logic for Success, Failure, and Command.
43
+ *
44
+ * @param {Effect} effect - The current Effect object
45
+ * @param {(value: any) => Effect} fn - The next function to run if the current effect is a Success
46
+ * @returns {Effect} The composed Effect
47
+ */
5
48
  const chain = (effect, fn) => {
6
49
  switch (effect.type) {
7
50
  case 'Success':
@@ -9,15 +52,29 @@ const chain = (effect, fn) => {
9
52
  case 'Failure':
10
53
  return effect;
11
54
  case 'Command':
12
- const next = (result) => chain(effect.next(result), fn);
55
+ const next = (/** @type {Effect} */ result) => chain(effect.next(result), fn);
13
56
  return Command(effect.cmd, next);
14
57
  }
15
58
  };
16
59
 
60
+ /**
61
+ * Composes a list of functions into a single Effect pipeline.
62
+ * Each function receives the output of the previous one.
63
+ *
64
+ * @param {...(input: any) => Effect} fns - Functions that return Success, Failure, or Command.
65
+ * @returns {(start: any) => Effect} A function that accepts an initial input and returns the final Effect tree.
66
+ */
17
67
  const effectPipe = (...fns) => {
18
68
  return (start) => fns.reduce(chain, Success(start));
19
69
  };
20
70
 
71
+ /**
72
+ * The Interpreter
73
+ * Iterates through the Effect tree, executing Commands and handling async flow.
74
+ *
75
+ * @param {Effect} effect - The Effect tree returned by a pipeline
76
+ * @returns {Promise<SuccessState | FailureState>}
77
+ */
21
78
  async function runEffect(effect) {
22
79
  while (effect.type === 'Command') {
23
80
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pure-effect",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A tiny, zero-dependency effect system for writing pure, testable JavaScript without mocks.",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
@@ -11,6 +11,8 @@
11
11
  "homepage": "https://github.com/aycangulez/pure-effect",
12
12
  "license": "MIT",
13
13
  "devDependencies": {
14
+ "@types/mocha": "^10.0.10",
15
+ "@types/node": "^24.10.1",
14
16
  "mocha": "^11.7.5"
15
17
  }
16
18
  }
package/test/all.js CHANGED
@@ -1,19 +1,23 @@
1
+ // @ts-check
2
+
1
3
  import { strict as assert } from 'assert';
2
4
  import { Success, Failure, Command, effectPipe, runEffect } from '../index.js';
3
5
 
6
+ /** @typedef {{id?: number, email: string, password: string}} User */
7
+
4
8
  const db = {
5
9
  users: new Map(),
6
- async findUserByEmail(email) {
10
+ async findUserByEmail(/** @type string */ email) {
7
11
  return this.users.get(email) || null;
8
12
  },
9
- async saveUser(user) {
10
- const u = { id: Date.now(), ...user };
13
+ async saveUser(/** @type {User} */ user) {
14
+ const u = { ...user, id: Date.now() };
11
15
  this.users.set(user.email, u);
12
16
  return u;
13
17
  },
14
18
  };
15
19
 
16
- function validateRegistration(input) {
20
+ function validateRegistration(/** @type {User} */ input) {
17
21
  const { email, password } = input;
18
22
  if (!email?.includes('@')) {
19
23
  return Failure('Invalid email format.');
@@ -24,26 +28,26 @@ function validateRegistration(input) {
24
28
  return Success(input);
25
29
  }
26
30
 
27
- function findUserByEmail(email) {
31
+ function findUserByEmail(/** @type string */ email) {
28
32
  const cmdFindUser = () => db.findUserByEmail(email);
29
- const next = (foundUser) => Success(foundUser);
33
+ const next = (/** @type {User} */ foundUser) => Success(foundUser);
30
34
  return Command(cmdFindUser, next);
31
35
  }
32
36
 
33
- function ensureEmailIsAvailable(foundUser) {
37
+ function ensureEmailIsAvailable(/** @type {User} */ foundUser) {
34
38
  return foundUser ? Failure('Email already in use.') : Success(true);
35
39
  }
36
40
 
37
- function saveUser(input) {
41
+ function saveUser(/** @type {User} */ input) {
38
42
  const { email, password } = input;
39
43
  const hashedPassword = `hashed_${password}`;
40
44
  const userToSave = { email, password: hashedPassword };
41
45
  const cmdSaveUser = () => db.saveUser(userToSave);
42
- const next = (savedUser) => Success(savedUser);
46
+ const next = (/** @type {User} */ savedUser) => Success(savedUser);
43
47
  return Command(cmdSaveUser, next);
44
48
  }
45
49
 
46
- const registerUserFlow = (input) =>
50
+ const registerUserFlow = (/** @type {User} */ input) =>
47
51
  effectPipe(
48
52
  validateRegistration,
49
53
  () => findUserByEmail(input.email),
@@ -51,7 +55,7 @@ const registerUserFlow = (input) =>
51
55
  () => saveUser(input)
52
56
  )(input);
53
57
 
54
- async function registerUser(input) {
58
+ async function registerUser(/** @type {User} */ input) {
55
59
  return await runEffect(registerUserFlow(input));
56
60
  }
57
61