UNPKG

power-redis

Version:

Production-grade Redis abstraction for Node.js with strict key formatting, safe JSON serialization, advanced list/queue operations, SCAN-based pattern tools, TTL helpers, batch UNLINK deletion, and Redis Streams support. Perfect for high-load microservice

367 lines (364 loc) 12.5 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { PowerRedis: () => PowerRedis }); module.exports = __toCommonJS(index_exports); // src/PowerRedis.ts var import_full_utils = require("full-utils"); var PowerRedis = class { constructor() { this.isStrictCheckConnection = ["true", "on", "yes", "y", "1"].includes(String(process.env.REDIS_STRICT_CHECK_CONNECTION ?? "").trim().toLowerCase()); } checkConnection() { return !!this.redis && (this.redis.status === "ready" || (this.isStrictCheckConnection ? false : this.redis.status === "connecting" || this.redis.status === "reconnecting")); } toPatternString(...parts) { for (const p of parts) { const s = (0, import_full_utils.formatToTrim)(p); if (!(0, import_full_utils.isStrFilled)(s) || s.includes(":") || /\s/.test(s)) { throw new Error(`Pattern segment invalid (no ":", spaces): "${s}"`); } } return parts.join(":"); } toKeyString(...parts) { for (const p of parts) { const s = (0, import_full_utils.formatToTrim)(p); if (!(0, import_full_utils.isStrFilled)(s) || s.includes(":") || /[\*\?\[\]\s]/.test(s)) { throw new Error(`Key segment is invalid (no ":", spaces or glob chars * ? [ ] allowed): "${s}"`); } } return parts.join(":"); } fromKeyString(key) { return key.split(":").filter(Boolean); } fromPayload(value) { if (!(0, import_full_utils.isStr)(value)) { return null; } if (!(0, import_full_utils.isStrFilled)(value)) { return ""; } try { const parsed = (0, import_full_utils.jsonDecode)(value); if ((0, import_full_utils.isNum)(parsed) || (0, import_full_utils.isBool)(parsed) || (0, import_full_utils.isStr)(parsed) || (0, import_full_utils.isArr)(parsed) || (0, import_full_utils.isObj)(parsed)) { return parsed; } } catch { } if ((0, import_full_utils.isStrBool)(value)) { return (0, import_full_utils.formatToBool)(value); } return value; } toPayload(value) { if ((0, import_full_utils.isArr)(value) || (0, import_full_utils.isObj)(value)) { return (0, import_full_utils.jsonEncode)(value); } return String(value ?? ""); } async lpopCountCompat(key, count) { const cli = this.redis; if ((0, import_full_utils.isFunc)(cli.lpop)) { try { const res = await cli.lpop(key, count); if ((0, import_full_utils.isArr)(res)) { return Array.from(res); } if ((0, import_full_utils.isStr)(res)) { return [res]; } } catch { } } const tx = this.redis.multi(); tx.lrange(key, 0, count - 1); tx.ltrim(key, count, -1); const execRes = await tx.exec(); if (!(0, import_full_utils.isArrFilled)(execRes)) { return []; } const firstTuple = execRes[0]; const first = firstTuple?.[1]; if ((0, import_full_utils.isArr)(first)) { return Array.from(first); } return []; } async keys(pattern, limit = 100, scanSize = 1e3) { if (!(0, import_full_utils.isStrFilled)(pattern)) { throw new Error("Pattern format error."); } if (!(0, import_full_utils.isNumP)(limit)) { throw new Error("Limit format error."); } if (!(0, import_full_utils.isNumP)(scanSize)) { throw new Error("Size format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } const keys = /* @__PURE__ */ new Set(); let cursor = "0"; do { const [nextCursor, found] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", scanSize); cursor = nextCursor; for (const k of found) { if (!keys.has(k)) { keys.add(k); if (keys.size >= limit) { return Array.from(keys); } } } } while (cursor !== "0"); return Array.from(keys); } async getOne(key) { if (!(0, import_full_utils.isStrFilled)(key)) { throw new Error("Key format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } return this.fromPayload(await this.redis.get(key)); } async getMany(pattern, limit = 100, scanSize = 1e3, chunkSize = 1e3) { if (!(0, import_full_utils.isNumP)(chunkSize)) { throw new Error('Property "chunkSize" format error.'); } const keys = await this.keys(pattern, limit, scanSize); const result = {}; if (!(0, import_full_utils.isArrFilled)(keys)) { return result; } for (let i = 0; i < keys.length; i += chunkSize) { const chunk = keys.slice(i, i + chunkSize); const values = await this.redis.mget(...chunk); for (let j = 0; j < chunk.length; j++) { result[chunk[j]] = this.fromPayload(values[j] ?? null); } } return result; } async getList(key, limit = 100, remove = false) { if (!(0, import_full_utils.isStrFilled)(key)) { throw new Error("Key format error."); } if (!(0, import_full_utils.isNumP)(limit)) { throw new Error("Limit format error."); } const result = []; for await (const chunk of this.getListIterator(key, limit, remove)) { result.push(...chunk); } return result; } async *getListIterator(key, limit = 100, remove = false) { if (!this.checkConnection()) { throw new Error("Redis connection error."); } if (!(0, import_full_utils.isStrFilled)(key)) { throw new Error("Key format error."); } if (!(0, import_full_utils.isNumP)(limit)) { throw new Error("Limit format error."); } if (remove) { while (true) { const items = await this.lpopCountCompat(key, limit); if (!(0, import_full_utils.isArr)(items) || items.length === 0) { break; } yield items.map((item) => this.fromPayload(item)); if (items.length < limit) { break; } } return; } const n = await this.redis.llen(key); if (!(0, import_full_utils.isNumP)(n)) { return; } let start = 0; while (start < n) { const stop = Math.min(start + limit - 1, n - 1); const chunk = await this.redis.lrange(key, start, stop); if (chunk.length === 0) { start += limit; continue; } yield chunk.map((item) => this.fromPayload(item)); start += limit; } } async setOne(key, value, ttlSec) { if (!(0, import_full_utils.isStrFilled)(key)) { throw new Error("Key format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } return (0, import_full_utils.isNumP)(ttlSec) ? await this.redis.set(key, this.toPayload(value), "EX", ttlSec) : await this.redis.set(key, this.toPayload(value)); } async setMany(values, ttlSec) { if (!(0, import_full_utils.isArrFilled)(values)) { throw new Error("Payload format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } if (!(0, import_full_utils.isNumP)(ttlSec)) { const kv = []; for (const { key, value } of values) { if (!(0, import_full_utils.isStrFilled)(key)) { throw new Error("Key format error."); } kv.push(key, this.toPayload(value)); } const res2 = await this.redis.mset(...kv); return res2 === "OK" ? values.length : 0; } const tx = this.redis.multi(); for (const { key, value } of values) { if (!(0, import_full_utils.isStrFilled)(key)) { throw new Error("Key format error."); } tx.set(key, this.toPayload(value), "EX", ttlSec); } const res = await tx.exec(); if (!(0, import_full_utils.isArrFilled)(res)) { return 0; } let ok = 0; for (const item of res) { if (!(0, import_full_utils.isArrFilled)(item)) { continue; } const [err, reply] = item; if (!err && reply === "OK") { ok++; } } return ok; } async pushOne(key, value, ttlSec) { if (!(0, import_full_utils.isStrFilled)(key)) { throw new Error("Key format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } if ((0, import_full_utils.isNumP)(ttlSec)) { const tx = this.redis.multi(); tx.rpush(key, this.toPayload(value)); tx.expire(key, ttlSec); const res = await tx.exec(); if ((0, import_full_utils.isArrFilled)(res)) { const [[err1, pushReply], [err2, expireReply]] = res; if (!err1 && !err2 && (0, import_full_utils.isNumPZ)(Number(pushReply)) && (0, import_full_utils.isNumPZ)(Number(expireReply))) { return Number(pushReply); } } return 0; } return await this.redis.rpush(key, this.toPayload(value)); } async pushMany(key, values, ttlSec) { if (!(0, import_full_utils.isStrFilled)(key)) { throw new Error("Key format error."); } if (!(0, import_full_utils.isArrFilled)(values)) { throw new Error("Payload format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } if ((0, import_full_utils.isNumP)(ttlSec)) { const tx = this.redis.multi(); tx.rpush(key, ...values.map((value) => this.toPayload(value))); tx.expire(key, ttlSec); const res = await tx.exec(); if ((0, import_full_utils.isArrFilled)(res)) { const rpushRes = res[0]; const expireRes = res[1]; const [err1, pushReply] = rpushRes ?? [new Error("rpush missing"), 0]; const [err2, expireReply] = expireRes ?? [new Error("expire missing"), 0]; if (!err1 && !err2 && (0, import_full_utils.isNumPZ)(Number(pushReply)) && (0, import_full_utils.isNumPZ)(Number(expireReply))) { return Number(pushReply); } } return 0; } return await this.redis.rpush(key, ...values.map((value) => this.toPayload(value))); } async dropMany(pattern, size = 1e3) { if (!this.checkConnection()) { throw new Error("Redis connection error."); } try { let cursor = "0", total = 0; do { const [next, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", size); cursor = next; if ((0, import_full_utils.isArrFilled)(keys)) { total += keys.length; for (let i = 0; i < keys.length; i += size) { const chunk = keys.slice(i, i + size); (0, import_full_utils.isFunc)(this.redis.unlink) ? await this.redis.unlink(...chunk) : await this.redis.del(...chunk); } } } while (cursor !== "0"); return total; } catch (err) { } throw new Error("Redis drop many error."); } async incr(key, ttl) { const result = await this.redis.incr(key); if ((0, import_full_utils.isNumP)(ttl)) { await this.redis.pexpire(key, ttl); } return result; } async expire(key, ttl) { return await this.redis.expire(key, ttl); } async script(subcommand, script) { return await this.redis.script("LOAD", script); } async xgroup(script, stream, group, from, mkstream) { await this.redis.xgroup(script, stream, group, from, mkstream); } async xreadgroup(groupKey, group, consumer, blockKey, block, countKey, count, streamKey, stream, condition) { return await this.redis.xreadgroup(groupKey, group, consumer, blockKey, block, countKey, count, streamKey, stream, condition); } async pttl(key) { return await this.redis.pttl(key); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { PowerRedis });