# Feature Flag Client

A universal **feature flag client** that works for both **Node.js** and **React** applications. This package allows applications to fetch feature flags from a remote API and determine if a feature is enabled or disabled.

## Features
- ✅ Works in both **Node.js (CommonJS & ESM)** and **React (browser environment)**
- ✅ Fetches feature flags from a configurable API
- ✅ Uses the optimized `/flags/active` endpoint with sub-10ms response time
- ✅ Supports **automatic polling** every minute (for Node.js)
- ✅ **NEW:** Supports **configurable polling** for browser environments
- ✅ Supports **manual fetching** (for React)
- ✅ Uses **Axios** (peer dependency) for HTTP requests

---

## Installation

### **Step 1: Install the package**
```sh
# Using Yarn
yarn add eehitus-feature-flag-client axios

# Using npm
npm install eehitus-feature-flag-client axios
```
> **Note:** `axios` is a **peer dependency**, so it must be installed separately.

---

## Usage

### **Node.js (ES Modules)**
```javascript
import { FeatureFlagClient } from "eehitus-feature-flag-client";

const client = new FeatureFlagClient(
  "https://your-feature-flag-api.com", // API URL (without /flags/active)
  1234,                               // Application ID
  "live"                              // Environment (dev, test, prelive, live)
);

// Fetch flags manually (optional, useful for immediate use)
await client.fetchOnce();

// Check if a feature is enabled
console.log("New Dashboard Enabled:", client.flagEnabled("new-dashboard"));

// Check feature flags dynamically every minute
setInterval(() => {
  console.log("Dark Mode Enabled:", client.flagEnabled("dark-mode"));
}, 60000);
```

### **Node.js (CommonJS)**
```javascript
const { FeatureFlagClient } = require("eehitus-feature-flag-client");

const client = new FeatureFlagClient(
  "https://your-feature-flag-api.com",
  1234,
  "production"
);

setTimeout(() => {
  console.log("Feature Enabled:", client.flagEnabled("beta-feature"));
}, 70000);
```

### **React (with Hooks)**
```javascript
import { useEffect, useState } from "react";
import { FeatureFlagClient } from "eehitus-feature-flag-client";

// Creating a client with browser polling enabled
const client = new FeatureFlagClient(
  "https://your-feature-flag-api.com",
  1234,
  "dev",
  {
    enableBrowserPolling: true,
    pollingInterval: 2 * 60 * 1000 // Poll every 2 minutes
  }
);

export default function FeatureFlagsExample() {
  const [flagEnabled, setFlagEnabled] = useState(false);

  useEffect(() => {
    async function fetchFlags() {
      // IMPORTANT: Always await fetchOnce before checking flags
      await client.fetchOnce();
      setFlagEnabled(client.flagEnabled("new-dashboard"));
    }

    fetchFlags();
    
    // Clean up when component unmounts
    return () => {
      client.stopFetching();
    };
  }, []);

  return <div>{flagEnabled ? "Feature is ON" : "Feature is OFF"}</div>;
}
```

---

## Advanced React Implementation

For more robust feature flag management in React applications, we recommend using a context-based provider pattern. This approach makes feature flags available throughout your application and handles loading/caching efficiently.

### **Step 1: Create a Feature Flag Provider**

Create a file named `FeatureFlagProvider.tsx`:

```tsx
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { FeatureFlagClient } from 'eehitus-feature-flag-client';

// Create a Feature Flag Client instance with browser polling enabled
const featureFlagClient = new FeatureFlagClient(
  process.env.REACT_APP_FLAG_API_URL || 'http://localhost:3000',
  Number(process.env.REACT_APP_FLAG_APP_ID) || 9,
  process.env.REACT_APP_FLAG_ENVIRONMENT || 'dev',
  {
    enableBrowserPolling: true,
    pollingInterval: 5 * 60 * 1000 // 5 minutes
  }
);

// Define the context type
interface FeatureFlagContextType {
  isLoading: boolean;
  flags: Record<string, boolean>;
  refreshFlags: () => Promise<void>;
  isFlagEnabled: (flagName: string) => boolean;
}

// Create context with default values
const FeatureFlagContext = createContext<FeatureFlagContextType>({
  isLoading: true,
  flags: {},
  refreshFlags: async () => {},
  isFlagEnabled: () => false,
});

// Create a hook for using feature flags
export const useFeatureFlags = () => useContext(FeatureFlagContext);

interface FeatureFlagProviderProps {
  children: ReactNode;
}

export function FeatureFlagProvider({ children }: FeatureFlagProviderProps): JSX.Element {
  const [isLoading, setIsLoading] = useState(true);
  const [flags, setFlags] = useState<Record<string, boolean>>({});
  const [error, setError] = useState<Error | null>(null);

  // Common flags to check
  const commonFlags = [
    'new-dashboard',
    'dark-mode',
    'beta-features',
    // Add any other flags you want to check
  ];

  // Function to load feature flags
  const loadFeatureFlags = async () => {
    try {
      setIsLoading(true);
      
      // Use fetchFresh to ensure a new request is made
      await featureFlagClient.fetchFresh();
      
      // Create a map of all flags for easy access
      const flagsMap: Record<string, boolean> = {};
      
      commonFlags.forEach(flagName => {
        flagsMap[flagName] = featureFlagClient.flagEnabled(flagName);
      });
      
      setFlags(flagsMap);
      setError(null);
    } catch (err) {
      console.error('Failed to load feature flags:', err);
      setError(err instanceof Error ? err : new Error(String(err)));
    } finally {
      setIsLoading(false);
    }
  };

  // Load flags on component mount
  useEffect(() => {
    // Initial load
    loadFeatureFlags();
    
    // Clean up when component unmounts
    // The client handles polling automatically
    return () => {
      featureFlagClient.stopFetching();
    };
  }, []);

  // Function to check if a flag is enabled
  const isFlagEnabled = (flagName: string): boolean => {
    // First check our cached flags
    if (flagName in flags) {
      return flags[flagName];
    }
    
    // If not in our cache, check directly from the client
    return featureFlagClient.flagEnabled(flagName);
  };

  // Function to manually refresh flags
  const refreshFlags = async (): Promise<void> => {
    await loadFeatureFlags();
  };

  return (
    <FeatureFlagContext.Provider
      value={{
        isLoading,
        flags,
        refreshFlags,
        isFlagEnabled,
      }}
    >
      {children}
    </FeatureFlagContext.Provider>
  );
}

// Higher-order component for feature flag conditional rendering
interface FeatureFlaggedProps {
  flagName: string;
  fallback?: React.ReactNode;
  children: React.ReactNode;
}

export function FeatureFlagged({ flagName, fallback = null, children }: FeatureFlaggedProps): JSX.Element {
  const { isFlagEnabled } = useFeatureFlags();
  
  return isFlagEnabled(flagName) ? <>{children}</> : <>{fallback}</>;
}
```

### **Step 2: Wrap Your Application with the Provider**

In your main `index.tsx` or `App.tsx`:

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { FeatureFlagProvider } from './feature-flags/FeatureFlagProvider';

ReactDOM.render(
  <React.StrictMode>
    <FeatureFlagProvider>
      <App />
    </FeatureFlagProvider>
  </React.StrictMode>,
  document.getElementById('root')
);
```

### **Step 3: Use Feature Flags in Components**

Now you can use feature flags throughout your application in three ways:

**Method 1: Using the `useFeatureFlags` hook**

```tsx
import { useFeatureFlags } from './feature-flags/FeatureFlagProvider';

