Member Junction
    Preparing search index...

    Redis-backed implementation of the MemberJunction ILocalStorageProvider interface.

    Provides persistent, shared caching for server-side environments using Redis. This is a drop-in replacement for InMemoryLocalStorageProvider — all consumers (like LocalCacheManager, ProviderBase metadata caching, etc.) work without any code changes.

    All keys follow the pattern: {prefix}:{category}:{key}

    • prefix — configurable, defaults to "mj" to isolate MJ data in shared Redis instances
    • category — maps to the MJ cache category (RunViewCache, Metadata, DatasetCache, etc.)
    • key — the original key from the caller

    Keys written with an IndexGroup are also recorded in a Redis Set at {prefix}:__group__:{category}:{group} (the RunView cache uses the entity name), which GetIndexGroupKeys() reads and prunes. ClearCategory() and GetCategoryKeys() walk the keyspace with SCAN; no category-wide set is kept, because expiry never removes set members and such a set grows without bound once keys carry a TTL.

    Redis has native key expiration. The provider supports TTL at two levels:

    1. defaultTTLSeconds in config — applied to all SetItem calls
    2. ttlSeconds parameter on SetItem — overrides the default per-call

    Redis operations are wrapped in try/catch blocks. Connection errors are logged via LogError() but do not throw — the provider gracefully returns null for reads and silently skips writes. This prevents a Redis outage from crashing the application. The ioredis client handles automatic reconnection.

    import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';

    const provider = new RedisLocalStorageProvider({
    url: 'redis://localhost:6379',
    defaultTTLSeconds: 300 // 5-minute default TTL
    });

    await provider.SetItem('user:123', JSON.stringify(userData), 'UserCache');
    const cached = await provider.GetItem('user:123', 'UserCache');

    Implements

    Index

    Constructors

    • Creates a new Redis local storage provider and establishes a connection.

      The constructor sets up the ioredis client with automatic reconnection, error handling, and optional logging. The client connects lazily on the first command, so construction itself does not block.

      Parameters

      • config: RedisProviderConfig = {}

        Redis connection and behavior configuration. At minimum, provide either url or options. If neither is provided, connects to localhost:6379.

      Returns RedisLocalStorageProvider

      // Connect to local Redis
      const provider = new RedisLocalStorageProvider({});

      // Connect to Azure Managed Redis with TLS
      const provider = new RedisLocalStorageProvider({
      url: 'rediss://default:ACCESS_KEY@myredis.redis.cache.windows.net:6380',
      defaultTTLSeconds: 600
      });

      // Connect to AWS ElastiCache
      const provider = new RedisLocalStorageProvider({
      options: {
      host: 'my-cluster.abc123.use1.cache.amazonaws.com',
      port: 6379,
      tls: {}
      }
      });

    Properties

    SharedAcrossProcesses: true

    true — every process configured with the same URL and key prefix shares this keyspace. See ILocalStorageProvider.SharedAcrossProcesses.

    SharesReferences: false

    false — values are JSON-serialized onto the wire and parsed back on read, so a Redis-backed cache never hands out a live reference to a caller's object. See ILocalStorageProvider.SharesReferences.

    SupportsCrossProcessPersistence: true

    true — that is the whole point of this provider: every server in the fleet reads what any other wrote. See ILocalStorageProvider.SupportsCrossProcessPersistence.

    Accessors

    • get Client(): Redis

      Returns the underlying ioredis client instance for advanced operations.

      Use with caution — direct client access bypasses key prefixing and category tracking. Prefer the ILocalStorageProvider methods for standard operations.

      For pub/sub, use PublishMessage, PublishMessageAndWait and SubscribeToChannel instead: they share the provider's subscriber connection and namespace channels with the key prefix.

      Returns Redis

      // Use for Redis-specific commands the provider does not wrap, such as streams
      const client = provider.Client;
      await client.xadd('audit-events', '*', 'type', 'login');
    • get IsConnected(): boolean

      Whether the Redis client currently has an active connection.

      Note: ioredis automatically reconnects on failure, so a false value here is usually transient. The provider continues to accept commands (they queue until reconnection succeeds or retry limit is hit).

      Returns boolean

    Methods

    • Clears all keys belonging to a specific category, together with the category's index-group sets and the category set older versions of this provider maintained.

      Keys are found with SCAN, so the operation also removes keys a peer wrote and keys whose index entry was lost.

      Parameters

      • category: string

        The category to clear. If empty, clears the "default" category.

      Returns Promise<void>

      // Clear all cached RunView results
      await provider.ClearCategory('RunViewCache');
    • ClearCategory, but reporting what happened instead of swallowing it. An administrator running mj cache clear needs to know a category did NOT go: the old path logged (with logging off, by default, in the CLI) and returned normally, so the command printed a key count and "servers will reload" for a clear that never happened.

      While Redis is unreachable the clear is recorded for the reconnect flush and reported as not done — the flush will clear it, but reporting success now would be exactly the lie this method exists to stop.

      Parameters

      • category: string

      Returns Promise<{ Error?: string; Ok: boolean }>

      Ok, plus the error when it failed.

    • Gracefully disconnects from Redis.

      Sends a QUIT command and waits for pending replies. After calling this method, the provider should not be used for further operations.

      Call this during application shutdown to ensure clean disconnection.

      Returns Promise<void>

      // During application shutdown
      process.on('SIGTERM', async () => {
      await redisProvider.Disconnect();
      process.exit(0);
      });
    • Checks if a key exists in the specified category.

      This is more efficient than GetItem() when you only need to check existence without retrieving the value (avoids transferring the value over the network).

      Parameters

      • key: string

        The key to check

      • Optionalcategory: string

        Optional category for key isolation (defaults to "default")

      Returns Promise<boolean>

      true if the key exists and has not expired

      if (await provider.Exists('view:users', 'RunViewCache')) {
      // Use cached value
      }
    • Returns all live keys belonging to a specific category.

      Walks the keyspace with SCAN, so the cost grows with the whole keyspace. Meant for administration and diagnostics; per-entity lookups on the save path use GetIndexGroupKeys.

      Parameters

      • category: string

        The category to list keys from

      Returns Promise<string[]>

      Array of original key names (without the Redis prefix/category prefix)

      const keys = await provider.GetCategoryKeys('RunViewCache');
      console.log(`${keys.length} cached views`);
    • Returns the keys written to category with IndexGroup = group that still exist.

      Members whose key has expired or been removed are dropped from the group set as a side effect, so the set shrinks back to the live keys whenever it is read. The read and the prune are one Lua script: a peer's SADD cannot land between them and be pruned unseen.

      Parameters

      • category: string

        The category the keys were written to

      • group: string

        The index group (for the RunView cache, the entity name)

      Returns Promise<string[]>

      The live keys, without the Redis prefix/category prefix

    • Retrieves a value from Redis by key and optional category.

      Redis stores values as strings — this method JSON-deserializes the stored value internally so callers see a typed object back. Returns null for missing keys, Redis unavailability, or corrupt JSON.

      Type Parameters

      • T = unknown

        Expected type of the stored value. Caller-controlled.

      Parameters

      • key: string

        The key to look up

      • Optionalcategory: string

        Optional category for key isolation (defaults to "default")

      Returns Promise<T | null>

      interface CachedEntity { ID: string; Name: string; }
      const value = await provider.GetItem<CachedEntity>('entity-metadata', 'Metadata');
      if (value) {
      console.log(value.Name); // already typed
      }
    • Batched read using Redis MGET — one command, one network round-trip, N values returned. For N keys this is ~N× faster than individual GETs which each pay full RTT.

      Inputs are deduplicated (same key requested twice → one Redis fetch, one map entry). Missing keys, corrupt JSON entries, and Redis errors all map to null for the affected key — callers see them as cache misses.

      Type Parameters

      • T = unknown

        Expected type of all stored values. Caller-controlled.

      Parameters

      • keys: string[]

        The keys to retrieve.

      • Optionalcategory: string

        Optional category for key isolation.

      Returns Promise<Map<string, T | null>>

    • Returns the remaining time-to-live (in seconds) for a key.

      Parameters

      • key: string

        The key to check

      • Optionalcategory: string

        Optional category for key isolation (defaults to "default")

      Returns Promise<number | null>

      TTL in seconds, -1 if no expiration is set, -2 if the key doesn't exist, or null if Redis is unavailable

      const ttl = await provider.GetTTL('view:users', 'RunViewCache');
      if (ttl !== null && ttl > 0) {
      console.log(`Key expires in ${ttl} seconds`);
      }
    • Registers a callback for cache change events from other servers. The callback fires whenever another server instance modifies a cached entry (via SetItem, Remove, or ClearCategory).

      Events from this server instance (identified by MJGlobal.ProcessUUID) are automatically filtered out.

      Parameters

      Returns () => void

      A function that, when called, removes this callback registration

      const unsubscribe = provider.OnCacheChanged((event) => {
      // Dispatch to LocalCacheManager for callback routing
      LocalCacheManager.Instance.DispatchCacheChange(event);
      });

      // Later, on shutdown:
      unsubscribe();
    • Registers a callback for the loss of the Redis connection.

      Fires on a transition only — not on the first connect, and not again while a reconnect is being retried. IsConnected reports the current state but not that it changed, so a consumer wanting to degrade or report health needs this rather than a poll.

      Use this in preference to reading the log: status-level logging is suppressed when GetProductionStatus() is true, so a deployed process says nothing when its cache client dies.

      Parameters

      • callback: (reason: string) => void

        Invoked with a short human-readable reason for the loss

      Returns () => void

      A function that removes this registration

      const stop = provider.OnConnectionLost(reason => health.markCacheDown(reason));
      provider.OnConnectionRestored(() => health.markCacheUp());
    • Registers a callback for the Redis connection being restored after a loss. Not fired for the first connection.

      Cache correctness after the gap is handled by the provider itself; see OnReconciliationRequired for the signal that local state had to be dropped.

      Parameters

      • callback: () => void

        Invoked when the connection comes back

      Returns () => void

      A function that removes this registration

    • Registers a callback for the provider having concluded, after a connection gap, that local cache state must be dropped.

      The standard categories are flushed by the provider itself via OnCacheChanged. Use this for state those categories do not describe — an in-process engine, a derived index, a memoized permission set — which would otherwise survive a reconnect while stale.

      Does not fire when nothing was invalidated anywhere during the gap.

      Parameters

      • callback: () => void

        Invoked when a flush is required

      Returns () => void

      A function that removes this registration

    • Pings the Redis server to verify connectivity.

      Useful for health checks and connection validation.

      Returns Promise<boolean>

      true if the server responds with PONG, false otherwise

      const healthy = await provider.Ping();
      if (!healthy) {
      console.error('Redis is unreachable');
      }
    • Publishes an arbitrary message on a named channel. Fire-and-forget: the promise is not awaited and a failure is logged rather than raised, so a caller on a hot path is never blocked or broken by the message bus. Does nothing when pub/sub is disabled.

      Use PublishMessageAndWait when the caller needs to know whether the message went out.

      The channel is namespaced with the provider's key prefix, so several applications can share one Redis without hearing each other.

      Parameters

      • channel: string

        Logical channel name (unprefixed).

      • payload: string

        Message body. Serialize before calling — this layer is shape-agnostic.

      Returns void

      If channel is the name reserved for cache invalidation.

    • Publishes a message on a named channel and waits for Redis to accept it.

      Use this instead of PublishMessage when the caller must report honestly whether the message went out — for example, telling a user whether a cancellation reached the server running their request.

      While the connection is down the publish waits for it to come back rather than failing, so a caller that needs a deadline should race it against a timeout.

      Parameters

      • channel: string

        Logical channel name (unprefixed), namespaced as in PublishMessage.

      • payload: string

        Message body. Serialize before calling — this layer is shape-agnostic.

      Returns Promise<number>

      The number of subscribers that received the message, across every process sharing the Redis — including this one, if it subscribes to the same channel. Zero means the message was published but nobody was listening.

      If pub/sub is disabled, if channel is reserved, or if Redis rejects the publish.

    • Removes a key from Redis and from its category tracking set.

      Parameters

      • key: string

        The key to remove

      • Optionalcategory: string

        Optional category for key isolation (defaults to "default")

      Returns Promise<void>

      await provider.Remove('view:users', 'RunViewCache');
      
    • Extends a lease this provider holds, without being able to extend anyone else's: the script only acts when the stored token is this provider's. A lease that has already expired and been taken by another process is NOT stolen back — the caller learns it lost its turn.

      Parameters

      • name: string
      • ttlMs: number

      Returns Promise<boolean>

      true when this process still holds the lease afterwards.

    • Stores a value in Redis under the given key and optional category.

      Redis stores values as strings — this method JSON-serializes the value internally. Callers should pass plain data (objects/arrays/primitives). Class instances will lose their prototype on retrieval; functions, Maps, Sets, and Dates have JSON's usual limitations.

      The key expires after the per-call TTL when one is given, otherwise after the configured defaultTTLSeconds. If neither is set (or the value is 0), the key persists indefinitely.

      When the options carry an IndexGroup, the key is also added to that group's Redis Set so GetIndexGroupKeys can find it. The set is kept alive at least as long as its longest-lived member. There is no category-wide set: one would grow for ever once keys expire, because expiry never removes set members. Category-wide operations use SCAN.

      Type Parameters

      • T

        Type of the value being stored. Caller-controlled.

      Parameters

      • key: string

        The key to store under

      • value: T

        The value to store (will be JSON-serialized internally)

      • Optionalcategory: string

        Optional category for key isolation (defaults to "default")

      • Optionaloptions: number | LocalStorageWriteOptions

        Expiry and index group. A bare number is accepted as the TTL in seconds.

      Returns Promise<void>

      // Store with default TTL
      await provider.SetItem('view:users', { results, maxUpdatedAt }, 'RunViewCache');

      // Store with explicit 10-minute TTL, indexed under the entity name
      await provider.SetItem('Users|…', { results }, 'RunViewCache', { TTLSeconds: 600, IndexGroup: 'Users' });
    • Starts listening for cache change events from other server instances. Creates a dedicated Redis connection for pub/sub (required by Redis protocol — a client in subscribe mode cannot execute other commands).

      Must be called explicitly after construction. No-op if enablePubSub is false in the config. Concurrent and repeated calls share one start-up; if it fails, the subscriber is discarded and the next call starts a fresh one.

      Returns Promise<void>

      const provider = new RedisLocalStorageProvider({
      url: 'redis://localhost:6379',
      enablePubSub: true
      });
      await provider.StartListening();

      // Register for change events
      provider.OnCacheChanged((event) => {
      console.log(`Key "${event.CacheKey}" changed by server ${event.SourceServerId}`);
      });
    • Registers a handler for messages on a named channel, starting the subscriber if needed.

      When pub/sub is disabled this registers nothing and returns an inert unsubscribe; check IsPubSubEnabled to tell that apart from a live subscription.

      No echo suppression happens here — this layer does not know the payload shape. A publisher that needs it must stamp its own origin on the message and check it in the handler.

      Redis does not replay messages, so anything published while the subscriber is disconnected is never delivered.

      Parameters

      Returns Promise<() => void>

      A function that removes this handler. When the last handler on a channel is removed, the provider unsubscribes from it in Redis.

      If channel is reserved, or if Redis rejects the subscribe.

    • Runs work holding an exclusive lock on key across every process sharing this keyspace.

      The lock is SET {prefix}:__lock__:{category}:{key} <token> NX PX 10000, retried every 20 ms for up to 5 s, and released only by its holder (compare-and-delete). The 10 s expiry frees a lock whose holder died; work is expected to be a short read-modify-write.

      Type Parameters

      • T

      Parameters

      • key: string
      • category: string
      • work: () => Promise<T>

      Returns Promise<T>

      when the lock is not acquired in time; work has not run