next-shared-cache

repository·canary·Indexed 19 days ago

https://github.com/caching-tools/next-shared-cache

A specialized ISR/Data cache API for Next.js applications designed for shared caching in distributed, multi-instance environments. It provides @neshca/cache-handler for managing shared caches, on-demand revalidation, and TTL management across App and Pages Routers. The library includes @neshca/json-replacer-reviver for Buffer serialization and supports custom storage implementations via the Handler interface.

Tokens
23.7K
Snippets
71
Records
103
Agent score
63%

What's inside next-shared-cache

  1. Overview of @neshca/cache-handler

    canary
    @neshca/cache-handler is a specialized ISR (Incremental Static Regeneration) and Data cache API designed for Next.js applications. It is specifically built to solve the problem of non-shared caches in distributed, self-hosted environments where multiple application instances run simultaneously. By using a shared cache, you ensure data consistency across instances and simplify on-demand revalidation across all replicas.
  2. Use @neshca/server for HTTP caching

    canary

    Note: @neshca/server is deprecated as of @neshca/cache-handler version 1.7.0 and will be removed in the next major release. Users should migrate to using @neshca/cache-handler/server Handler directly.

    @neshca/server is an efficient HTTP caching server designed to work with @neshca/cache-handler/server. It implements an LRU (Least Recently Used) eviction policy, which automatically removes the least recently accessed items when the cache reaches its capacity to optimize resource usage.

  3. Implementing revalidation in a custom Cache Handler

    canary

    If you are building a custom cache handler for @neshca/cache-handler, you must handle the 'marked as revalidated' state to support App Router's revalidatePath behavior.

    1. In revalidateTag: You must manually mark the cache entry as revalidated (e.g., by updating a flag or metadata in your storage).
    2. In get: You must check if the entry has been marked as revalidated. If it has, return null to signal that the cache is stale and needs to be refreshed.
  4. Configure `keyExpirationStrategy` for Redis Cluster

    canary

    You can choose between two expiration strategies for cache keys:

    • 'EXAT': Uses the EXAT option of the SET command. This is more efficient than EXPIREAT but requires Redis server 6.2.0 or newer.
    • 'EXPIREAT': Uses the EXPIREAT command. This requires an additional command call and requires Redis server 4.0.0 or newer.
  5. Use multiple cache handlers for layered caching

    canary

    The handlers property accepts an array of objects conforming to the Handler interface. You can use multiple handlers to create a caching hierarchy (e.g., a fast local cache followed by a shared remote cache).

    If a handler in the array is null or undefined, it is ignored. This is useful for conditional caching, such as opting out of a remote cache during build processes or falling back to a local cache if a service like Redis is unavailable.

    CacheHandler.onCreation(async () => {
      let handler;
    
      if (process.env.REDIS_AVAILABLE) {
        await client.connect();
    
        handler = await createRedisHandler({
          client,
        });
      } else {
        handler = {
          // ... fallback local handler implementation
        };
      }
    
      return {
        handlers: [handler],
      };
    });
  6. Optimize `revalidateTagQuerySize`

    canary

    The revalidateTagQuerySize option controls the number of tags retrieved in a single query when scanning or searching for tags in Redis.

    • Higher values: Reduce the number of commands sent to Redis but increase the amount of data transferred over the network.
    • Considerations: Redis uses TCP; typically, 65,535 bytes is the maximum packet size (though this depends on MTU). Adjust this value to balance command frequency against network payload size.
  7. Key features of @neshca/cache-handler

    canary

    The library provides several capabilities for managing Next.js caching:

    • Shared Cache: Enables distributed caching for self-hosted deployments with multiple replicas.
    • On-Demand Revalidation: Simplifies the process of revalidating cache across all application instances.
    • TTL Management: Automatically handles cache cleanup to maintain storage efficiency.
    • Router Support: Provides unified setup for both Next.js App Router and Pages Router.
    • Advanced Caching Functions:
      • neshCache: A replacement for Next.js unstable_cache that offers more granular control.
      • neshClassicCache: Designed for the Pages Router to cache expensive operations within getServerSideProps and API routes.
    • Cache Pre-population: Supports using the Next.js instrumentation hook to automatically populate the cache with initial data when the application starts.
  8. Optimize `revalidateTagQuerySize` for performance

    canary

    The revalidateTagQuerySize option controls how many tags are retrieved in a single query when scanning or searching for tags in Redis.

    • Higher values: Reduce the total number of commands sent to Redis but increase the amount of data transferred over the network.
    • Lower values: Increase the number of commands but reduce the payload size per command.

    When tuning this value, consider that Redis uses TCP and typically has a maximum packet size of 65,535 bytes (though this may be lower depending on your MTU).

  9. Create a custom Redis handler for @neshca/cache-handler

    canary

    To create a custom Redis handler, you must implement a handler object containing name, get, set, and revalidateTag methods, and register it using CacheHandler.onCreation.

    Implementation Details:

    • onCreation: Use this callback to initialize your connection (e.g., a Redis client). Always create the client inside this callback.
    • get(key, { implicitTags }): Retrieves the cached value. You must handle JSON parsing and tag validation (comparing stored tags against revalidation timestamps).
    • set(key, cacheHandlerValue): Stores the value. You should handle stringification, expiration (using cacheHandlerValue.lifespan), and tag storage.
    • revalidateTag(tag): Invalidates cache entries associated with a specific tag. This involves updating revalidation timestamps for implicit tags and scanning/deleting keys for explicit tags.
    • Error Handling: Do not use try/catch blocks inside handler methods. If a method throws an error, @neshca/cache-handler will automatically fall back to the next available handler in the handlers array.
    • name: A string identifier useful for debugging and logging.
    import { CacheHandler } from '@neshca/cache-handler';
    import { isImplicitTag } from '@neshca/cache-handler/helpers';
    import { createClient, commandOptions } from 'redis';
    
    CacheHandler.onCreation(async () => {
      const client = createClient({ url: 'redis://localhost:6379' });
      await client.connect();
    
      const customRedisHandler = {
        name: 'redis-strings-custom',
        async get(key, { implicitTags }) {
          // Implementation logic...
        },
        async set(key, cacheHandlerValue) {
          // Implementation logic...
        },
        async revalidateTag(tag) {
          // Implementation logic...
        },
      };
    
      return {
        handlers: [customRedisHandler],
      };
    });
    
    export default CacheHandler;
  10. Build the app without requiring a Redis connection

    canary

    To prevent build failures when a Redis server is unavailable (e.g., during simultaneous deployment of the app and the Redis server), you can modify your cache-handler.mjs to conditionally initialize the Redis handler.

    Use an environment variable, such as REDIS_AVAILABLE, to wrap the connection logic and handler creation inside the CacheHandler.onCreation method. If the variable is not present or is falsy, the handler will not be initialized, allowing the build to proceed without a Redis connection.

    CacheHandler.onCreation(async () => {
      let handler;
    
      if (process.env.REDIS_AVAILABLE) {
        await client.connect();
    
        handler = await createRedisHandler({
          client,
        });
      }
    
      return {
        handlers: [handler],
      };
    });
  11. Verify that the Cache Handler is active using Console Logging

    canary

    To manually verify that your custom cache handler is being invoked, you can add console.log statements directly into your cache handler implementation. This is useful for confirming that the get and set methods are actually being called by Next.js during runtime.

    1. Add logs to your handler methods (e.g., in cache-handler.mjs).
    2. Build and start your application.
    3. Navigate through your application in a browser and check the server console for your log output.
    const handler = {
      name: 'my-cache-handler',
      async get(key) {
        console.log('handler.get', key);
        return cacheStore.get(key);
      },
      async set(key, value) {
        console.log('handler.set', key, value);
        cacheStore.set(key, value);
      },
    };