Thanks to visit codestin.com
Credit goes to github.com

Skip to content

cerebralatlas/Revali

Repository files navigation

Revali Logo

Revali

CI Status NPM Version NPM Downloads Tests Passing TypeScript License Bundle Size GitHub Stars

Framework-agnostic stale-while-revalidate (SWR) data fetching library
A powerful yet lightweight caching library for JavaScript/TypeScript. Works seamlessly with React, Vue, Svelte, or vanilla JS projects.


Features

  • Stale-While-Revalidate: Return cached data instantly, then refresh in the background
  • Smart caching with TTL, LRU eviction, and configurable cache size limits
  • Auto re-render when cache updates (via pub/sub pattern)
  • Request deduplication: Prevent duplicate network requests automatically
  • Advanced error handling with exponential backoff retry strategy
  • Optimistic updates: Manual cache mutation with optional revalidation
  • Auto revalidation on window focus and network reconnection (configurable)
  • Polling / interval revalidation: Automatic data refresh at specified intervals
  • Memory management: Automatic cleanup and cache size limits
  • Cache introspection: Built-in cache management and debugging APIs
  • TypeScript-first: Full type safety with zero any types
  • Framework-agnostic: Use directly or wrap in React/Vue/Svelte hooks
  • Lightweight: ~1.8KB gzipped (~6KB raw), zero dependencies

Installation

npm install revali
yarn add revali
pnpm add revali

Quick Start

import { revaliFetch, subscribe, mutate } from 'revali';

// Fetch data with caching and SWR behavior
const userData = await revaliFetch(
  'user/1',
  () => fetch('https://api.example.com/users/1').then((r) => r.json()),
  {
    ttl: 5 * 60 * 1000, // 5 minutes cache
    retries: 3, // Retry failed requests 3 times
    revalidateOnFocus: true, // Refresh when window regains focus
    refreshInterval: 30 * 1000, // Poll every 30 seconds
  },
);

// Subscribe to cache updates (with error handling)
const unsubscribe = subscribe('user/1', (data, error) => {
  if (error) {
    console.error('Fetch error:', error);
  } else {
    console.log('Updated data:', data);
  }
});

// Optimistic updates with automatic revalidation
mutate(
  'user/1',
  (prev) => ({
    ...prev,
    name: 'Updated Name',
  }),
  true,
); // Will revalidate after mutation

// Cache management
import { clearCache, getCacheInfo, cleanup } from 'revali';

clearCache('user/1'); // Clear specific cache
clearCache(); // Clear all cache
console.log(getCacheInfo()); // { size: 5, keys: ["user/1", ...] }

// Manual revalidation control
import { triggerRevalidation } from 'revali';

triggerRevalidation(); // Manually trigger revalidation for all eligible entries

// Clean up everything
cleanup(); // Clear all cache and remove all subscribers
unsubscribe(); // Or just unsubscribe from specific key

Framework Integration

React Hook

import { useState, useEffect, useCallback } from 'react';
import { revaliFetch, subscribe, mutate, type RevaliOptions } from 'revali';

interface UseRevaliResult<T> {
  data: T | undefined;
  error: Error | undefined;
  isLoading: boolean;
  isValidating: boolean;
  mutate: (data: T | ((prev: T | undefined) => T), shouldRevalidate?: boolean) => T;
}

export function useRevali<T>(
  key: string,
  fetcher: () => Promise<T>,
  options?: RevaliOptions,
): UseRevaliResult<T> {
  const [data, setData] = useState<T | undefined>();
  const [error, setError] = useState<Error | undefined>();
  const [isLoading, setIsLoading] = useState(true);
  const [isValidating, setIsValidating] = useState(false);

  useEffect(() => {
    let mounted = true;

    const loadData = async () => {
      if (!mounted) return;

      setIsLoading(true);
      setIsValidating(true);

      try {
        const result = await revaliFetch(key, fetcher, options);
        if (mounted) {
          setData(result);
          setError(undefined);
        }
      } catch (err) {
        if (mounted) {
          setError(err instanceof Error ? err : new Error(String(err)));
        }
      } finally {
        if (mounted) {
          setIsLoading(false);
          setIsValidating(false);
        }
      }
    };

    loadData();

    // Subscribe to cache updates
    const unsubscribe = subscribe(key, (newData, newError) => {
      if (!mounted) return;

      setData(newData);
      setError(newError);
      setIsValidating(false);
    });

    return () => {
      mounted = false;
      unsubscribe();
    };
  }, [key]);

  const mutateFn = useCallback(
    (data: T | ((prev: T | undefined) => T), shouldRevalidate = true) => {
      return mutate(key, data, shouldRevalidate);
    },
    [key],
  );

  return { data, error, isLoading, isValidating, mutate: mutateFn };
}

// Usage
function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading, mutate } = useRevali(
    `user/${userId}`,
    () => fetch(`/api/users/${userId}`).then((r) => r.json()),
    { ttl: 5 * 60 * 1000 },
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{data?.name}</h1>
      <button onClick={() => mutate((prev) => ({ ...prev, name: 'New Name' }))}>Update Name</button>
    </div>
  );
}

Vue Composition API

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { revaliFetch, subscribe, type RevaliOptions } from 'revali';

interface UseRevaliResult<T> {
  data: Ref<T | undefined>;
  error: Ref<Error | undefined>;
  isLoading: Ref<boolean>;
}

function useRevali<T>(
  key: string,
  fetcher: () => Promise<T>,
  options?: RevaliOptions,
): UseRevaliResult<T> {
  const data = ref<T>();
  const error = ref<Error>();
  const isLoading = ref(true);

  let unsubscribe: (() => void) | null = null;

  const load = async () => {
    isLoading.value = true;
    try {
      data.value = await revaliFetch(key, fetcher, options);
      error.value = undefined;
    } catch (err) {
      error.value = err instanceof Error ? err : new Error(String(err));
    } finally {
      isLoading.value = false;
    }
  };

  onMounted(async () => {
    await load();

    unsubscribe = subscribe(key, (newData, newError) => {
      data.value = newData;
      error.value = newError;
    });
  });

  onUnmounted(() => {
    unsubscribe?.();
  });

  return { data, error, isLoading };
}

// Usage
const {
  data: user,
  error,
  isLoading,
} = useRevali('user/1', () => fetch('/api/user/1').then((r) => r.json()));
</script>

API Reference

Core Functions

revaliFetch<T>(key, fetcher, options?): Promise<T>

The main function for fetching and caching data.

const data = await revaliFetch('posts/1', () => fetch('/api/posts/1').then((r) => r.json()), {
  ttl: 5 * 60 * 1000, // Cache for 5 minutes
  retries: 3, // Retry 3 times on failure
  retryDelay: 1000, // Initial retry delay
  maxCacheSize: 50, // Max cache entries
  revalidateOnFocus: true, // Revalidate on window focus
  revalidateOnReconnect: true, // Revalidate on network reconnect
});

subscribe<T>(key, callback): () => void

Subscribe to cache updates for a specific key.

const unsubscribe = subscribe('user/1', (data, error) => {
  if (error) console.error('Error:', error);
  else console.log('Data updated:', data);
});

mutate<T>(key, data, shouldRevalidate?): T

Manually update cache and optionally trigger revalidation.

// Update with new data
mutate('user/1', { id: 1, name: 'John Doe' });

// Update with function
mutate('user/1', (prev) => ({ ...prev, name: 'Jane Doe' }));

// Update without revalidation
mutate('user/1', newData, false);

clearCache(key?): void

Clear cache entries.

clearCache('user/1'); // Clear specific key
clearCache(); // Clear all cache

getCacheInfo(): { size: number; keys: string[] }

Get information about the current cache state.

const { size, keys } = getCacheInfo();
console.log(`Cache has ${size} entries:`, keys);

cleanup(): void

Clean up all cache entries and remove all subscribers. Useful for complete cleanup.

import { cleanup } from 'revali';

// Clear everything - cache and subscribers
cleanup();

triggerRevalidation(): void

Manually trigger revalidation for all eligible cache entries.

import { triggerRevalidation } from 'revali';

// Manually revalidate all entries that have revalidateOnFocus or revalidateOnReconnect enabled
triggerRevalidation();

initAutoRevalidation(): void

Manually initialize automatic revalidation listeners. This is called automatically when importing Revali, but can be called manually if needed.

import { initAutoRevalidation } from 'revali';

// Manually set up window focus and network reconnect listeners
// (This is done automatically when importing Revali)
initAutoRevalidation();

getPollingInfo(): { activeCount: number; keys: string[] }

Get information about active polling tasks.

import { getPollingInfo } from 'revali';

const pollingInfo = getPollingInfo();
console.log(`Active polling tasks: ${pollingInfo.activeCount}`);
console.log('Polling keys:', pollingInfo.keys);

hasActivePolling(key: string): boolean

Check if a specific key has active polling.

import { hasActivePolling } from 'revali';

if (hasActivePolling('user/1')) {
  console.log('User data is being polled');
}

cleanupPolling(): void

Clean up all active polling tasks. Useful for cleanup when your application is shutting down.

import { cleanupPolling } from 'revali';

// Stop all polling tasks
cleanupPolling();

TypeScript Types

export type Fetcher<T> = () => Promise<T>;
export type Subscriber<T> = (data: T | undefined, error?: Error) => void;

export interface RevaliOptions {
  retries?: number; // Max retry attempts (default: 2)
  retryDelay?: number; // Initial retry delay in ms (default: 300)
  ttl?: number; // Cache TTL in ms (default: 300000 = 5min)
  maxCacheSize?: number; // Max cache entries (default: 100)
  revalidateOnFocus?: boolean; // Revalidate on focus (default: true)
  revalidateOnReconnect?: boolean; // Revalidate on reconnect (default: true)
  refreshInterval?: number; // Polling interval in ms, 0 means no polling (default: 0)
  refreshWhenHidden?: boolean; // Continue polling when page is hidden (default: false)
  refreshWhenOffline?: boolean; // Continue polling when offline (default: false)
  dedupingInterval?: number; // Deduping interval in ms (default: 2000)
}

export interface CacheEntry<T> {
  data: T | undefined;
  timestamp: number;
  error?: Error;
  fetcher: Fetcher<T>;
  options: RevaliOptions;
}

export interface RevaliState<T> {
  data?: T;
  error?: Error;
  isLoading: boolean;
  isValidating: boolean;
}

// Default configuration constant
export const DEFAULT_OPTIONS: Required<RevaliOptions>;

Comparison with Popular Libraries

Feature SWR TanStack Query Revali 🚀
Bundle Size ~6KB ~13KB ~1.8KB
Framework Support React Multi Any
TypeScript-first
Request Deduplication
Cache with TTL
Error Retry + Backoff
Optimistic Updates
Background Revalidation
Focus/Reconnect Revalidate
Memory Management Limited
Cache Introspection Limited
Zero Dependencies

Roadmap

Completed (v0.2.0)

  • ✅ Basic cache & subscription system
  • ✅ Request deduplication
  • ✅ Error retry with exponential backoff
  • ✅ Revalidate on focus & network reconnect
  • ✅ Manual mutate / optimistic updates
  • ✅ TTL-based cache expiration
  • ✅ LRU cache eviction
  • ✅ Memory management & cleanup
  • ✅ Full TypeScript support
  • ✅ Cache introspection APIs

Completed (v0.3.0)

  • ✅ Polling / interval revalidation
  • ✅ Request cancellation (AbortController)

In Progress (v0.3.0)

  • Middleware system
  • Built-in React/Vue/Svelte hooks

Future (v0.4.0)

  • Pagination & infinite loading
  • Offline support with persistence
  • DevTools browser extension
  • GraphQL integration
  • SSR/SSG support

Advanced Usage

Auto-Initialization Behavior

Revali automatically sets up revalidation listeners when imported:

import { revaliFetch } from 'revali';
// Automatically listens for:
// - Window focus events (revalidates when tab becomes active)
// - Network online events (revalidates when connection restored)

Manual Control

You can control revalidation behavior manually:

import { triggerRevalidation, initAutoRevalidation, DEFAULT_OPTIONS } from 'revali';

// Use default options as base for custom configuration
const customOptions = {
  ...DEFAULT_OPTIONS,
  ttl: 10 * 60 * 1000, // Override TTL to 10 minutes
  retries: 5, // Override retries to 5
};

// Manually trigger revalidation
triggerRevalidation();

// Access default configuration
console.log('Default TTL:', DEFAULT_OPTIONS.ttl);

Polling / Interval Revalidation

Configure automatic data refresh at specified intervals:

import { revaliFetch, getPollingInfo, hasActivePolling } from 'revali';

// Basic polling - refresh every 30 seconds
const liveStats = await revaliFetch(
  'live-stats',
  () => fetch('/api/stats').then(r => r.json()),
  {
    refreshInterval: 30 * 1000, // 30 seconds
    ttl: 5 * 60 * 1000, // 5 minutes cache
  }
);

// Advanced polling configuration
const criticalData = await revaliFetch(
  'critical-data',
  fetchCriticalData,
  {
    refreshInterval: 5 * 1000,   // Poll every 5 seconds
    refreshWhenHidden: true,     // Continue when tab is not active
    refreshWhenOffline: false,   // Pause when offline
    dedupingInterval: 2000,      // Prevent requests closer than 2s
    ttl: 10 * 1000,             // Short cache TTL for fresh data
  }
);

// Check polling status
console.log('Polling info:', getPollingInfo());
console.log('Is polling active:', hasActivePolling('live-stats'));

// Polling automatically stops when cache is cleared
clearCache('live-stats'); // Stops polling for this key

Polling Best Practices

  1. Choose appropriate intervals: Balance freshness needs with server load
  2. Use refreshWhenHidden: false for non-critical data to save resources
  3. Configure dedupingInterval to prevent excessive requests
  4. Monitor polling with getPollingInfo() for debugging

Request Cancellation (AbortController)

Revali supports request cancellation using the standard AbortController API, providing fine-grained control over request lifecycle:

Basic Cancellation

import { revaliFetch, cancel } from 'revali';

// Cancel a specific request
const key = 'user-data';
const promise = revaliFetch(key, async (signal) => {
  const response = await fetch('/api/user', { signal });
  return response.json();
});

// Cancel the request
cancel(key);

// The promise will reject with CancellationError
try {
  await promise;
} catch (error) {
  if (error.name === 'CancellationError') {
    console.log('Request was cancelled');
  }
}

External AbortController

import { revaliFetch } from 'revali';

// Use your own AbortController
const controller = new AbortController();
const promise = revaliFetch('data', async (signal) => {
  const response = await fetch('/api/data', { signal });
  return response.json();
}, {
  signal: controller.signal
});

// Cancel using your controller
controller.abort();

Request Timeout

import { revaliFetch } from 'revali';

// Automatically cancel after timeout
const data = await revaliFetch('slow-api', async (signal) => {
  const response = await fetch('/api/slow', { signal });
  return response.json();
}, {
  abortTimeout: 5000 // Cancel after 5 seconds
});

Cancel on Revalidate

import { revaliFetch } from 'revali';

// Cancel previous request when starting new one
const searchResults = await revaliFetch(`search-${query}`, async (signal) => {
  const response = await fetch(`/api/search?q=${query}`, { signal });
  return response.json();
}, {
  abortOnRevalidate: true // Cancel previous search when new search starts
});

Cancellation API

import { 
  cancel, 
  cancelAll, 
  isCancelled, 
  getCancellationInfo,
  isCancellationError 
} from 'revali';

// Cancel specific request
const cancelled = cancel('request-key');
console.log('Cancelled:', cancelled);

// Cancel all active requests
const cancelledCount = cancelAll();
console.log('Cancelled count:', cancelledCount);

// Check if request was cancelled
const wasCancelled = isCancelled('request-key');

// Get cancellation information
const info = getCancellationInfo();
console.log('Active requests:', info.activeCount);
console.log('Active keys:', info.keys);

// Check if error is cancellation error
try {
  await someRequest();
} catch (error) {
  if (isCancellationError(error)) {
    console.log('Request was cancelled');
  }
}

Best Practices for Cancellation

  1. Always handle AbortSignal in your fetchers:

    const fetcher = async (signal?: AbortSignal) => {
      const response = await fetch('/api/data', { signal });
      if (!response.ok) throw new Error('Failed');
      return response.json();
    };
  2. Use abortOnRevalidate for search/filter scenarios:

    const searchData = await revaliFetch(`search-${query}`, fetcher, {
      abortOnRevalidate: true // Cancel previous search
    });
  3. Set reasonable timeouts for slow operations:

    const heavyComputation = await revaliFetch('compute', fetcher, {
      abortTimeout: 30000 // 30 second timeout
    });
  4. Clean up on component unmount (React example):

    useEffect(() => {
      const controller = new AbortController();
      
      revaliFetch('component-data', fetcher, {
        signal: controller.signal
      });
      
      return () => controller.abort(); // Clean up on unmount
    }, []);

Tree-Shaking Support

Thanks to the modular architecture, bundlers can tree-shake unused code:

// Only imports the functions you use
import { revaliFetch, mutate } from 'revali';
// ✅ Other modules (cleanup, revalidation) won't be included in bundle

Performance Best Practices

1. Optimize Cache Keys

Use consistent, descriptive cache keys:

// ✅ Good - consistent and descriptive
const userId = 123;
const userData = await revaliFetch(`user/${userId}`, fetchUser);
const userPosts = await revaliFetch(`user/${userId}/posts`, fetchUserPosts);

// ❌ Avoid - inconsistent or too generic
const userData = await revaliFetch(`user-${userId}`, fetchUser);
const userPosts = await revaliFetch(`posts`, fetchUserPosts);

2. Configure Appropriate TTL

Set TTL based on data freshness requirements:

// Fast-changing data - shorter TTL
const liveData = await revaliFetch('live-stats', fetchStats, {
  ttl: 30 * 1000, // 30 seconds
});

// Relatively stable data - longer TTL
const userData = await revaliFetch('user/profile', fetchProfile, {
  ttl: 5 * 60 * 1000, // 5 minutes
});

// Static data - very long TTL
const appConfig = await revaliFetch('app/config', fetchConfig, {
  ttl: 60 * 60 * 1000, // 1 hour
});

3. Manage Cache Size

Configure appropriate cache limits:

const options = {
  maxCacheSize: 50, // Limit cache entries for memory efficiency
  ttl: 5 * 60 * 1000,
};

4. Cleanup on Unmount

Always cleanup subscriptions:

useEffect(() => {
  const unsubscribe = subscribe(key, handleUpdate);
  return () => {
    unsubscribe(); // Prevent memory leaks
  };
}, [key]);

Why Revali?

Revali (from "revalidate") was created to understand how modern data fetching libraries work under the hood, while providing a framework-agnostic solution that's both powerful and lightweight.

Design Philosophy

  • Framework Agnostic: Works with any UI library or vanilla JavaScript
  • TypeScript First: Built with type safety as a priority
  • Zero Dependencies: No external dependencies, minimal bundle size
  • Memory Conscious: Smart caching with automatic cleanup
  • Developer Experience: Simple API with powerful features
  • Modular Architecture: Clean separation of concerns for better maintainability

Contributing

We welcome contributions! Here's how you can help:

  • Report bugs via GitHub Issues
  • Suggest features or improvements
  • Improve documentation and examples
  • Add tests for edge cases
  • Submit PRs for bug fixes or features

Development Setup

git clone https://github.com/cerebralatlas/revali.git
cd revali
pnpm install

# Development
pnpm run dev

# Build
pnpm run build

# Test
pnpm run test
pnpm run test:coverage    # Run tests with coverage report
pnpm run test:ui         # Run tests with UI

# Type checking
pnpm run type-check

# Linting
pnpm run lint
pnpm run lint:fix

Project Quality

  • 98% Test Coverage: Comprehensive test suite covering all core functionality
  • Modular Architecture: Clean separation of concerns for maintainability
  • Zero Dependencies: No external runtime dependencies

Bundle Size Breakdown

Format Raw Size Gzipped Minified + Gzipped
ESM ~6.2KB ~1.8KB ~1.8KB
CJS ~7.6KB ~2.2KB ~2.2KB

Actual network transfer size is the gzipped size, making Revali one of the smallest SWR libraries available.

License

MIT © Cerebral Atlas


Star this repo if you find it useful!

About

Framework-agnostic stale-while-revalidate (SWR) data fetching library

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors 2

  •  
  •