npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

merged-config

v1.0.0

Published

A flexible, hierarchical configuration service with remote config support, caching, and runtime overrides

Readme

merged-config

A flexible, hierarchical configuration management service with remote config support, caching, runtime overrides, and React integration.

Features

  • Hierarchical Configuration - Merge configs from multiple sources with clear precedence
  • Remote Config - Fetch and cache configuration from a remote API
  • Runtime Overrides - Modify config values at runtime (user and debug overrides)
  • Development Mode Support - Special debug overrides that persist across app restarts
  • React Integration - Context provider and hooks for seamless React/React Native integration
  • Platform Agnostic - Works with AsyncStorage (React Native), localStorage (Web), or custom storage
  • TypeScript - Full type safety with comprehensive TypeScript definitions
  • Wildcard Defaults - Support for default values with wildcard matching
  • Subscription System - React to config changes anywhere in your app

Installation

npm install merged-config

Peer Dependencies

For React/React Native projects:

npm install react

For React Native with AsyncStorage:

npm install @react-native-async-storage/async-storage

Quick Start

1. Initialize the Config Service

import { ConfigService, AsyncStorageAdapter } from 'merged-config';
import AsyncStorage from '@react-native-async-storage/async-storage';
import localConfig from './local-config.json';
import appSettings from './app-settings.json';
import debugConfig from './debug-config.json'; // optional

// Initialize the singleton
ConfigService.initialize({
  localConfig,
  appSettings: {
    remoteConfigUrl: appSettings.remoteConfigUrl,
    remoteConfigTTL: appSettings.remoteConfigTTL,
  },
  debugConfig,
  storageAdapter: new AsyncStorageAdapter(AsyncStorage),
  isDev: __DEV__, // or process.env.NODE_ENV === 'development'
  logger: (tag, level, message, data) => {
    console.log(`[${tag}] [${level}] ${message}`, data);
  },
});

// Get the instance
const configService = ConfigService.getInstance();

2. Use in React/React Native

import { ConfigProvider, useConfig, useConfigValue } from 'merged-config/react';

// Wrap your app with ConfigProvider
function App() {
  return (
    <ConfigProvider>
      <YourApp />
    </ConfigProvider>
  );
}

// Use config in components
function MyComponent() {
  const { config, meta, status, reload } = useConfig();

  // Or get a specific value
  const apiUrl = useConfigValue('api.weather.url', 'https://default.api.com');

  return (
    <div>
      <p>Config Status: {status}</p>
      <p>Config Source: {meta.source}</p>
      <p>API URL: {apiUrl}</p>
      <button onClick={reload}>Reload Config</button>
    </div>
  );
}

3. Use Without React

import { ConfigService } from 'merged-config';

const configService = ConfigService.getInstance();

// Get config values
const apiUrl = configService.get('api.weather.url', 'https://default.api.com');
const timeout = configService.get('api.weather.timeout', 5000);

// Subscribe to changes
const unsubscribe = configService.subscribe(() => {
  console.log('Config updated!');
  const newUrl = configService.get('api.weather.url');
});

// Cleanup
unsubscribe();

Configuration Files

app-settings.json

Contains settings for remote config loading:

{
  "remoteConfigUrl": "https://api.example.com/config",
  "remoteConfigTTL": 3600000,
  "remoteConfigRetryDelay": 30000
}

local-config.json

Your base configuration with fallback values:

{
  "app": {
    "name": "Weather App",
    "version": "1.0.0",
    "defaults": {
      "api": {
        "*": {
          "units": "e",
          "language": "en-US",
          "format": "json",
          "timeout": 5000
        }
      }
    }
  },
  "api": {
    "weather": {
      "url": "https://api.weather.com",
      "timeout": 10000
    }
  }
}

debug-config.json (DEV mode only)

Overrides for development/testing:

{
  "api": {
    "weather": {
      "url": "https://staging.api.weather.com"
    }
  },
  "debug": {
    "enableLogging": true,
    "bypassCache": false
  }
}

