docs/lib/redis-accessor.js

126 строки
3.5 KiB
JavaScript

const Redis = require('ioredis')
const InMemoryRedis = require('ioredis-mock')
const { CI, NODE_ENV, REDIS_URL, REDIS_MAX_DB } = process.env
// Do not use real a Redis client for CI, tests, or if the REDIS_URL is not provided
const useRealRedis = !CI && NODE_ENV !== 'test' && !!REDIS_URL
// By default, every Redis instance supports database numbers 0 - 15
const redisMaxDb = REDIS_MAX_DB || 15
// Enable better stack traces in non-production environments
const redisBaseOptions = {
showFriendlyErrorStack: NODE_ENV !== 'production'
}
class RedisAccessor {
constructor ({ databaseNumber = 0, prefix = null, allowSetFailures = false } = {}) {
if (!Number.isInteger(databaseNumber) || databaseNumber < 0 || databaseNumber > redisMaxDb) {
throw new TypeError(
`Redis database number must be an integer between 0 and ${redisMaxDb} but was: ${JSON.stringify(databaseNumber)}`
)
}
const redisClient = useRealRedis
? new Redis(REDIS_URL, { ...redisBaseOptions, db: databaseNumber })
: new InMemoryRedis()
this._client = redisClient
this._prefix = prefix ? prefix.replace(/:+$/, '') + ':' : ''
// Allow for graceful failures if a Redis SET operation fails?
this._allowSetFailures = allowSetFailures === true
}
/** @private */
prefix (key) {
if (typeof key !== 'string' || !key) {
throw new TypeError(`Key must be a non-empty string but was: ${JSON.stringify(key)}`)
}
return this._prefix + key
}
static translateSetArguments (options = {}) {
const setArgs = []
const defaults = {
newOnly: false,
existingOnly: false,
expireIn: null, // No expiration
rollingExpiration: true
}
const opts = { ...defaults, ...options }
if (opts.newOnly === true) {
if (opts.existingOnly === true) {
throw new TypeError('Misconfiguration: entry cannot be both new and existing')
}
setArgs.push('NX')
} else if (opts.existingOnly === true) {
setArgs.push('XX')
}
if (Number.isFinite(opts.expireIn)) {
const ttl = Math.round(opts.expireIn)
if (ttl < 1) {
throw new TypeError('Misconfiguration: cannot set a TTL of less than 1 millisecond')
}
setArgs.push('PX')
setArgs.push(ttl)
}
// otherwise there is no expiration
if (opts.rollingExpiration === false) {
if (opts.newOnly === true) {
throw new TypeError('Misconfiguration: cannot keep an existing TTL on a new entry')
}
setArgs.push('KEEPTTL')
}
return setArgs
}
async set (key, value, options = {}) {
const fullKey = this.prefix(key)
if (typeof value !== 'string' || !value) {
throw new TypeError(`Value must be a non-empty string but was: ${JSON.stringify(value)}`)
}
// Handle optional arguments
const setArgs = this.constructor.translateSetArguments(options)
try {
const result = await this._client.set(fullKey, value, ...setArgs)
return result === 'OK'
} catch (err) {
const errorText = `Failed to set value in Redis.
Key: ${fullKey}
Error: ${err.message}`
if (this._allowSetFailures === true) {
// Allow for graceful failure
console.error(errorText)
return false
}
throw new Error(errorText)
}
}
async get (key) {
const value = await this._client.get(this.prefix(key))
return value
}
async exists (key) {
const result = await this._client.exists(this.prefix(key))
return result === 1
}
}
module.exports = RedisAccessor