function MyComponent() {
  const { isFlagEnabled, isLoading } = useFeatureFlags();
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  return (
    <div>
      {isFlagEnabled('dark-mode') && (
        <button>Switch to Light Mode</button>
      )}
    </div>
  );
}
```

**Method 2: Using the `FeatureFlagged` component**

```tsx
import { FeatureFlagged } from './feature-flags/FeatureFlagProvider';
import OldDashboard from './components/OldDashboard';
import NewDashboard from './components/NewDashboard';

function DashboardPage() {
  return (
    <FeatureFlagged 
      flagName="new-dashboard" 
      fallback={<OldDashboard />}
    >
      <NewDashboard />
    </FeatureFlagged>
  );
}
```

**Method 3: Refreshing flags manually**

```tsx
import { useFeatureFlags } from './feature-flags/FeatureFlagProvider';

function SettingsPage() {
  const { refreshFlags, flags } = useFeatureFlags();
  
  return (
    <div>
      <h1>Application Settings</h1>
      <button onClick={refreshFlags}>Refresh Feature Flags</button>
      
      <pre>{JSON.stringify(flags, null, 2)}</pre>
    </div>
  );
}
```

---

## API

### `new FeatureFlagClient(url: string, appId: number, environment: string, options?: FeatureFlagClientOptions)`
Creates a new feature flag client instance.

| Parameter     | Type     | Description                        |
|--------------|---------|------------------------------------|
| `url`         | `string` | Base API endpoint (without `/flags/active`) |
| `appId`       | `number` | Application identifier (ID)       |
| `environment` | `string` | Environment (`dev`, `test`, `prelive`, `live`) |
| `options`     | `object` | Optional configuration options (see below) |

#### FeatureFlagClientOptions

| Option                | Type      | Default | Description                               |
|----------------------|-----------|---------|-------------------------------------------|
| `enableBrowserPolling` | `boolean` | `false` | Enable polling in browser environments    |
| `pollingInterval`      | `number`  | `60000` | Polling interval in milliseconds (default: 1 minute) |

### `.fetchOnce(): Promise<Record<string, boolean>>`
Fetches flags if they haven't been loaded before. Returns the current flag states.

### `.fetchFresh(): Promise<Record<string, boolean>>`
Forces a fresh fetch of feature flags from the server, regardless of whether they've been loaded before. Returns the updated flag states.

### `.flagEnabled(flagName: string): boolean`
Returns `true` if the feature flag is enabled, otherwise `false`.

### `.getFlagEnabledAsync(flagName: string): Promise<boolean>`
Asynchronously ensures flags are loaded before checking if a flag is enabled. Useful for initial page load.

### `.ensureFlagsLoaded(): Promise<void>`
Ensures that flags have been loaded at least once before proceeding.

### `.startPolling(interval?: number): void`
Starts periodic polling for flag updates. If an interval is provided, it updates the polling interval.

### `.stopFetching(): void`
Stops automatic flag updates for both Node.js and browser environments.

### `.isPollingActive(): boolean`
Checks if polling is currently active.

### `.setPollingInterval(interval: number): void`
Updates the polling interval. If polling is already active, it restarts with the new interval.

---

## Important React Notes

- **Browser Polling**: The new `enableBrowserPolling` option allows automatic flag refreshing in browser environments.
- **Always await fetchOnce() or fetchFresh()**: When using the client directly in React, always await the fetch methods before checking flags to prevent race conditions.
- **Context API**: Using the context-based provider approach is recommended for most React applications as it manages loading states and provides a centralized way to access flags.
- **Environment Variables**: Configure your application with the appropriate environment variables for your feature flag API.

---

## Cleanup
To stop fetching when shutting down your app:

### Node.js
```javascript
process.on("SIGINT", () => {
  console.log("Stopping feature flag updates...");
  client.stopFetching();
  process.exit();
});
```

### React
```javascript
useEffect(() => {
  // Initialize flags...
  
  return () => {
    client.stopFetching();
  };
}, []);
```

---

## License
MIT License

---

Now, your **Node.js** and **React** applications can seamlessly use feature flags to dynamically enable or disable features! 🚀