Config Merge Order (Lowest to Highest Priority)

  1. Local Config (local-config.json) - Base configuration
  2. Remote Config - Fetched from API, cached locally
  3. Debug Config (debug-config.json) - DEV mode only
  4. User Overrides - Runtime-only, set via updateConfigByPath() or mergeConfig()
  5. Debug Overrides - DEV mode only, set via setDebugOverride(), persisted in cache

API Reference

ConfigService Methods

get(path: string, defaultValue?: any): any

Get a config value by dot-separated path.

const url = configService.get('api.weather.url');
const timeout = configService.get('api.weather.timeout', 5000);
const allConfig = configService.get(''); // Get entire config with _meta

updateConfigByPath(path: string, value: any): Promise<void>

Update a config value at runtime (not persisted).

await configService.updateConfigByPath('api.weather.timeout', 15000);

mergeConfig(configObject: any): Promise<void>

Merge an object into the config at runtime (not persisted).

await configService.mergeConfig({
  api: {
    weather: {
      timeout: 15000,
      retries: 3
    }
  }
});

getUserOverrides(): any

Get current runtime user overrides.

const overrides = configService.getUserOverrides();

clearUserOverrides(): Promise<void>

Clear all user overrides.

await configService.clearUserOverrides();

subscribe(fn: () => void): () => void

Subscribe to config changes. Returns an unsubscribe function.

const unsubscribe = configService.subscribe(() => {
  console.log('Config changed!');
});

// Later...
unsubscribe();

reload(background?: boolean): Promise<void>

Reload config from remote source.

await configService.reload(); // Throws on error
await configService.reload(true); // Silent background reload

isReady(): boolean

Check if config is ready (initialized).

if (configService.isReady()) {
  // Config is ready
}

waitForReady(): Promise<void>

Wait for config to be ready.

await configService.waitForReady();
// Config is now ready

DEV Mode Methods

These methods only work when isDev is true:

setDebugOverride(path: string, value: any): void

Set a debug override (persisted across app restarts in DEV mode).

configService.setDebugOverride('api.weather.url', 'https://localhost:3000');

clearDebugOverrides(): void

Clear all debug overrides.

configService.clearDebugOverrides();

getDebugOverrides(): any

Get current debug overrides.

const overrides = configService.getDebugOverrides();

toggleCacheBypass(): boolean

Toggle cache bypass for debugging. Returns new state.

const enabled = configService.toggleCacheBypass();

React Hooks

useConfig()

Access the full config context.

const { config, meta, status, reload } = useConfig();

Returns:

  • config - The full config object with _meta
  • meta - Config metadata (source, timestamp, etc.)
  • status - Loading state: 'loading' | 'ready' | 'error'
  • reload - Function to reload config

useConfigValue<T>(path: string, defaultValue?: T): T

Get a specific config value that updates on changes.

const apiUrl = useConfigValue<string>('api.weather.url', 'https://default.com');
const timeout = useConfigValue<number>('api.weather.timeout', 5000);

Storage Adapters

AsyncStorageAdapter (React Native)

import { AsyncStorageAdapter } from 'merged-config';
import AsyncStorage from '@react-native-async-storage/async-storage';

const adapter = new AsyncStorageAdapter(AsyncStorage);

LocalStorageAdapter (Web)

import { LocalStorageAdapter } from 'merged-config';

const adapter = new LocalStorageAdapter();

MemoryStorageAdapter (Testing/No Persistence)

import { MemoryStorageAdapter } from 'merged-config';

const adapter = new MemoryStorageAdapter();

Custom Storage Adapter

Implement the StorageAdapter interface:

import type { StorageAdapter } from 'merged-config';

class MyStorageAdapter implements StorageAdapter {
  async getItem(key: string): Promise<string | null> {
    // Your implementation
  }

  async setItem(key: string, value: string): Promise<void> {
    // Your implementation
  }

  async removeItem(key: string): Promise<void> {
    // Your implementation
  }
}

Wildcard Defaults

