@type-cacheable/redis-adapter
Advanced tools
Comparing version 5.2.0 to 6.0.0
@@ -35,2 +35,2 @@ import { RedisClient, Callback } from 'redis'; | ||
} | ||
export declare const useAdapter: (client: RedisClient) => void; | ||
export declare const useAdapter: (client: RedisClient, asFallback?: boolean | undefined) => void; |
@@ -17,223 +17,204 @@ "use strict"; | ||
const SCALAR_KEY = ''; | ||
// When values are returned from redis, numbers can be converted to strings, so we need to store them | ||
// in a way that we can differentiate them from numbers that were intentionally stored as strings | ||
const NUMBER_IDENTIFIER = 'n'; | ||
const BOOL_IDENTIFIER = 'b'; | ||
let RedisAdapter = /** @class */ (() => { | ||
class RedisAdapter { | ||
constructor(redisClient) { | ||
class RedisAdapter { | ||
constructor(redisClient) { | ||
this.clientReady = false; | ||
this.isPingingClient = false; | ||
this.redisClient = redisClient; | ||
this.clientReady = this.redisClient.ping(); | ||
this.redisClient.on('ready', () => { | ||
this.clientReady = true; | ||
}); | ||
this.redisClient.on('error', () => { | ||
this.clientReady = false; | ||
this.isPingingClient = false; | ||
this.redisClient = redisClient; | ||
this.clientReady = this.redisClient.ping(); | ||
this.redisClient.on('ready', () => { | ||
}); | ||
this.checkIfReady(); | ||
} | ||
/** | ||
* checkIfReady will return the last received ready status of the client. | ||
* If the client isn't ready, it will ping the redis client to check if it's ready | ||
* yet. This isn't a perfect solution, because we're not waiting for the response (so as to | ||
* not add latency to the underlying method calls). I believe it's a reasonable trade-off to | ||
* have a potential cache miss rather than add latency to all decorated method calls. | ||
*/ | ||
checkIfReady() { | ||
if (!this.clientReady && !this.isPingingClient) { | ||
this.isPingingClient = true; | ||
this.redisClient.ping(() => { | ||
this.clientReady = true; | ||
this.isPingingClient = false; | ||
}); | ||
this.redisClient.on('error', () => { | ||
this.clientReady = false; | ||
}); | ||
this.checkIfReady(); | ||
} | ||
/** | ||
* checkIfReady will return the last received ready status of the client. | ||
* If the client isn't ready, it will ping the redis client to check if it's ready | ||
* yet. This isn't a perfect solution, because we're not waiting for the response (so as to | ||
* not add latency to the underlying method calls). I believe it's a reasonable trade-off to | ||
* have a potential cache miss rather than add latency to all decorated method calls. | ||
*/ | ||
checkIfReady() { | ||
if (!this.clientReady && !this.isPingingClient) { | ||
this.isPingingClient = true; | ||
this.redisClient.ping(() => { | ||
this.clientReady = true; | ||
this.isPingingClient = false; | ||
return this.clientReady; | ||
} | ||
// Redis doesn't have a standard TTL, it's at a per-key basis | ||
getClientTTL() { | ||
return 0; | ||
} | ||
get(cacheKey) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const isReady = this.checkIfReady(); | ||
if (isReady) { | ||
return new Promise((resolve, reject) => { | ||
if (cacheKey.includes(':')) { | ||
this.redisClient.hgetall(cacheKey, RedisAdapter.responseCallback(resolve, reject)); | ||
} | ||
else { | ||
this.redisClient.get(cacheKey, RedisAdapter.responseCallback(resolve, reject)); | ||
} | ||
}).then((result) => { | ||
const usableResult = core_1.parseIfRequired(result); | ||
if (usableResult && | ||
typeof usableResult === 'object' && | ||
Object.keys(usableResult).every((key) => Number.isInteger(Number(key)))) { | ||
return Object.values(usableResult); | ||
} | ||
return usableResult; | ||
}); | ||
} | ||
return this.clientReady; | ||
} | ||
// Redis doesn't have a standard TTL, it's at a per-key basis | ||
getClientTTL() { | ||
return 0; | ||
} | ||
get(cacheKey) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const isReady = this.checkIfReady(); | ||
if (isReady) { | ||
return new Promise((resolve, reject) => { | ||
if (cacheKey.includes(':')) { | ||
this.redisClient.hgetall(cacheKey, RedisAdapter.responseCallback(resolve, reject)); | ||
throw new Error('Redis client is not accepting connections.'); | ||
}); | ||
} | ||
/** | ||
* set - Sets a key equal to a value in a Redis cache | ||
* | ||
* @param cacheKey The key to store the value under | ||
* @param value The value to store | ||
* @param ttl Time to Live (how long, in seconds, the value should be cached) | ||
* | ||
* @returns {Promise} | ||
*/ | ||
set(cacheKey, value, ttl) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const isReady = this.checkIfReady(); | ||
if (isReady) { | ||
return new Promise((resolve, reject) => { | ||
if (cacheKey.includes(':')) { | ||
if (typeof value === 'object') { | ||
const args = RedisAdapter.buildSetArgumentsFromObject(value); | ||
this.redisClient.hmset(cacheKey, args, (err, result) => { | ||
if (!err) { | ||
// hmset doesn't add expiration by default, so we have to implement that here if ttl is given | ||
if (ttl) { | ||
this.redisClient.expire(cacheKey, ttl, RedisAdapter.responseCallback(resolve, reject)); | ||
return; | ||
} | ||
} | ||
RedisAdapter.responseCallback(resolve, reject)(err, result); | ||
}); | ||
} | ||
else { | ||
this.redisClient.get(cacheKey, RedisAdapter.responseCallback(resolve, reject)); | ||
this.redisClient.hmset(cacheKey, RedisAdapter.buildSetArgumentsFromObject({ [SCALAR_KEY]: JSON.stringify(value) }), (err, result) => { | ||
if (!err) { | ||
// hset doesn't add expiration by default, so we have to implement that here if ttl is given | ||
if (ttl) { | ||
this.redisClient.expire(cacheKey, ttl, RedisAdapter.responseCallback(resolve, reject)); | ||
return; | ||
} | ||
} | ||
RedisAdapter.responseCallback(resolve, reject)(err, result); | ||
}); | ||
} | ||
}).then((result) => { | ||
const usableResult = core_1.parseIfRequired(result); | ||
if (usableResult && | ||
typeof usableResult === 'object' && | ||
Object.keys(usableResult).every((key) => Number.isInteger(Number(key)))) { | ||
return Object.keys(usableResult).map((key) => core_1.parseIfRequired(usableResult[key])); | ||
} | ||
else { | ||
const usableValue = JSON.stringify(value); | ||
if (ttl) { | ||
this.redisClient.set(cacheKey, usableValue, 'EX', ttl, RedisAdapter.responseCallback(resolve, reject)); | ||
} | ||
return usableResult; | ||
}); | ||
} | ||
throw new Error('Redis client is not accepting connections.'); | ||
}); | ||
} | ||
/** | ||
* set - Sets a key equal to a value in a Redis cache | ||
* | ||
* @param cacheKey The key to store the value under | ||
* @param value The value to store | ||
* @param ttl Time to Live (how long, in seconds, the value should be cached) | ||
* | ||
* @returns {Promise} | ||
*/ | ||
set(cacheKey, value, ttl) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const isReady = this.checkIfReady(); | ||
if (isReady) { | ||
return new Promise((resolve, reject) => { | ||
if (cacheKey.includes(':')) { | ||
if (typeof value === 'object') { | ||
const args = RedisAdapter.buildSetArgumentsFromObject(value); | ||
this.redisClient.hmset(cacheKey, args, (err, result) => { | ||
if (!err) { | ||
// hmset doesn't add expiration by default, so we have to implement that here if ttl is given | ||
if (ttl) { | ||
this.redisClient.expire(cacheKey, ttl, RedisAdapter.responseCallback(resolve, reject)); | ||
return; | ||
} | ||
} | ||
RedisAdapter.responseCallback(resolve, reject)(err, result); | ||
}); | ||
} | ||
else { | ||
this.redisClient.hmset(cacheKey, RedisAdapter.buildSetArgumentsFromObject({ [SCALAR_KEY]: value }), (err, result) => { | ||
if (!err) { | ||
// hset doesn't add expiration by default, so we have to implement that here if ttl is given | ||
if (ttl) { | ||
this.redisClient.expire(cacheKey, ttl, RedisAdapter.responseCallback(resolve, reject)); | ||
return; | ||
} | ||
} | ||
RedisAdapter.responseCallback(resolve, reject)(err, result); | ||
}); | ||
} | ||
} | ||
else { | ||
const usableValue = typeof value === 'string' ? value : JSON.stringify(value); | ||
if (ttl) { | ||
this.redisClient.set(cacheKey, usableValue, 'EX', ttl, RedisAdapter.responseCallback(resolve, reject)); | ||
} | ||
else { | ||
this.redisClient.set(cacheKey, usableValue, RedisAdapter.responseCallback(resolve, reject)); | ||
} | ||
this.redisClient.set(cacheKey, usableValue, RedisAdapter.responseCallback(resolve, reject)); | ||
} | ||
}); | ||
} | ||
throw new Error('Redis client is not accepting connections.'); | ||
}); | ||
} | ||
del(keyOrKeys) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const isReady = this.checkIfReady(); | ||
if (isReady) { | ||
return new Promise((resolve, reject) => { | ||
this.redisClient.del(keyOrKeys, RedisAdapter.responseCallback(resolve, reject)); | ||
}); | ||
} | ||
throw new Error('Redis client is not accepting connections.'); | ||
}); | ||
} | ||
keys(pattern) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const isReady = this.checkIfReady(); | ||
if (isReady) { | ||
return new Promise((resolve, reject) => { | ||
this.redisClient.scan('0', 'MATCH', `*${pattern}*`, 'COUNT', '1000', RedisAdapter.responseScanCommandCallback(resolve, reject)); | ||
}); | ||
} | ||
throw new Error('Redis client is not accepting connections.'); | ||
}); | ||
} | ||
} | ||
}); | ||
} | ||
throw new Error('Redis client is not accepting connections.'); | ||
}); | ||
} | ||
RedisAdapter.buildSetArgumentsFromObject = (objectValue) => Object.keys(objectValue).reduce((accum, objectKey) => { | ||
let value = objectValue[objectKey]; | ||
switch (typeof value) { | ||
case 'object': { | ||
value = JSON.stringify(value); | ||
break; | ||
del(keyOrKeys) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const isReady = this.checkIfReady(); | ||
if (isReady) { | ||
return new Promise((resolve, reject) => { | ||
this.redisClient.del(keyOrKeys, RedisAdapter.responseCallback(resolve, reject)); | ||
}); | ||
} | ||
case 'number': { | ||
value = `${value}${NUMBER_IDENTIFIER}`; | ||
break; | ||
} | ||
case 'boolean': { | ||
value = `${value}${BOOL_IDENTIFIER}`; | ||
break; | ||
} | ||
default: | ||
break; | ||
} | ||
accum.push(objectKey, value); | ||
return accum; | ||
}, []); | ||
RedisAdapter.transformRedisResponse = (response) => { | ||
if (response && typeof response === 'object') { | ||
return Object.entries(response).reduce((accum, curr) => { | ||
const [key, value] = curr; | ||
switch (typeof value) { | ||
case 'string': { | ||
if (value.endsWith(NUMBER_IDENTIFIER) && | ||
parseFloat(value).toString() === value.substr(0, value.length - 1)) { | ||
accum[key] = parseFloat(value); | ||
break; | ||
} | ||
else if (value.endsWith(BOOL_IDENTIFIER) && | ||
(value === 'false' + BOOL_IDENTIFIER || value === 'true' + BOOL_IDENTIFIER)) { | ||
accum[key] = value === 'true' + BOOL_IDENTIFIER; | ||
break; | ||
} | ||
throw new Error('Redis client is not accepting connections.'); | ||
}); | ||
} | ||
keys(pattern) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const isReady = this.checkIfReady(); | ||
let keys = []; | ||
let cursor = '0'; | ||
if (isReady) { | ||
while (cursor) { | ||
const result = (yield new Promise((resolve, reject) => { | ||
this.redisClient.scan(cursor, 'MATCH', pattern, 'COUNT', '1000', RedisAdapter.responseScanCommandCallback(resolve, reject)); | ||
})); | ||
if (result) { | ||
// array exists at index 1 from SCAN command, cursor is at 0 | ||
cursor = cursor !== result[0] ? result[0] : null; | ||
keys = [...keys, ...result[1]]; | ||
} | ||
default: { | ||
accum[key] = value; | ||
break; | ||
else { | ||
cursor = null; | ||
} | ||
} | ||
return accum; | ||
}, {}); | ||
} | ||
return keys; | ||
} | ||
throw new Error('Redis client is not accepting connections.'); | ||
}); | ||
} | ||
} | ||
exports.RedisAdapter = RedisAdapter; | ||
RedisAdapter.buildSetArgumentsFromObject = (objectValue) => Object.keys(objectValue).reduce((accum, objectKey) => { | ||
accum.push(objectKey, JSON.stringify(objectValue[objectKey])); | ||
return accum; | ||
}, []); | ||
RedisAdapter.transformRedisResponse = (response) => { | ||
if (response && typeof response === 'object') { | ||
return Object.entries(response).reduce((accum, curr) => { | ||
const [key, value] = curr; | ||
accum[key] = JSON.parse(value); | ||
return accum; | ||
}, {}); | ||
} | ||
try { | ||
return JSON.parse(response); | ||
} | ||
catch (_a) { | ||
return response; | ||
}; | ||
RedisAdapter.responseCallback = (resolve, reject) => (err, response) => { | ||
if (err) { | ||
reject(err); | ||
} | ||
else { | ||
if (response && | ||
typeof response === 'object' && | ||
Object.keys(response).length === 1 && | ||
response[SCALAR_KEY]) { | ||
resolve(RedisAdapter.transformRedisResponse(response)[SCALAR_KEY]); | ||
return; | ||
} | ||
resolve(RedisAdapter.transformRedisResponse(response)); | ||
} | ||
}; | ||
RedisAdapter.responseScanCommandCallback = (resolve, reject) => (err, response) => { | ||
if (err) { | ||
reject(err); | ||
} | ||
else { | ||
// array exists at index '1' from SCAN command | ||
resolve(response['1']); | ||
} | ||
}; | ||
RedisAdapter.responseCallback = (resolve, reject) => (err, response) => { | ||
if (err) { | ||
reject(err); | ||
} | ||
else { | ||
if (response && | ||
typeof response === 'object' && | ||
Object.keys(response).length === 1 && | ||
response[SCALAR_KEY]) { | ||
resolve(RedisAdapter.transformRedisResponse(response)[SCALAR_KEY]); | ||
return; | ||
} | ||
}; | ||
return RedisAdapter; | ||
})(); | ||
exports.RedisAdapter = RedisAdapter; | ||
exports.useAdapter = (client) => { | ||
resolve(RedisAdapter.transformRedisResponse(response)); | ||
} | ||
}; | ||
RedisAdapter.responseScanCommandCallback = (resolve, reject) => (err, response) => { | ||
if (err) { | ||
reject(err); | ||
} | ||
else { | ||
resolve(response); | ||
return; | ||
} | ||
}; | ||
exports.useAdapter = (client, asFallback) => { | ||
const redisAdapter = new RedisAdapter(client); | ||
core_1.default.setClient(redisAdapter); | ||
if (asFallback) { | ||
core_1.default.setFallbackClient(redisAdapter); | ||
} | ||
else { | ||
core_1.default.setClient(redisAdapter); | ||
} | ||
}; |
{ | ||
"name": "@type-cacheable/redis-adapter", | ||
"version": "5.2.0", | ||
"version": "6.0.0", | ||
"description": "Adapter for using redis with type-cacheable", | ||
@@ -49,3 +49,3 @@ "main": "dist/index.js", | ||
"devDependencies": { | ||
"@types/jest": "^25.1.0", | ||
"@types/jest": "^26.0.0", | ||
"@types/redis": "^2.8.13", | ||
@@ -61,5 +61,5 @@ "jest": "^25.1.0", | ||
"dependencies": { | ||
"@type-cacheable/core": "^5.2.0" | ||
"@type-cacheable/core": "^6.0.0" | ||
}, | ||
"gitHead": "a5f3495fb4f0c2c05c2bd81900ba69ae9cbd657e" | ||
"gitHead": "cff04b6483ff5c538fc798f11fd33ca15c665a31" | ||
} |
14733
254
+ Added@type-cacheable/core@6.2.0(transitive)
+ Addedserialize-javascript@4.0.0(transitive)
- Removed@type-cacheable/core@5.2.0(transitive)
- Removedserialize-javascript@3.1.0(transitive)
Updated@type-cacheable/core@^6.0.0