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
JavaScript
// 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
};