New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@santoshkurmi/pro_cache

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@santoshkurmi/pro_cache

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

latest
npmnpm
Version
1.0.1
Version published
Maintainers
1
Created
Source

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

  • Election: When you open your app, tabs coordinate using localStorage and BroadcastChannel. One tab becomes the Leader.
  • Single Connection: Only the Leader opens a WebSocket connection to your backend.
  • Broadcast: When the Leader receives an invalidation event from the server, it broadcasts that event to all Follower tabs via a BroadcastChannel.
  • 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

OptionDescription
routeToCacheKeyTransforms a dynamic URL (like /todos/1) into a generic key (/todos/{id}) for broad invalidation.
handleMessageIntercept any socket message before it's processed. Gives access to ctx.db, ctx.cache, and ctx.broadcast.
shouldInvalidateA predicate to determine if a specific key should be purged from cache based on incoming server data.
getTimestampReturns 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

  • npm run build
  • npm login
  • npm publish --access public

License

MIT

FAQs

Package last updated on 08 Jan 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts