Skip to content

Simple and robust caching layer using Firebase Realtime Database

License

Notifications You must be signed in to change notification settings

iamanishroy/flamecache

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

16 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Flamecache

npm version License: MIT TypeScript Bundle size

A Redis-like caching layer for Firebase Realtime Database with automatic expiration, counters, and batch operations.

Flamecache is a lightweight, robust caching solution that brings Redis-style operations to Firebase Realtime Database. Perfect for serverless applications, real-time apps, and projects already using Firebase.

✨ Features

  • πŸš€ Redis-like API - Familiar get, set, incr, decr, expire operations
  • ⚑ Fast & Lightweight - Only ~1KB (minified), no heavy dependencies
  • πŸ”„ Auto-expiration - Built-in TTL support with automatic cleanup
  • πŸ“Š Counters - Atomic increment/decrement operations
  • πŸ“¦ Batch Operations - Multi-get, multi-set, multi-delete
  • 🎯 TypeScript - Full type safety with generics
  • πŸ”Œ Zero Config - Works out of the box
  • 🌐 Serverless Ready - Perfect for Firebase Functions, Vercel, Netlify

πŸ“¦ Installation

npm install flamecache firebase

Flamecache requires Firebase as a peer dependency. If you don't have Firebase installed:

npm install firebase flamecache

πŸš€ Quick Start

import { createCache } from 'flamecache';

// Initialize cache
const cache = createCache({
  firebase: {
    apiKey: 'your-api-key',
    databaseURL: 'https://your-project.firebaseio.com',
    projectId: 'your-project-id',
  },
  rootPath: 'my-cache', // Root path in Firebase (default: 'flamecache')
  ttl: 3600, // Default TTL: 1 hour
  disableCache: false, // Optional: disable cache interactions (useful for testing/development)
});

// Basic usage
await cache.set('user:123', { name: 'Alice', email: 'alice@example.com' });
const user = await cache.get('user:123');

// With custom TTL (in seconds)
await cache.set('temp:token', 'abc123', 300); // Expires in 5 minutes

// Counters
await cache.incr('views:post:456'); // Increment view count
const views = await cache.get('views:post:456'); // 1

// Read-through caching
const posts = await cache.wrap(
  'posts:latest',
  async () => {
    // This function only runs on cache miss
    const response = await fetch('https://api.example.com/posts');
    return response.json();
  },
  600
); // Cache for 10 minutes

πŸ“– API Reference

Core Operations

get<T>(key: string): Promise<T | null>

Retrieve a value from cache.

const user = await cache.get<User>('user:123');
if (user) {
  console.log(user.name);
}

set<T>(key: string, data: T, ttl?: number): Promise<void>

Store a value in cache with optional TTL (in seconds).

// With default TTL
await cache.set('user:123', userData);

// With custom TTL (10 minutes)
await cache.set('session:abc', sessionData, 600);

// Never expires
await cache.set('config', configData, 0);

del(key: string): Promise<void>

Delete a key from cache.

await cache.del('user:123');

has(key: string): Promise<boolean>

Check if a key exists and is not expired.

if (await cache.has('session:abc')) {
  console.log('Session is valid');
}

clear(): Promise<void>

Clear all cache entries.

await cache.clear();

disconnect(): Promise<void>

Close the connection to Firebase. Crucial for Node.js scripts to exit cleanly.

await cache.disconnect();

Counter Operations

incr(key: string, by?: number): Promise<number>

Increment a numeric value. Returns the new value.

const views = await cache.incr('views:post:123'); // Increment by 1
const score = await cache.incr('score:player', 10); // Increment by 10

decr(key: string, by?: number): Promise<number>

Decrement a numeric value. Returns the new value.

const stock = await cache.decr('stock:item:456'); // Decrement by 1
const credits = await cache.decr('credits:user', 5); // Decrement by 5

TTL Operations

expire(key: string, ttl: number): Promise<boolean>

Set or update expiration time for an existing key (in seconds).

await cache.expire('session:abc', 300); // Expire in 5 minutes
await cache.expire('permanent:key', 0); // Remove expiration

getTtl(key: string): Promise<number>

Get remaining time-to-live in seconds.

  • Returns 0 if key doesn't exist or is expired
  • Returns -1 if key has no expiration
  • Returns remaining seconds otherwise
const remaining = await cache.getTtl('session:abc');
if (remaining > 0 && remaining < 300) {
  console.log('Session expiring soon!');
}

touch(key: string, ttl?: number): Promise<boolean>

Refresh TTL without changing the value.

// Use default TTL
await cache.touch('session:abc');

// Use custom TTL
await cache.touch('session:abc', 3600);

Batch Operations

mget<T>(keys: string[]): Promise<(T | null)[]>

Get multiple keys at once.

const [user1, user2, user3] = await cache.mget(['user:1', 'user:2', 'user:3']);

mset(entries: Record<string, any>, ttl?: number): Promise<void>

Set multiple keys at once.

await cache.mset(
  {
    'user:1': userData1,
    'user:2': userData2,
    'user:3': userData3,
  },
  600
); // All expire in 10 minutes

mdel(keys: string[]): Promise<void>

Delete multiple keys at once.

await cache.mdel(['temp:1', 'temp:2', 'temp:3']);

Advanced Operations

wrap<T>(key: string, fetchFn: () => Promise<T>, ttl?: number): Promise<T>

Read-through caching pattern. Gets from cache or fetches and stores automatically.

const userData = await cache.wrap(
  'user:123',
  async () => {
    // This only runs on cache miss
    const response = await fetch(`/api/users/123`);
    return response.json();
  },
  300
);

pull<T>(key: string): Promise<T | null>

Get and delete in one operation (atomic pop).

const token = await cache.pull('temp:verification-token');
// Token is retrieved and deleted

🎯 Use Cases

1. Rate Limiting

async function checkRateLimit(userId: string): Promise<boolean> {
  const key = `ratelimit:${userId}`;
  const count = await cache.incr(key);

  // Set expiration on first request
  if (count === 1) {
    await cache.expire(key, 60); // Reset every minute
  }

  return count <= 100; // Max 100 requests per minute
}

// Usage
if (!(await checkRateLimit('user123'))) {
  throw new Error('Rate limit exceeded');
}

2. Session Management

class SessionManager {
  async create(userId: string, data: any): Promise<string> {
    const sessionId = generateId();
    await cache.set(
      `session:${sessionId}`,
      {
        userId,
        ...data,
        createdAt: Date.now(),
      },
      3600
    ); // 1 hour session
    return sessionId;
  }

  async get(sessionId: string) {
    return await cache.get(`session:${sessionId}`);
  }

  async extend(sessionId: string): Promise<boolean> {
    return await cache.touch(`session:${sessionId}`, 3600);
  }

  async destroy(sessionId: string): Promise<void> {
    await cache.del(`session:${sessionId}`);
  }
}

3. API Response Caching

async function getUser(userId: string) {
  return cache.wrap(
    `api:user:${userId}`,
    async () => {
      console.log('Fetching from API...');
      const response = await fetch(`https://api.example.com/users/${userId}`);
      return response.json();
    },
    600
  ); // Cache for 10 minutes
}

// First call - fetches from API
const user1 = await getUser('123');

// Second call - uses cache (within 10 minutes)
const user2 = await getUser('123');

4. Leaderboard / Scoring

async function addScore(playerId: string, points: number) {
  await cache.incr(`score:${playerId}`, points);
}

async function getTopScores(playerIds: string[]) {
  const keys = playerIds.map((id) => `score:${id}`);
  const scores = await cache.mget<number>(keys);

  return playerIds
    .map((id, i) => ({
      playerId: id,
      score: scores[i] || 0,
    }))
    .sort((a, b) => b.score - a.score);
}

5. Temporary Tokens

async function createVerificationToken(email: string): Promise<string> {
  const token = generateToken();
  await cache.set(`verify:${token}`, { email }, 900); // 15 minutes
  return token;
}

async function verifyToken(token: string) {
  // Pull removes the token after reading (one-time use)
  const data = await cache.pull(`verify:${token}`);
  if (!data) {
    throw new Error('Invalid or expired token');
  }
  return data;
}

6. Real-time Analytics

