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

@santoshkurmi/pro_cache

v1.0.1

Published

High-performance reactive caching and WebSocket synchronization library for React & SolidJS

Readme

ProCache

High-performance reactive caching and WebSocket synchronization library for React & SolidJS.

ProCache provides a robust, multi-tab synchronization layer for your web applications. It implements a Leader/Follower election system to ensure only one WebSocket connection is active across multiple browser tabs, reducing server load and ensuring consistent data invalidation.


🏗 How It Works (Architecture)

ProCache is built to solve the "One Socket, Many Tabs" problem while keeping all tabs in perfect sync.

👑 Leader/Follower System

  1. Election: When you open your app, tabs coordinate using localStorage and BroadcastChannel. One tab becomes the Leader.
  2. Single Connection: Only the Leader opens a WebSocket connection to your backend.
  3. Broadcast: When the Leader receives an invalidation event from the server, it broadcasts that event to all Follower tabs via a BroadcastChannel.
  4. Automatic Handoff: If the Leader tab is closed, a Follower instantly detects the absence and elects itself as the new Leader, establishing a fresh WebSocket.

🔄 Multi-Tab Synchronization

  • State Sync: If you toggle the cache "OFF" in Tab #5, Tab #1 (Leader) will disconnect its socket, and every other tab will instantly update its UI to reflect the new state.
  • Timestamp Guard: Every piece of data is stored with a server-authoritative timestamp. If Tab A fetches data, Tab B will know it's fresh because they share the same IndexedDB and state logic.

🚀 Installation

npm install pro_cache

⚙️ Advanced Configuration

The createProCache function allows full control over how your app handles data and messages.

import { createProCache } from 'pro_cache';

let currentToken = '';

export const setCacheToken = (token: string) => {
    currentToken = token;
};

const apiBase = `http://${window.location.hostname}:3001/api`;
const WS_URL = `ws://${window.location.hostname}:8080/ws`;

export const cache = createProCache({ 
    debug: true,
    enabled:localStorage.getItem('pro_cache_enabled') != 'false',
    autoRefetchOnInvalidation: false,
    cacheWritesOffline: true, // Enable offline writing
     db: {
        dbName: 'finance_pro_cache',
        dbVersion: 1
    }, 
    api: {
        baseUrl: apiBase,
        defaultCacheTtl: 600
    },
    getTimestamp: (response) => {
        const serverTs = response.headers?.['x-server-timestamp'];
        return serverTs ? parseInt(serverTs, 10) : Date.now();
    },
    ws: {
        url: () => `${WS_URL}?token=${currentToken}`,
        routeToCacheKey: (routePath: string) => routePath,
        activityIndicatorDuration: 1000,
        startup: {
            enableCacheBeforeSocket: false, // Don't serve stale data on boot
            waitForSocket: true,            // Wait for socket to connect
            socketWaitTimeout: 3000         // 3s timeout
        },
        defaultBackgroundDelay: 1000,
        backgroundPollInterval: 200,
        shouldInvalidate: async (key, value, db) => {
            if (cache.config.debug) console.log(`[App] Checking invalidation for ${key}`, value);
            if (key === 'all') {
                 if (cache.config.debug) console.log('[App] Full cache clear requested');
                 return true; 
            }
            const localTs = await db.getTimestamp(key);
            if (localTs && localTs >= value) {
                if (cache.config.debug) console.log(`[App] Ignoring stale data for ${key}`);
                return false;
            }
            return true;
        },
        handleMessage: async (msg, ctx, defaultHandler) => {
             ctx.log('[App] Custom Message Middleware:', msg);

             // Handle maintenance mode
             if (msg.type === 'server-maintenance') {
                 alert('Maintenance Mode: ' + msg.message);
                 return;
             }

             // 2. Manual Invalidation Handling (Sync/Delta)
             if (msg.type === 'invalidate' || msg.type === 'invalidate-delta') {
                 const data = msg.data as Record<string, number>;
                 
                 // A) Full Sync Logic (type: 'invalidate')
                 if (msg.type === 'invalidate') {
                     // Check for Clock Drift / Server State Reset
                     const driftTime = msg.drift_time;
                     const storedDrift = localStorage.getItem('pro_cache_drift_time');

                     if (driftTime && String(driftTime) !== storedDrift) {
                         ctx.log(`[App] Manual: Clock Drift detected (${storedDrift} -> ${driftTime}). Clearing ALL.`);
                         localStorage.setItem('pro_cache_drift_time', String(driftTime));
                         ctx.cache.clear();
                         await ctx.db.clearAll();
                         ctx.broadcast({ type: 'ws-invalidate-all', timestamp: Date.now() });
                         ctx.enableCache();
                         return;
                     }

                     const keys = Object.keys(data);
                     if (keys.length === 0) {
                         ctx.log('[App] Manual: Full Sync - Empty Data -> Clearing ALL');
                         ctx.cache.clear();
                         await ctx.db.clearAll();
                         ctx.broadcast({ type: 'ws-invalidate-all', timestamp: Date.now() });
                         ctx.enableCache(); // Enable cache after sync
                         return;
                     }
                     
                     // Filter only "Fresh" keys (those we want to KEEP)
                     const validCacheKeys: string[] = [];
                     
                     for (const [key, timestamp] of Object.entries(data)) {
                         const localTs = await ctx.db.getTimestamp(key);
                         // If Local is Fresh (>= Server), we keep it.
                         if (localTs && localTs >= timestamp) {
                             validCacheKeys.push(ctx.routeToCacheKey(key));
                         } else {
                             // If Stale or Missing locally, we don't add it to validCacheKeys.
                             // invalidateExcept will wipe it out.
                             ctx.log(`[App] Manual: Stale data detected for ${key} - will be cleared by sync`);
                         }
                     }

                     ctx.log('[App] Manual: Full Sync - Keeping valid keys, clearing others (Stale + Missing)');
                     await ctx.invalidateExcept(validCacheKeys);
                     ctx.enableCache(); // Enable cache after sync
                     return;
                 }

                 // B) Handle Updates (Delta only)
                 for (const [key, timestamp] of Object.entries(data)) {
                     const localTs = await ctx.db.getTimestamp(key);
                     
                     if (localTs && localTs >= timestamp) {
                         ctx.log(`[App] Manual: Ignoring stale ${key}`);
                         continue;
                     }
                     
                     const cacheKey = ctx.routeToCacheKey(key);
                     ctx.log(`[App] Manual: Invalidating ${cacheKey} (from ${key})`);
                     ctx.cache.invalidate(cacheKey);
                     ctx.broadcast({ type: 'ws-invalidate', key: cacheKey, timestamp });
                     
                    if (document.hasFocus()) {
                         ctx.log(`[App] Manual: Active tab - triggering subscribers immediately`, cacheKey);
                         ctx.triggerSubscribers(cacheKey);
                     } else {
                         ctx.log(`[App] Manual: Background tab - polling subscribers`);
                         ctx.pollSubscribers(cacheKey);
                     }
                 }
                 return;
             }

             // 3. Default for everything else
             if (defaultHandler) {
                 await defaultHandler(msg);
             }
        }
    }
});

