141 lines
3.3 KiB
JavaScript
141 lines
3.3 KiB
JavaScript
import { createClient } from 'redis';
|
|
import { config } from './config.js';
|
|
|
|
class InMemoryRedisClient {
|
|
constructor() {
|
|
this.mode = 'memory';
|
|
this._strings = new Map();
|
|
this._sortedSets = new Map();
|
|
}
|
|
|
|
_now() {
|
|
return Date.now();
|
|
}
|
|
|
|
_purgeExpiredStrings() {
|
|
const now = this._now();
|
|
for (const [key, entry] of this._strings.entries()) {
|
|
if (entry.expiresAt !== null && entry.expiresAt <= now) {
|
|
this._strings.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
_getStringEntry(key) {
|
|
this._purgeExpiredStrings();
|
|
return this._strings.get(key) ?? null;
|
|
}
|
|
|
|
_getSortedSet(key) {
|
|
let set = this._sortedSets.get(key);
|
|
if (!set) {
|
|
set = new Map();
|
|
this._sortedSets.set(key, set);
|
|
}
|
|
return set;
|
|
}
|
|
|
|
async connect() {
|
|
return this;
|
|
}
|
|
|
|
on() {
|
|
return this;
|
|
}
|
|
|
|
async zAdd(key, entries) {
|
|
const set = this._getSortedSet(key);
|
|
for (const entry of entries) {
|
|
set.set(String(entry.value), Number(entry.score));
|
|
}
|
|
}
|
|
|
|
async zCard(key) {
|
|
return this._getSortedSet(key).size;
|
|
}
|
|
|
|
async zRem(key, ...values) {
|
|
const set = this._getSortedSet(key);
|
|
let removed = 0;
|
|
for (const value of values) {
|
|
if (set.delete(String(value))) {
|
|
removed += 1;
|
|
}
|
|
}
|
|
return removed;
|
|
}
|
|
|
|
async zRange(key, start, stop) {
|
|
const items = Array.from(this._getSortedSet(key).entries())
|
|
.sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0]))
|
|
.map(([value]) => value);
|
|
|
|
const normalizedStop = stop < 0 ? items.length + stop : stop;
|
|
return items.slice(start, normalizedStop + 1);
|
|
}
|
|
|
|
async set(key, value, options = {}) {
|
|
const existing = this._getStringEntry(key);
|
|
if (options.NX && existing) {
|
|
return null;
|
|
}
|
|
|
|
let expiresAt = null;
|
|
if (typeof options.PX === 'number') {
|
|
expiresAt = this._now() + options.PX;
|
|
} else if (typeof options.EX === 'number') {
|
|
expiresAt = this._now() + (options.EX * 1000);
|
|
}
|
|
|
|
this._strings.set(key, { value: String(value), expiresAt });
|
|
return 'OK';
|
|
}
|
|
|
|
async get(key) {
|
|
const entry = this._getStringEntry(key);
|
|
return entry ? entry.value : null;
|
|
}
|
|
|
|
async del(key) {
|
|
this._purgeExpiredStrings();
|
|
const existed = this._strings.delete(key);
|
|
return existed ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
export async function createRedis() {
|
|
const client = createClient({
|
|
url: config.redisUrl,
|
|
socket: {
|
|
connectTimeout: 3000,
|
|
reconnectStrategy: false,
|
|
},
|
|
});
|
|
client.on('error', (err) => {
|
|
console.error('[redis] error', err);
|
|
});
|
|
|
|
try {
|
|
await client.connect();
|
|
client.mode = 'redis';
|
|
console.log('[redis] connected');
|
|
return client;
|
|
} catch (error) {
|
|
try {
|
|
if (typeof client.disconnect === 'function') {
|
|
await client.disconnect();
|
|
} else if (typeof client.quit === 'function') {
|
|
await client.quit();
|
|
}
|
|
} catch {
|
|
// Ignore cleanup errors and continue with in-memory fallback.
|
|
}
|
|
console.warn('[redis] unavailable, using in-memory fallback:', error instanceof Error ? error.message : String(error));
|
|
return new InMemoryRedisClient();
|
|
}
|
|
}
|
|
|
|
export function key(...parts) {
|
|
return config.redisKeyPrefix + parts.join('');
|
|
}
|