// Track page views
async function trackPageView(pageId: string) {
  await cache.incr(`views:${pageId}`);
  await cache.incr(`views:today:${pageId}`);

  // Expire daily stats at midnight
  const secondsUntilMidnight = getSecondsUntilMidnight();
  await cache.expire(`views:today:${pageId}`, secondsUntilMidnight);
}

// Get analytics
async function getAnalytics(pageIds: string[]) {
  const totalKeys = pageIds.map((id) => `views:${id}`);
  const todayKeys = pageIds.map((id) => `views:today:${id}`);

  const [totalViews, todayViews] = await Promise.all([
    cache.mget<number>(totalKeys),
    cache.mget<number>(todayKeys),
  ]);

  return pageIds.map((id, i) => ({
    pageId: id,
    total: totalViews[i] || 0,
    today: todayViews[i] || 0,
  }));
}

πŸ“Š Performance

Here are the benchmark results running against a real Firebase Realtime Database instance (50 iterations per operation):

Operation Avg Latency Throughput
GET (Cache Hit) ~110ms 9 ops/sec
SET (Write) ~120ms 8 ops/sec
INCR (Atomic) ~230ms 4 ops/sec
MSET (Batch Write) ~2.4ms / key 415+ keys/sec
MGET (Batch Read) ~3.6ms / key 275+ keys/sec

All benchmarks include real-world network latency to Firebase. Batch operations leverage parallelization and atomic multi-path updates for maximum efficiency.

You can run these benchmarks yourself:

npm run example benchmark

See the benchmark script for details.

πŸ”§ Configuration

interface CacheConfig {
  // Firebase configuration (required)
  firebase: {
    apiKey: string;
    authDomain?: string;
    databaseURL: string;
    projectId: string;
    storageBucket?: string;
    messagingSenderId?: string;
    appId?: string;
  };

  // Root path in Firebase database (default: 'flamecache')
  rootPath?: string;

  // Default TTL in seconds (default: 3600 = 1 hour, 0 = never expires)
  ttl?: number;

  // Enable debug logs (default: false)
  debug?: boolean;

  // Disable all cache operations (default: false)
  // When true: get/mget return null, set/mset/del do nothing, wrap always fetches
  disableCache?: boolean;
}

// Example with all options
const cache = createCache({
  firebase: {
    /* ... */
  },
  rootPath: 'my-cache',
  ttl: 7200, // 2 hours
  debug: true, // Log all operations
});

πŸ”‘ Key Naming Conventions

Use colons (:) to create hierarchical keys for better organization:

// Good βœ…
'user:123';
'user:123:profile';
'session:abc123';
'api:github:users:456';
'ratelimit:user:789';
'cache:posts:latest';

// Avoid ❌
'user_123';
'user-profile-123';
'session.abc123';

🎨 TypeScript Support

Full TypeScript support with generics:

interface User {
  id: string;
  name: string;
  email: string;
}

// Type-safe operations
const user = await cache.get<User>('user:123');
// user is typed as User | null

await cache.set<User>('user:123', {
  id: '123',
  name: 'Alice',
  email: 'alice@example.com',
});

// Works with complex types
type ApiResponse = {
  data: User[];
  meta: { total: number };
};

const response = await cache.wrap<ApiResponse>('api:users', fetchUsers);

πŸ”’ Firebase Security Rules

Development

{
  "rules": {
    "flamecache": {
      ".read": true,
      ".write": true
    }
  }
}

Production

{
  "rules": {
    "flamecache": {
      ".read": "auth != null",
      ".write": "auth != null"
    }
  }
}

Advanced (per-user caching)

{
  "rules": {
    "flamecache": {
      "users": {
        "$uid": {
          ".read": "$uid === auth.uid",
          ".write": "$uid === auth.uid"
        }
      },
      "public": {
        ".read": true,
        ".write": "auth != null"
      }
    }
  }
}

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

# Clone the repo
git clone https://github.com/iamanishroy/flamecache.git
cd flamecache

# Install dependencies
npm install

# Run tests
npm test

# Build
npm run build

πŸ“ License

MIT Β© Anish Roy

πŸ”— Links

About

Simple and robust caching layer using Firebase Realtime Database

Resources

License

Stars

Watchers

Forks

Packages

No packages published