npm package: @msitifa/mlib
Description: Production-ready factory library for building modular, scalable Redux features with Redux-Saga
Author: Lachqar Hamza
Latest version: 1.0.0

# makeFeature Factory Library

> **Production-ready factory system for building modular, scalable Redux features with Redux-Saga**

[![npm version](https://img.shields.io/npm/v/@msitifa/mlib.svg)](https://www.npmjs.com/package/@msitifa/mlib)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![CI](https://github.com/msitifa/mlib/workflows/CI/badge.svg)](https://github.com/msitifa/mlib/actions)
[![Node.js Version](https://img.shields.io/node/v/@msitifa/mlib.svg)](https://nodejs.org)
[![Downloads](https://img.shields.io/npm/dm/@msitifa/mlib.svg)](https://www.npmjs.com/package/@msitifa/mlib)
[![GitHub Stars](https://img.shields.io/github/stars/msitifa/mlib.svg)](https://github.com/msitifa/mlib/stargazers)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)

## 🚀 Installation

```bash
# Using npm
npm install @msitifa/mlib redux-saga

# Using yarn
yarn add @msitifa/mlib redux-saga

# Using pnpm
pnpm add @msitifa/mlib redux-saga
```

**Quick Start:**

```javascript
import makeFeature from '@msitifa/mlib';

const myFeature = makeFeature({
  config: {
    events: {
      internal: ['increment', 'decrement'],
      external: [],
    },
    reducers: ({ types }) => [
      {
        eventType: types.increment,
        handler: (draft) => {
          draft.count += 1;
        },
      },
    ],
  },
  constants: { featureName: 'counter' },
  initialState: { count: 0 },
});
```

For detailed usage instructions, see **[NPM_USAGE.md](./NPM_USAGE.md)**.

## 📚 Documentation

- **[README.md](./README.md)** (this file) - Complete API reference and usage guide
- **[NPM_USAGE.md](./NPM_USAGE.md)** - NPM installation and usage guide
- **[TESTING.md](./TESTING.md)** - Testing strategy, patterns, and examples
- **[MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md)** - Upgrading from old API to new dependency injection
- **[makeSelectors/README.md](./makeSelectors/README.md)** - Cross-feature selector patterns
- **[**tests**/README.md](./**tests**/README.md)** - Integration test documentation
- **[HUSKY_GUIDE.md](./HUSKY_GUIDE.md)** - Git hooks configuration and usage

## 📚 Table of Contents

- [Installation](#-installation)
- [Overview](#overview)
- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Feature Structure](#feature-structure)
- [Advanced Usage](#advanced-usage)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
- [Examples](#examples)

---

## Overview

The `makeFeature` factory library provides a standardized, declarative approach to building Redux features with integrated side effects (Redux-Saga). It eliminates boilerplate and enforces consistent patterns across your application.

### Key Benefits

- **🎯 Zero Boilerplate**: Define features declaratively with configuration objects
- **🔒 Type Safety**: Automatic event type generation and validation
- **📦 Modular**: Each feature is self-contained and portable
- **🧪 Testable**: Pure functions and dependency injection throughout
- **⚡ Performance**: Built-in memoization and optimized state updates
- **🔗 Composable**: Features can listen to and interact with other features
- **🛡️ Error Resilient**: Comprehensive validation with helpful error messages

### What You Get

Each feature created with `makeFeature` includes:

- **Events**: Type-safe action creators
- **Reducer**: Immutable state updates via Immer
- **Selectors**: Memoized state selectors with cross-feature support
- **Middlewares**: Side effect handlers (business logic)
- **Listeners**: Cross-feature event handlers
- **Types**: Namespaced event type constants

---

## Architecture

```
┌─────────────────────────────────────────────────────────────┐
│                      makeFeature                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                  Configuration                         │  │
│  │  • events (internal/external)                         │  │
│  │  • reducers                                           │  │
│  │  • middlewares                                        │  │
│  │  • listeners                                          │  │
│  │  • selectors                                          │  │
│  └───────────────────────────────────────────────────────┘  │
│                            ▼                                 │
│  ┌──────────┬──────────┬──────────┬──────────┬──────────┐  │
│  │  Types   │  Events  │ Reducer  │Selectors │   Saga   │  │
│  └──────────┴──────────┴──────────┴──────────┴──────────┘  │
│                            ▼                                 │
│  ┌───────────────────────────────────────────────────────┐  │
│  │           Sealed Feature Module                       │  │
│  │  • types: { internal, external }                      │  │
│  │  • events: { eventName: Function }                    │  │
│  │  • reducer: Function                                  │  │
│  │  • selectors: { selectorName: Function }             │  │
│  │  • middlewares: GeneratorFunction                     │  │
│  │  • constants: { featureName, ... }                    │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
```

---

## Quick Start

### 1. Define Your Feature Structure

```javascript
// features/cart/constants/index.js
export default {
  featureName: 'cart',
  key: '@cart',
};

// features/cart/initialState/index.js
export default {
  items: [],
  total: 0,
  isLoading: false,
};
```

### 2. Configure Events

Events are divided into two categories:

- **Internal Events** (REQUIRED): Events owned and handled by this feature
- **External Events** (OPTIONAL): Events that other features will listen to
  - Only needed if this feature dispatches events that other features will handle
  - Can be omitted if the feature is self-contained

**Business Rule**: If your feature needs to listen to events from other features, you must:

1. Configure `listeners` in the feature
2. Provide `externalTypes` when creating the feature
3. Optionally define `external` events if your feature also dispatches cross-feature events

```javascript
// features/cart/internalEvents/config.js
// REQUIRED - Always define internal events
export default [
  'setup',
  { name: 'itemAdded', eventParams: ['item'] },
  { name: 'itemRemoved', eventParams: ['itemId'] },
  'checkoutStarted',
  'checkoutCompleted',
  'checkoutFailed',
];

// features/cart/externalEvents/config.js
// OPTIONAL - Only if other features need to listen to cart events
export default [
  'cartCheckoutCompleted', // Other features can listen to this
  'cartItemCountChanged',  // Other features can listen to this
];

// If your cart feature doesn't dispatch events to other features,
// you can omit this file or export an empty array:
export default [];
```

### 3. Configure Reducers

```javascript
// features/cart/reducer/config.js
export default function makeCartReducersConfig({ types }) {
  return [
    {
      eventType: types.itemAdded,
      handler: (draft, { payload }) => {
        draft.items.push(payload.item);
        draft.total += payload.item.price;
      },
    },
    {
      eventType: types.itemRemoved,
      handler: (draft, { payload }) => {
        const index = draft.items.findIndex((i) => i.id === payload.itemId);
        if (index !== -1) {
          draft.total -= draft.items[index].price;
          draft.items.splice(index, 1);
        }
      },
    },
    {
      eventType: types.checkoutStarted,
      handler: (draft) => {
        draft.isLoading = true;
      },
    },
    {
      eventType: types.checkoutCompleted,
      handler: (draft) => {
        draft.items = [];
        draft.total = 0;
        draft.isLoading = false;
      },
    },
  ];
}
```

### 4. Configure Middlewares (Side Effects)

```javascript
// features/cart/middlewares/config.js
import setup from './setup';
import checkout from './checkout';

export default function makeMiddlewaresConfig({ types }) {
  return [
    {
      name: 'setup',
      on: types.setup,
      worker: setup,
    },
    {
      name: 'checkout',
      on: types.checkoutStarted,
      worker: checkout,
    },
  ];
}

// features/cart/middlewares/checkout/makeWorker.js
import { call, put } from 'redux-saga/effects';

export default function factory({ events, selectors, api }) {
  return function* worker() {
    try {
      const items = yield select(selectors.items);
      const response = yield call(api.processCheckout, { items });

      if (response.success) {
        yield put(events.checkoutCompleted());
      } else {
        yield put(events.checkoutFailed());
      }
    } catch (error) {
      yield put(events.checkoutFailed());
    }
  };
}
```

### 5. Configure Listeners (Cross-Feature Events)

**OPTIONAL** - Only needed if your feature listens to events from other features.

**Business Rule**: When configuring listeners:

1. You MUST provide `externalTypes` when creating the feature
2. External types map the event names to their actual type strings from other features
3. If you don't have listeners, you can omit this file entirely

```javascript
// features/cart/listeners/config.js
import handleUserLogout from './handleUserLogout';

export default function makeListenersConfig({ types }) {
  return [
    {
      name: 'handleUserLogout',
      on: types.userLoggedOut, // From externalTypes
      worker: handleUserLogout,
    },
  ];
}

// features/cart/listeners/handleUserLogout/makeWorker.js
import { put } from 'redux-saga/effects';

export default function factory({ events }) {
  return function* worker() {
    // Clear cart when user logs out
    yield put(events.checkoutCompleted());
  };
}
```

**For features without cross-feature communication**, you can either:

- Omit the listeners config file
- Export an empty function: `export default () => [];`

### 6. Create the Feature

```javascript
// features/cart/makeConfig.js
export default function makeCartConfig({
  selectorsConfig,
  externalEventsConfig,
  internalEventsConfig,
  listenersConfig,
  middlewaresConfig,
  makeCartReducersConfig,
}) {
  return {
    events: {
      internal: internalEventsConfig, // REQUIRED
      external: externalEventsConfig, // OPTIONAL - only if dispatching to other features
    },
    middlewares: ({ types }) =>
      typeof middlewaresConfig === 'function'
        ? middlewaresConfig({ types })
        : middlewaresConfig,
    listeners: ({ types }) =>
      typeof listenersConfig === 'function'
        ? listenersConfig({ types })
        : listenersConfig,
    selectors: selectorsConfig,
    reducers: ({ types }) => makeCartReducersConfig({ types }),
  };
}

// features/cart/index.js
import makeFeature from '@msitifa/mlib';
import constants from './constants';
import initialState from './initialState';
import makeConfig from './makeConfig';
import internalEventsConfig from './internalEvents/config';
import externalEventsConfig from './externalEvents/config'; // Can be empty array
import middlewaresConfig from './middlewares/config';
import listenersConfig from './listeners/config'; // Can be omitted if no listeners
import makeCartReducersConfig from './reducer/config';
import selectorsConfig from './selectors/config';

const config = makeConfig({
  selectorsConfig,
  externalEventsConfig,
  internalEventsConfig,
  listenersConfig,
  middlewaresConfig,
  makeCartReducersConfig,
});

// Example 1: Feature WITH listeners (needs externalTypes)
const cart = makeFeature({
  config,
  constants,
  initialState,
  externalTypes: {
    userLoggedOut: '[auth] ::: user-logged-out', // REQUIRED when listeners exist
    paymentProcessed: '[payment] ::: payment-processed',
  },
  inject: () => ({
    api: cartAPI,
    logger: appLogger,
  }),
});

// Example 2: Self-contained feature WITHOUT listeners (no externalTypes needed)
const simpleCart = makeFeature({
  config, // config.listeners would be empty or undefined
  constants,
  initialState,
  // externalTypes omitted - not needed without listeners
  inject: () => ({
    api: cartAPI,
  }),
});

export default cart;
```

### 7. Integrate with Redux

```javascript
// store/rootReducer.js
import { combineReducers } from 'redux';
import cart from '../features/cart';
import auth from '../features/auth';

export default combineReducers({
  [cart.constants.featureName]: cart.reducer,
  [auth.constants.featureName]: auth.reducer,
});

// store/rootSaga.js
import { all } from 'redux-saga/effects';
import cart from '../features/cart';
import auth from '../features/auth';

export default function* rootSaga() {
  yield all([
    cart.middlewares(),
    auth.middlewares(),
  ]);
}
```

### 8. Use in Components

```javascript
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import cart from './features/cart';

function CartComponent() {
  const dispatch = useDispatch();
  const items = useSelector(cart.selectors.items);
  const total = useSelector(cart.selectors.total);

  const handleAddItem = (item) => {
    dispatch(cart.events.itemAdded({ item }));
  };

  const handleCheckout = () => {
    dispatch(cart.events.checkoutStarted());
  };

  return (
    <div>
      <h2>Cart ({items.length} items)</h2>
      <p>Total: ${total}</p>
      <button onClick={handleCheckout}>Checkout</button>
    </div>
  );
}
```

---

## Core Concepts

### Business Rules for Cross-Feature Communication

Understanding when to use external events, listeners, and external types:

#### Rule 1: Internal Events (Always Required)

- Every feature MUST have internal events
- These are events owned and handled by the feature itself
- Used for state changes and internal business logic

#### Rule 2: External Events (Optional)

- Only needed if **OTHER features will listen to this feature's events**
- These are events that this feature dispatches for cross-feature communication
- If your feature is self-contained, you can omit external events or use an empty array

**Example scenarios:**

- ✅ Cart feature dispatches `cartCheckoutCompleted` that notifications feature listens to → Define external event
- ❌ Cart feature only manages its own state → No external events needed

#### Rule 3: Listeners (Optional)

- Only needed if **THIS feature listens to events from OTHER features**
- Requires `externalTypes` to be provided when creating the feature
- If you configure listeners without externalTypes, makeFeature will throw an error

**Example scenarios:**

- ✅ Cart feature clears itself when auth feature dispatches `userLoggedOut` → Need listeners + externalTypes
- ❌ Cart feature works independently → No listeners needed

#### Rule 4: External Types (Conditional)

- REQUIRED if listeners are configured
- OPTIONAL if no listeners exist
- Maps external event names to their actual type strings from other features

**Summary Matrix:**

| Feature Behavior             | Internal Events | External Events | Listeners     | External Types |
| ---------------------------- | --------------- | --------------- | ------------- | -------------- |
| Self-contained feature       | ✅ Required     | ❌ Not needed   | ❌ Not needed | ❌ Not needed  |
| Dispatches to other features | ✅ Required     | ✅ Required     | ❌ Not needed | ❌ Not needed  |
| Listens to other features    | ✅ Required     | ❌ Not needed   | ✅ Required   | ✅ Required    |
| Bidirectional communication  | ✅ Required     | ✅ Required     | ✅ Required   | ✅ Required    |

---

### 1. Events

Events are the actions that trigger state changes or side effects. There are two types:

- **Internal Events**: Events owned by this feature
- **External Events**: Events from other features that this feature listens to

```javascript
// Event configuration formats:
const eventsConfig = [
  // 1. Simple event (no parameters) - payload will be empty {}
  'setup',

  // 2. Event with parameters - payload will be { item: ..., quantity: ... }
  { name: 'itemAdded', eventParams: ['item', 'quantity'] },

  // 3. Event with parameter parser - transforms parameters before creating payload
  {
    name: 'itemUpdated',
    eventParams: ['itemId', 'changes'],
    paramsParser: (params) => ({
      ...params,
      timestamp: Date.now(),
    }),
  },
];
```

**Generated event creators:**

```javascript
// Usage:
dispatch(events.setup());
// => { type: '[cart] ::: setup', payload: {} }

dispatch(events.itemAdded({ item: {...}, quantity: 2 }));
// => { type: '[cart] ::: item-added', payload: { item: {...}, quantity: 2 } }
```

### 2. Reducers

Reducers handle state updates using Immer for immutability:

```javascript
{
  eventType: types.itemAdded,
  handler: (draft, action) => {
    // Mutate draft directly - Immer handles immutability
    draft.items.push(action.payload.item);
    draft.total += action.payload.item.price;
  }
}
```

### 3. Selectors

Selectors extract and derive state with automatic memoization:

```javascript
// Basic selectors (auto-generated from initialState)
selectors.items(state); // => state.cart.items
selectors.total(state); // => state.cart.total
selectors.all(state); // => state.cart

// Advanced selectors (from config)
const selectorsConfig = [
  {
    name: 'itemCount',
    dependencies: ['items'],
    selector: (state, items) => items.length,
    params: [],
  },
  {
    name: 'itemById',
    dependencies: ['items'],
    selector: (state, items, itemId) => items.find((i) => i.id === itemId),
    params: ['itemId'],
  },
];

// Usage:
const count = useSelector(cart.selectors.itemCount);
const item = useSelector((state) => cart.selectors.itemById(state, 'item-123'));
```

### 4. Middlewares

Middlewares handle side effects (API calls, async operations):

```javascript
{
  name: 'checkout',
  on: types.checkoutStarted, // Can be string or array of strings
  worker: checkoutWorker,    // Factory function that returns saga worker
}
```

**Worker structure:**

```javascript
// makeWorker.js
export default function factory({ events, selectors, api, constants }) {
  // Factory receives dependencies via dependency injection
  return function* worker(action) {
    // Worker is a saga generator
    const items = yield select(selectors.items);
    const result = yield call(api.checkout, items);

    if (result.success) {
      yield put(events.checkoutCompleted());
    }
  };
}
```

### 5. Listeners

Listeners respond to events from other features:

```javascript
{
  name: 'handleUserLogout',
  types: externalTypes.userLoggedOut, // External event type
  worker: handleUserLogoutWorker,
}
```

### 6. Dependency Injection

The library supports two ways to inject dependencies into workers (middlewares and listeners):

#### Option 1: Static Dependencies

Use `dependencies` object for static, pre-configured services:

```javascript
const feature = makeFeature({
  config,
  constants,
  initialState,
  dependencies: {
    api: cartAPI,
    persistor: storage,
    logger: appLogger,
    config: appConfig,
  },
});
```

#### Option 2: Computed Dependencies

Use `inject` function for dependencies that need to be computed at feature creation time:

```javascript
const feature = makeFeature({
  config,
  constants,
  initialState,
  inject: () => ({
    // Computed at feature creation time
    prices: Object.fromEntries(
      Object.entries(configManager.prices).map(([k, v]) => [k, v.coins])
    ),
    timestamp: Date.now(),
    apiWithAuth: createAPIWithToken(authService.getToken()),
  }),
});
```

#### Option 3: Combined (with Priority)

When both are provided, `inject()` **overrides** `dependencies` for same keys:

```javascript
const feature = makeFeature({
  config,
  constants,
  initialState,
  dependencies: {
    api: defaultAPI,
    timeout: 5000,
    logger: console,
  },
  inject: () => ({
    timeout: 10000, // ← Overrides dependencies.timeout
    timestamp: Date.now(), // ← New dependency
  }),
});

// Workers receive:
// {
//   api: defaultAPI,      ← from dependencies
//   timeout: 10000,       ← from inject (overrides)
//   logger: console,      ← from dependencies
//   timestamp: <value>    ← from inject (new)
// }
```

**Dependency Priority**: `inject()` > `dependencies`

#### Per-Worker Dependencies

Workers can have their own specific dependencies that override global ones:

```javascript
{
  name: 'checkout',
  on: types.checkoutStarted,
  worker: checkoutWorker,
  dependencies: {
    // Worker-specific overrides
    timeout: 30000,  // Overrides global timeout for this worker only
    specialService: checkoutService,  // Worker-specific service
  },
}
```

**Full Priority Chain**: Per-worker `dependencies` > global `inject()` > global `dependencies`

---

## API Reference

### makeFeature(params)

Creates a complete feature module.

**Parameters:**

| Parameter                | Type            | Required | Description                              |
| ------------------------ | --------------- | -------- | ---------------------------------------- |
| `config`                 | Object          | Yes      | Feature configuration object             |
| `config.events`          | Object          | Yes      | Event configurations (internal/external) |
| `config.events.internal` | Array           | Yes      | Internal event definitions               |
| `config.events.external` | Array           | Yes      | External event definitions               |
| `config.reducers`        | Function        | Yes      | Function returning reducer config        |
| `config.middlewares`     | Function\|Array | No       | Middleware configurations                |
| `config.listeners`       | Function\|Array | No       | Listener configurations                  |
| `config.selectors`       | Array           | No       | Advanced selector configurations         |
| `constants`              | Object          | Yes      | Feature constants                        |
| `constants.featureName`  | string          | Yes      | Unique feature name                      |
| `initialState`           | Object          | Yes      | Initial Redux state                      |
| `externalTypes`          | Object          | No       | External event types from other features |
| `dependencies`           | Object          | No       | Static shared dependencies for workers   |
| `inject`                 | Function        | No       | Function returning computed dependencies |

**Returns:**

```javascript
{
  types: {
    internal: { eventName: 'string', ... },
    external: { eventName: 'string', ... }
  },
  events: {
    eventName: Function, // Action creator
    ...
  },
  reducer: Function,           // Redux reducer
  selectors: {
    propertyName: Function,   // Selector
    ...
  },
  constants: Object,           // Feature constants
  middlewares: GeneratorFunction // Combined saga
}
```

### Event Configuration

```javascript
// String format (no parameters)
'eventName'

// Object format (with parameters)
{
  name: 'eventName',
  eventParams: ['param1', 'param2'],
  paramsParser: (params) => ({ ...params, extra: 'value' }) // Optional
}
```

### Reducer Configuration

```javascript
{
  eventType: string,           // Event type constant
  handler: (draft, action) => void  // Immer handler function
}

// OR with named function (legacy)
{
  eventType: string,
  functionName: 'functionName'
}
```

### Middleware Configuration

```javascript
{
  name: string,                    // Middleware name
  types: string | string[],        // Event type(s) to watch
  worker: Function,                // Worker factory
  dependencies: Object,            // Optional per-middleware dependencies (overrides global)
  makeWatcher: Function            // Optional custom watcher (defaults to takeLatest)
}
```

### Selector Configuration

```javascript
{
  name: string,                    // Selector name
  dependencies: Array,             // Array of dependency selectors
  selector: Function,              // Selector function
  params: string[]                 // Parameter names
}

// Dependency formats:
// 1. Local selector (string)
'selectorName'

// 2. Cross-feature selector (object)
{
  feature: 'featureName',
  selector: 'selectorName',
  params: ['param1']
}
```

---

## Feature Structure

Standard directory structure for a feature:

```
features/myFeature/
├── index.js                    # Feature export
├── makeFeature.js              # Feature factory wrapper (optional)
├── makeConfig.js               # Config builder
├── constants/
│   └── index.js                # Feature constants
├── initialState/
│   └── index.js                # Initial state
├── internalEvents/
│   └── config.js               # Internal event definitions
├── externalEvents/
│   └── config.js               # External event definitions
├── reducer/
│   └── config.js               # Reducer configurations
├── selectors/
│   └── config.js               # Selector configurations
├── middlewares/
│   ├── config.js               # Middleware list
│   ├── setup/
│   │   ├── index.js
│   │   └── makeWorker.js       # Saga worker
│   └── process/
│       ├── index.js
│       └── makeWorker.js
└── listeners/
    ├── config.js               # Listener list
    └── handleExternalEvent/
        ├── index.js
        └── makeWorker.js
```

---

## Advanced Usage

### Cross-Feature Selectors

```javascript
// selectors/config.js
export default [
  {
    name: 'userCartValue',
    dependencies: [
      'total', // Local selector
      { feature: 'auth', selector: 'currentUser', params: [] }, // Cross-feature
    ],
    selector: (state, total, currentUser) => {
      return currentUser?.isPremmium ? total * 0.9 : total;
    },
    params: [],
  },
];

// Feature setup
const feature = makeFeature({
  config,
  constants,
  initialState,
  getFeatureSelectors: (featureName) => {
    if (featureName === 'auth') return authFeature.selectors;
    return {};
  },
});
```

### Dynamic Event Parameters

```javascript
const eventsConfig = [
  {
    name: 'itemProcessed',
    eventParams: ['itemId', 'status'],
    paramsParser: (params) => ({
      ...params,
      timestamp: Date.now(),
      version: '1.0',
    }),
  },
];

// Generated:
dispatch(events.itemProcessed({ itemId: '123', status: 'complete' }));
// Payload: { itemId: '123', status: 'complete', timestamp: 1234567890, version: '1.0' }
```

### Complex Saga Workers

```javascript
// makeWorker.js
import { call, put, select, race, delay, take } from 'redux-saga/effects';

export default function factory({ events, selectors, api, logger, constants }) {
  return function* worker(action) {
    try {
      logger.info('Starting process', { action });

      // Get current state
      const currentState = yield select(selectors.all);

      // Race between API call and timeout
      const { response, timeout } = yield race({
        response: call(api.process, currentState),
        timeout: delay(5000),
      });

      if (timeout) {
        yield put(events.processTimeout());
        return;
      }

      if (response.success) {
        yield put(events.processCompleted({ result: response.data }));
      } else {
        yield put(events.processFailed({ error: response.error }));
      }
    } catch (error) {
      logger.error('Process error', error);
      yield put(events.processFailed({ error: error.message }));
    }
  };
}
```

### Multiple Feature Instances

Create multiple instances of the same feature (e.g., per reasoning type):

```javascript
const reasoningTypes = ['inductive', 'deductive', 'memory'];

const features = reasoningTypes.map((type) =>
  makeFeature({
    config,
    constants: {
      featureName: `${baseFeatureName}_${type}`,
      key: `@${baseFeatureName}:${type}`,
      type,
    },
    initialState,
    externalTypes,
    inject: () => ({ type }),
  })
);

// Export individually
export const inductiveFeature = features[0];
export const deductiveFeature = features[1];
export const memoryFeature = features[2];
```

---

## Best Practices

### 1. Feature Naming

- Use descriptive, domain-specific names
- Keep feature names lowercase and singular
- Use consistent naming across all files

```javascript
// ✅ Good
constants: {
  featureName: 'cart';
}
constants: {
  featureName: 'userProfile';
}

// ❌ Avoid
constants: {
  featureName: 'Cart';
}
constants: {
  featureName: 'user-profile';
}
```

### 2. Event Naming

- Use past tense for completed actions
- Use imperative for commands
- Be specific and descriptive

```javascript
// ✅ Good
('itemAdded', 'checkoutStarted', 'checkoutCompleted', 'setup', 'process');

// ❌ Avoid
('add', 'checkout', 'done', 'thing');
```

### 3. State Structure

- Keep state flat when possible
- Use normalized data for collections
- Avoid nested objects more than 2-3 levels deep

```javascript
// ✅ Good
{
  items: [1, 2, 3],
  itemsById: { 1: {...}, 2: {...}, 3: {...} },
  isLoading: false
}

// ❌ Avoid
{
  data: {
    items: {
      list: [...],
      metadata: { ... }
    }
  }
}
```

### 4. Selector Organization

- Start with basic selectors (auto-generated)
- Add derived selectors only when needed
- Keep selector logic simple and focused

```javascript
// Use config only for complex derived state
export default [
  {
    name: 'expensiveItems',
    dependencies: ['items'],
    selector: (state, items) => items.filter((i) => i.price > 100),
  },
];
```

### 5. Middleware Organization

- One middleware per business operation
- Keep workers focused and small
- Extract complex logic into separate functions

```javascript
// ✅ Good: Focused worker
function* checkoutWorker() {
  const items = yield select(selectors.items);
  const result = yield call(processCheckout, items);
  yield put(events.checkoutCompleted());
}

// ❌ Avoid: Doing too much
function* megaWorker() {
  // 100 lines of complex logic
}
```

### 6. Error Handling

- Always wrap saga workers in try-catch
- Dispatch failure events
- Log errors appropriately

```javascript
export default function factory({ events, logger }) {
  return function* worker(action) {
    try {
      // ... work
      yield put(events.success());
    } catch (error) {
      logger.error('Worker failed', error);
      yield put(events.failed({ error: error.message }));
    }
  };
}
```

### 7. Testing

- Test each factory independently
- Use mock dependencies
- Test happy path and error cases

```javascript
describe('myFeature', () => {
  const mockAPI = { process: jest.fn() };

  const feature = makeFeature({
    config,
    constants: { featureName: 'test' },
    initialState: { value: 0 },
    inject: () => ({ api: mockAPI }),
  });

  it('updates state on success', () => {
    const action = feature.events.itemAdded({ item: {...} });
    const newState = feature.reducer(undefined, action);
    expect(newState.items).toHaveLength(1);
  });
});
```

---

## Troubleshooting

### Common Errors

#### 1. "makeFeature: config.events must have internal and external properties"

**Cause**: Missing or incorrectly structured events configuration.

**Solution**:

```javascript
// ✅ Correct
config: {
  events: {
    internal: ['event1', 'event2'],
    external: ['externalEvent']
  },
  // ...
}

// ❌ Wrong
config: {
  events: ['event1', 'event2'], // Missing internal/external split
}
```

#### 2. "makeMiddlewares: eventsConfig must be an array"

**Cause**: Middleware config is not an array or function returning an array.

**Solution**:

```javascript
// ✅ Correct
middlewares: ({ types }) => [
  { name: 'setup', on: types.setup, worker: setupWorker },
];

// ❌ Wrong
middlewares: {
  setup: setupWorker;
}
```

#### 3. "Selector dependency 'xyz' for 'abc' is not defined"

**Cause**: Referenced a selector that doesn't exist.

**Solution**:

```javascript
// Ensure the dependency exists
{
  name: 'derived',
  dependencies: ['items'], // 'items' must exist as a selector
  selector: (state, items) => items.length
}
```

#### 4. Workers not receiving dependencies

**Cause**: Dependencies not provided correctly.

**Solution**:

```javascript
// ✅ Correct: Use inject function
const feature = makeFeature({
  config,
  constants,
  initialState,
  inject: () => ({ api, logger }), // Function that returns object
});

// ❌ Wrong: Passing object directly
inject: () => ({
  api, logger;
} // Should be a function
```

### Debugging Tips

1. **Check feature creation**: Ensure feature is created without errors

   ```javascript
   console.log('Feature:', feature);
   console.log('Types:', feature.types);
   console.log('Events:', Object.keys(feature.events));
   ```

2. **Verify event types**: Check generated type strings

   ```javascript
   console.log(feature.types.internal.itemAdded);
   // Should be: "[featureName] ::: item-added"
   ```

3. **Test selectors**: Verify selectors work with mock state

   ```javascript
   const mockState = { cart: { items: [] } };
   console.log(cart.selectors.items(mockState)); // Should return []
   ```

4. **Debug middlewares**: Add logging in workers
   ```javascript
   function* worker(action) {
     console.log('Worker triggered:', action.type);
     // ... rest of worker
   }
   ```

---

## Examples

### Example 1: Simple Counter Feature

```javascript
// constants/index.js
export default { featureName: 'counter' };

// initialState/index.js
export default { count: 0 };

// internalEvents/config.js
export default ['increment', 'decrement', 'reset'];

// externalEvents/config.js
export default [];

// reducer/config.js
export default function({ types }) {
  return [
    {
      eventType: types.increment,
      handler: (draft) => { draft.count += 1; }
    },
    {
      eventType: types.decrement,
      handler: (draft) => { draft.count -= 1; }
    },
    {
      eventType: types.reset,
      handler: (draft) => { draft.count = 0; }
    },
  ];
}

// selectors/config.js
export default [];

// middlewares/config.js
export default () => [];

// listeners/config.js
export default () => [];

// index.js
import makeFeature from '@msitifa/mlib';
// ... imports

const counter = makeFeature({ config, constants, initialState });
export default counter;
```

### Example 2: Feature with API Integration

See the [Quick Start](#quick-start) section for a complete cart example with API integration.

### Example 3: Cross-Feature Communication

```javascript
// features/notifications/listeners/config.js
export default function({ types }) {
  return [
    {
      name: 'handleCartCheckout',
      on: types.checkoutCompleted, // From cart feature
      worker: require('./handleCartCheckout').default,
    },
  ];
}

// features/notifications/listeners/handleCartCheckout/makeWorker.js
export default function factory({ events }) {
  return function* worker(action) {
    yield put(events.notificationAdded({
      message: 'Checkout completed successfully!',
      type: 'success',
    }));
  };
}

// Setup external types
const notifications = makeFeature({
  config,
  constants,
  initialState,
  externalTypes: {
    checkoutCompleted: cart.types.internal.checkoutCompleted,
  },
});
```

---

## Migration Guide

### From Manual Redux to makeFeature

**Before:**

```javascript
// actionTypes.js
export const ADD_ITEM = 'cart/ADD_ITEM';

// actions.js
export const addItem = (item) => ({ type: ADD_ITEM, payload: item });

// reducer.js
export default function (state = initialState, action) {
  switch (action.type) {
    case ADD_ITEM:
      return { ...state, items: [...state.items, action.payload] };
    default:
      return state;
  }
}

// sagas.js
function* handleAddItem(action) {
  /* ... */
}
export function* watchCart() {
  yield takeLatest(ADD_ITEM, handleAddItem);
}
```

**After:**

```javascript
// All in one feature module
const cart = makeFeature({
  config: {
    events: {
      internal: [{ name: 'itemAdded', eventParams: ['item'] }],
      external: [],
    },
    reducers: ({ types }) => [
      {
        eventType: types.itemAdded,
        handler: (draft, { payload }) => {
          draft.items.push(payload.item);
        },
      },
    ],
    middlewares: ({ types }) => [
      {
        name: 'addItem',
        on: types.itemAdded,
        worker: require('./middlewares/addItem').default,
      },
    ],
  },
  constants: { featureName: 'cart' },
  initialState: { items: [] },
});
```

---

## Contributing

When contributing new features or improvements to the factory library:

1. **Maintain backward compatibility** whenever possible
2. **Add tests** for new functionality
3. **Update documentation** with examples
4. **Follow the established patterns** for consistency
5. **Add JSDoc comments** to all public APIs

---

## License

Internal project library. See project root for license information.

---

## Support

For questions or issues:

- Check the [Troubleshooting](#troubleshooting) section
- Review existing feature implementations (balance, hearts)
- Contact the development team

---

**Version**: 1.0.0  
**Last Updated**: December 2025  
**Maintainers**: Development Team