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

356 lines (355 loc) 10.3 kB
// src/PowerRedis.ts import { isStrFilled, isStr, isStrBool, isArrFilled, isArr, isObj, isNum, isNumP, isNumPZ, isBool, isFunc, jsonDecode, jsonEncode, formatToTrim, formatToBool } from "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 = formatToTrim(p); if (!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 = formatToTrim(p); if (!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 (!isStr(value)) { return null; } if (!isStrFilled(value)) { return ""; } try { const parsed = jsonDecode(value); if (isNum(parsed) || isBool(parsed) || isStr(parsed) || isArr(parsed) || isObj(parsed)) { return parsed; } } catch { } if (isStrBool(value)) { return formatToBool(value); } return value; } toPayload(value) { if (isArr(value) || isObj(value)) { return jsonEncode(value); } return String(value ?? ""); } async lpopCountCompat(key, count) { const cli = this.redis; if (isFunc(cli.lpop)) { try { const res = await cli.lpop(key, count); if (isArr(res)) { return Array.from(res); } if (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 (!isArrFilled(execRes)) { return []; } const firstTuple = execRes[0]; const first = firstTuple?.[1]; if (isArr(first)) { return Array.from(first); } return []; } async keys(pattern, limit = 100, scanSize = 1e3) { if (!isStrFilled(pattern)) { throw new Error("Pattern format error."); } if (!isNumP(limit)) { throw new Error("Limit format error."); } if (!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 (!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 (!isNumP(chunkSize)) { throw new Error('Property "chunkSize" format error.'); } const keys = await this.keys(pattern, limit, scanSize); const result = {}; if (!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 (!isStrFilled(key)) { throw new Error("Key format error."); } if (!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 (!isStrFilled(key)) { throw new Error("Key format error."); } if (!isNumP(limit)) { throw new Error("Limit format error."); } if (remove) { while (true) { const items = await this.lpopCountCompat(key, limit); if (!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 (!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 (!isStrFilled(key)) { throw new Error("Key format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } return 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 (!isArrFilled(values)) { throw new Error("Payload format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } if (!isNumP(ttlSec)) { const kv = []; for (const { key, value } of values) { if (!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 (!isStrFilled(key)) { throw new Error("Key format error."); } tx.set(key, this.toPayload(value), "EX", ttlSec); } const res = await tx.exec(); if (!isArrFilled(res)) { return 0; } let ok = 0; for (const item of res) { if (!isArrFilled(item)) { continue; } const [err, reply] = item; if (!err && reply === "OK") { ok++; } } return ok; } async pushOne(key, value, ttlSec) { if (!isStrFilled(key)) { throw new Error("Key format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } if (isNumP(ttlSec)) { const tx = this.redis.multi(); tx.rpush(key, this.toPayload(value)); tx.expire(key, ttlSec); const res = await tx.exec(); if (isArrFilled(res)) { const [[err1, pushReply], [err2, expireReply]] = res; if (!err1 && !err2 && isNumPZ(Number(pushReply)) && isNumPZ(Number(expireReply))) { return Number(pushReply); } } return 0; } return await this.redis.rpush(key, this.toPayload(value)); } async pushMany(key, values, ttlSec) { if (!isStrFilled(key)) { throw new Error("Key format error."); } if (!isArrFilled(values)) { throw new Error("Payload format error."); } if (!this.checkConnection()) { throw new Error("Redis connection error."); } if (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 (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 && isNumPZ(Number(pushReply)) && 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 (isArrFilled(keys)) { total += keys.length; for (let i = 0; i < keys.length; i += size) { const chunk = keys.slice(i, i + size); 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 (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); } }; export { PowerRedis };