//wrap it inside the provider of your app
 <ProCacheProvider client={cache}>
        <App />
    </ProCacheProvider>

Key Config Options

| Option | Description | | :--- | :--- | | routeToCacheKey | Transforms a dynamic URL (like /todos/1) into a generic key (/todos/{id}) for broad invalidation. | | handleMessage | Intercept any socket message before it's processed. Gives access to ctx.db, ctx.cache, and ctx.broadcast. | | shouldInvalidate | A predicate to determine if a specific key should be purged from cache based on incoming server data. | | getTimestamp | Returns the authoritative server time (usually from headers). Essential for cache consistency. |


🎣 React Hooks

useLiveFetch

The primary hook for data fetching. It automatically connects to the invalidation system.

const { 
    data, 
    loading, 
    refetch, 
    isRefetching, 
    isRefetchNeeded 
} = useLiveFetch<Todo[]>('/todo/{id}',{ 
      params:{id:1},
      query:{orderBy:"asc"},
        autoRefetch: true // Manually testing this behavior now
    });
  • data: The current value (from cache first, then API).
  • loading: True during the initial fetch.
  • isRefetching: True during background updates (socket-triggered or manual refetch).
  • isRefetchNeeded: Returns true if an invalidation event was received but autoRefetch is disabled. Perfect for showing "New Data Available" banners.
  • refetch({ force: true }): Bypasses the cache entirely to fetch fresh data from the server.

useProCacheStatus

Monitor the heartbeat of the library:

const { wsStatus, isLeaderTab, isCacheEnabled, isDebugEnabled } = useProCacheStatus();

🛠 Manual State Control

You can bind keyboard shortcuts or UI toggles to manage the library state globally.

import { useToggleCaching, useProCache } from 'pro_cache';

function App() {
    const { toggle } = useToggleCaching();
    const client = useProCache();

    useEffect(() => {
        const handler = (e: KeyboardEvent) => {
            if (e.ctrlKey && e.key === 'k') toggle(); // Toggle Cache
            if (e.ctrlKey && e.shiftKey && e.key === 'D') {
                client.socket.setDebug(!client.socket.debugStatus());
            }
        };
        window.addEventListener('keydown', handler);
        return () => window.removeEventListener('keydown', handler);
    }, []);
}

📦 Publishing

  1. npm run build
  2. npm login
  3. npm publish --access public

License

MIT