forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathredis-accessor.js
137 lines (110 loc) · 3.85 KB
/
redis-accessor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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,
// Only add this configuration for TLS-enabled REDIS_URL values.
// Otherwise, it breaks for local Redis instances without TLS enabled.
...REDIS_URL.startsWith('rediss://') && {
tls: {
// Required for production Heroku Redis
rejectUnauthorized: false
}
}
})
: 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