The config service supports wildcard defaults using * in the app.defaults section:

{
  "app": {
    "defaults": {
      "api": {
        "*": {
          "units": "e",
          "language": "en-US"
        }
      }
    }
  }
}

Now any API config can fall back to these defaults:

// Even if api.weather.units doesn't exist, it returns "e"
const units = configService.get('api.weather.units', 'imperial'); // Returns "e"
const lang = configService.get('api.forecast.language'); // Returns "en-US"

Utility Functions

getConfigValue(config: any, path: string, defaultValue?: any): any

Standalone function to get a value from a config object:

import { getConfigValue } from 'merged-config';

const config = { api: { url: 'https://example.com' } };
const url = getConfigValue(config, 'api.url', 'default');

deepMerge(target: any, source: any): any

Deep merge two objects (arrays are replaced, not merged):

import { deepMerge } from 'merged-config';

const merged = deepMerge(
  { a: 1, b: { c: 2 } },
  { b: { d: 3 } }
);
// Result: { a: 1, b: { c: 2, d: 3 } }

Best Practices

1. Initialize Early

Initialize the ConfigService as early as possible in your app lifecycle, before rendering any components.

2. Use Wildcard Defaults

Define common defaults under app.defaults with wildcard paths to reduce duplication:

{
  "app": {
    "defaults": {
      "api": {
        "*": {
          "timeout": 5000,
          "retries": 3
        }
      }
    }
  }
}

3. Wait for Ready in Critical Paths

For services that need config on initialization:

class MyService {
  async init() {
    await ConfigService.getInstance().waitForReady();
    const apiUrl = ConfigService.getInstance().get('api.myservice.url');
    // Now safe to use config
  }
}

4. Use React Context for Components

Always use useConfig() or useConfigValue() in React components to automatically re-render on config changes.

5. Subscribe for Non-React Code

In services or utilities, use subscribe() to react to config changes:

class CacheManager {
  constructor() {
    const configService = ConfigService.getInstance();
    this.unsubscribe = configService.subscribe(() => {
      this.updateCacheSettings();
    });
  }

  destroy() {
    this.unsubscribe();
  }
}

6. Keep User Overrides Runtime-Only

User overrides (via updateConfigByPath or mergeConfig) are intentionally not persisted. Use debug overrides in DEV mode if you need persistence.

Architecture

┌─────────────────────────────────────────────────────────┐
│                    ConfigService                        │
│                    (Singleton)                          │
└─────────────────────────────────────────────────────────┘
                           │
           ┌───────────────┼───────────────┐
           │               │               │
           ▼               ▼               ▼
    ┌──────────┐   ┌──────────────┐  ┌──────────┐
    │  Local   │   │    Remote    │  │  Debug   │
    │  Config  │   │    Config    │  │  Config  │
    │  (Base)  │   │  (Cached)    │  │ (DEV)    │
    └──────────┘   └──────────────┘  └──────────┘
           │               │               │
           └───────────────┼───────────────┘
                           │
                    deepMerge (in order)
                           │
                           ▼
                   ┌──────────────┐
                   │     User     │
                   │  Overrides   │
                   │  (Runtime)   │
                   └──────────────┘
                           │
                    deepMerge
                           │
                           ▼
                   ┌──────────────┐
                   │    Debug     │
                   │  Overrides   │
                   │(DEV,Cached)  │
                   └──────────────┘
                           │
                           ▼
                   ┌──────────────┐
                   │    Final     │
                   │   Config     │
                   └──────────────┘

Config Loading Process

  1. Load local config (base)
  2. Check for cached remote config
  3. If cached config is valid, use it
  4. Attempt to fetch remote config in background
  5. On success, cache and merge remote config
  6. On failure, use cached config or fallback to local
  7. If in DEV mode, merge debug config
  8. Apply user overrides (runtime-only)
  9. If in DEV mode, apply debug overrides (cached)
  10. Provide final merged config to application

License

MIT

Support

For issues and feature requests, please open an issue on GitHub.