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
JavaScript
"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
});