Home > Web Front-end > JS Tutorial > Building a High-Performance Cache Manager in Node.js with TypeScript and ioredis

Building a High-Performance Cache Manager in Node.js with TypeScript and ioredis

Patricia Arquette
Release: 2024-10-30 09:43:27
Original
949 people have browsed it

Building a High-Performance Cache Manager in Node.js with TypeScript and ioredis

Boost your Node.js app performance with a versatile, easy-to-use cache manager built on ioredis. Simplify caching, optimize efficiency, and streamline operations.

I've developed a class built on top of ioredis for my own needs, focusing on ease of use and performance. It includes TypeScript support and aims for straightforward usage and efficient operation. It can still be improved and optimized further, with possibilities for dynamic database usage and more detailed error handling. I'd like to share it with you, and I'd appreciate any feedback or suggestions for enhancements.

import Redis, { type RedisOptions } from 'ioredis';

interface CacheConfig {
    defaultTTL?: number;
}

export class cacheManager {
    private static instance: cacheManager;
    private static redisClient: Redis;
    private currentKey: string | null;
    private defaultTTL: number;

    private static readonly DEFAULT_TTL = 3600;

    private constructor(config?: CacheConfig) {
        const redisConfig: RedisOptions = {
            db: 2,
            retryStrategy: (times: number) => {
                const delay = Math.min(times * 50, 2000);
                return delay;
            },
            lazyConnect: true,
            maxRetriesPerRequest: 3,
            enableReadyCheck: true,
            autoResubscribe: true,
            autoResendUnfulfilledCommands: true,
            reconnectOnError: (err: Error) => {
                const targetError = 'READONLY';
                return err.message.includes(targetError);
            },
        };

        if (!cacheManager.redisClient) {
            cacheManager.redisClient = new Redis(redisConfig);

            cacheManager.redisClient.on('error', (error: Error) => {
                console.error('Redis Client Error:', error);
            });

            cacheManager.redisClient.on('connect', () => {
                console.debug('Redis Client Connected');
            });

            cacheManager.redisClient.on('ready', () => {
                console.debug('Redis Client Ready');
            });
        }

        this.currentKey = null;
        this.defaultTTL = config?.defaultTTL ?? cacheManager.DEFAULT_TTL;
    }

    public static getInstance(config?: CacheConfig): cacheManager {
        if (!cacheManager.instance) {
            cacheManager.instance = new cacheManager(config);
        }
        return cacheManager.instance;
    }

    public key(key: string): cacheManager {
        this.validateKey(key);
        this.currentKey = key;
        return this;
    }

    private validateKey(key: string): void {
        if (key.length > 100) throw new Error('Key too long');
        if (!/^[\w:-]+$/.test(key)) throw new Error('Invalid key format');
    }

    public async getValue<T>(): Promise<T | null> {
        try {
            if (!this.currentKey) {
                throw new Error('Key is required');
            }
            const value = await cacheManager.redisClient.get(this.currentKey);
            return value ? JSON.parse(value) : null;
        } catch (error) {
            console.error('getValue Error:', error);
            return null;
        }
    }

    public async getMultiple<T>(keys: string[]): Promise<Record<string, T | null>> {
        try {
            const pipeline = cacheManager.redisClient.pipeline();
            for (const key of keys) {
                pipeline.get(key);
            }

            type PipelineResult = [Error | null, string | null][] | null;
            const results = (await pipeline.exec()) as PipelineResult;
            const output: Record<string, T | null> = {};

            if (!results) {
                return output;
            }

            keys.forEach((key, index) => {
                const result = results[index];
                if (result) {
                    const [err, value] = result;
                    if (!err && value) {
                        try {
                            output[key] = JSON.parse(value);
                        } catch {
                            output[key] = null;
                        }
                    } else {
                        output[key] = null;
                    }
                } else {
                    output[key] = null;
                }
            });

            return output;
        } catch (error) {
            console.error('getMultiple Error:', error);
            return {};
        }
    }

    public async setValue<T>(value: T, ttl: number = this.defaultTTL): Promise<boolean> {
        try {
            if (!this.currentKey) {
                throw new Error('Key is required');
            }
            const stringValue = JSON.stringify(value);

            if (ttl) {
                await cacheManager.redisClient.setex(this.currentKey, ttl, stringValue);
            } else {
                await cacheManager.redisClient.set(this.currentKey, stringValue);
            }

            return true;
        } catch (error) {
            console.error('setValue Error:', error);
            return false;
        }
    }

    public async setBulkValue<T>(keyValuePairs: Record<string, T>, ttl: number = this.defaultTTL, batchSize = 1000): Promise<boolean> {
        try {
            const entries = Object.entries(keyValuePairs);
            for (let i = 0; i < entries.length; i += batchSize) {
                const batch = entries.slice(i, i + batchSize);
                const pipeline = cacheManager.redisClient.pipeline();

                for (const [key, value] of batch) {
                    const stringValue = JSON.stringify(value);
                    if (ttl) {
                        pipeline.setex(key, ttl, stringValue);
                    } else {
                        pipeline.set(key, stringValue);
                    }
                }

                await pipeline.exec();
            }
            return true;
        } catch (error) {
            console.error('setBulkValue Error:', error);
            return false;
        }
    }

    public async getOrSetValue<T>(fallbackFn: () => Promise<T>, ttl: number = this.defaultTTL): Promise<T | null> {
        try {
            if (!this.currentKey) {
                throw new Error('Key is required');
            }

            const cachedValue = await this.getValue<T>();

            if (cachedValue !== null) {
                return cachedValue;
            }

            const value = await fallbackFn();
            await this.setValue(value, ttl);
            return value;
        } catch (error) {
            console.error('getOrSetValue Error:', error);
            return null;
        }
    }

    public async delete(): Promise<boolean> {
        try {
            if (!this.currentKey) {
                throw new Error('Key is required');
            }
            await cacheManager.redisClient.del(this.currentKey);
            return true;
        } catch (error) {
            console.error('delete Error:', error);
            return false;
        }
    }

    public async exists(): Promise<boolean> {
        try {
            if (!this.currentKey) {
                throw new Error('Key is required');
            }
            return (await cacheManager.redisClient.exists(this.currentKey)) === 1;
        } catch (error) {
            console.error('exists Error:', error);
            return false;
        }
    }

    public async getTTL(): Promise<number> {
        try {
            if (!this.currentKey) {
                throw new Error('Key is required');
            }
            return await cacheManager.redisClient.ttl(this.currentKey);
        } catch (error) {
            console.error('getTTL Error:', error);
            return -1;
        }
    }

    public static async disconnect(): Promise<void> {
        if (cacheManager.redisClient) {
            await cacheManager.redisClient.quit();
        }
    }
}

Copy after login

The above is the detailed content of Building a High-Performance Cache Manager in Node.js with TypeScript and ioredis. For more information, please follow other related articles on the PHP Chinese website!

source:dev.to
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template