diff --git a/.eslintrc b/.eslintrc index 2e2b313..9a6c0e9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,12 +5,8 @@ "node": true, "jest": true }, - "parserOptions": { - "ecmaVersion": 2018 - }, "rules": { - "no-multi-assign": 1, - "func-names": 1, - "no-underscore-dangle": 1 + "arrow-parens": ["error", "as-needed"], + "no-underscore-dangle": 0 } } diff --git a/.prettierrc b/.prettierrc index c50384f..a998616 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,5 +2,6 @@ "trailingComma": "none", "tabWidth": 2, "semi": false, - "singleQuote": true + "singleQuote": true, + "arrowParens": "avoid" } diff --git a/docs/redisCache.js.html b/docs/redisCache.js.html index 2e3d8a6..a081a0b 100644 --- a/docs/redisCache.js.html +++ b/docs/redisCache.js.html @@ -113,7 +113,7 @@

redisCache.js

let options = {} let store = null -const getNumber = num => !Number.isNaN(num) && num >= 0 ? num : null +const getNumber = (num) => (!Number.isNaN(num) && num >= 0 ? num : null) /** * @param {object} options @@ -182,7 +182,7 @@

redisCache.js

* Sets the ttlInSeconds * @returns {number} ttl */ -exports.setTtlInSeconds = ttl => { +exports.setTtlInSeconds = (ttl) => { options.ttlInSeconds = getNumber(ttl) return options.ttlInSeconds } @@ -216,7 +216,7 @@

redisCache.js

* @param {string} key - key for the value stored * @returns {any} value or null when the key is missing */ -exports.get = key => this.getStore().get(key) +exports.get = (key) => this.getStore().get(key) /** * Returns all keys matching pattern @@ -247,9 +247,7 @@

redisCache.js

* @property {number} opts.ttlInSeconds - time to live in seconds * @returns {string} 'OK' if successful */ -exports.wrap = async (key, fn, { - ttlInSeconds -} = {}) => { +exports.wrap = async (key, fn, { ttlInSeconds } = {}) => { const ttl = getNumber(ttlInSeconds) || options.ttlInSeconds if (ttl && ttl === 0) { diff --git a/lib/redisCache.js b/lib/redisCache.js index 62b7bd6..07273f3 100644 --- a/lib/redisCache.js +++ b/lib/redisCache.js @@ -8,7 +8,7 @@ const NotInitialisedError = require('./NotInitialisedError') let options = {} let store = null -const getNumber = (num) => (!Number.isNaN(num) && num >= 0 ? num : null) +const getNumber = num => (!Number.isNaN(num) && num >= 0 ? num : 0) /** * @param {object} options @@ -32,7 +32,7 @@ exports.init = ({ redisOptions, poolOptions, logger: createLogger(logger), - ttlInSeconds + ttlInSeconds: getNumber(ttlInSeconds) } store = new RedisStore(options) @@ -77,7 +77,7 @@ exports.getTtlInSeconds = () => options.ttlInSeconds * Sets the ttlInSeconds * @returns {number} ttl */ -exports.setTtlInSeconds = (ttl) => { +exports.setTtlInSeconds = ttl => { options.ttlInSeconds = getNumber(ttl) return options.ttlInSeconds } @@ -111,7 +111,7 @@ exports.getset = async (key, value, ttlInSeconds) => { * @param {string} key - key for the value stored * @returns {any} value or null when the key is missing */ -exports.get = (key) => this.getStore().get(key) +exports.get = key => this.getStore().get(key) /** * Returns all keys matching pattern @@ -145,7 +145,7 @@ exports.deleteAll = (pattern = '*') => this.getStore().deleteAll(pattern) exports.wrap = async (key, fn, { ttlInSeconds } = {}) => { const ttl = getNumber(ttlInSeconds) || options.ttlInSeconds - if (ttl && ttl === 0) { + if (ttl === 0) { debug(`Not caching, invalid ttl: ${ttlInSeconds}`) return fn() } diff --git a/lib/redisCache.test.js b/lib/redisCache.test.js index 79c90b3..b583e43 100644 --- a/lib/redisCache.test.js +++ b/lib/redisCache.test.js @@ -8,6 +8,7 @@ const { getPoolOptions, getRedisOptions, getset, + getStore, getTtlInSeconds, keys, set, @@ -22,17 +23,9 @@ describe('redisCache', () => { host: process.env.REDIS_HOST || '127.0.0.1', auth_pass: process.env.REDIS_AUTH } - const poolOptions = { - min: 2, - max: 4 - } + const poolOptions = { min: 2, max: 4 } const ttlInSeconds = 10 - const options = { - name, - redisOptions, - poolOptions, - ttlInSeconds - } + const options = { name, redisOptions, poolOptions, ttlInSeconds } init(options) const key = 'chuck-norris' @@ -69,6 +62,12 @@ describe('redisCache', () => { }) }) + describe('getStore', () => { + test('returns store', () => { + expect(getStore()).not.toBeNull() + }) + }) + describe('status', () => { test('get store stats', () => { const { name: statusName, size, available, pending } = status() @@ -126,6 +125,10 @@ describe('redisCache', () => { beforeAll(() => deleteAll()) + afterEach(() => { + setTtlInSeconds(ttlInSeconds) + }) + test("set if key doesn't exist", async () => { const localKey = genRandomStr() @@ -146,6 +149,7 @@ describe('redisCache', () => { test('do nothing when ttlInSeconds=0', async () => { const localKey = genRandomStr() + setTtlInSeconds(0) const result = await wrap(localKey, fn, { ttlInSeconds: 0 @@ -154,8 +158,9 @@ describe('redisCache', () => { expect(result).toBe(newValue) }) - test('do nothing when ttlInSeconds=0', async () => { + test('do nothing when ttlInSeconds < 0', async () => { const localKey = genRandomStr() + setTtlInSeconds(0) const result = await wrap(localKey, fn, { ttlInSeconds: -1 @@ -166,6 +171,7 @@ describe('redisCache', () => { test('do nothing when ttlInSeconds is invalid', async () => { const localKey = genRandomStr() + setTtlInSeconds('NOT_NUMBER') const result = await wrap(localKey, fn, { ttlInSeconds: 'NOT_NUMBER' @@ -176,14 +182,11 @@ describe('redisCache', () => { }) describe('keys', () => { - const keyValues = { - key1: 'value1', - 'test:key2': 'value2' - } + const keyValues = { key1: 'value1', 'test:key2': 'value2' } beforeAll(() => deleteAll()) beforeEach(() => - Promise.all(Object.keys(keyValues).map((k) => set(k, keyValues[k]))) + Promise.all(Object.keys(keyValues).map(k => set(k, keyValues[k]))) ) test('return all the keys', async () => @@ -203,14 +206,11 @@ describe('redisCache', () => { }) describe('del', () => { - const keyValues = { - key1: 'value1', - key2: 'value2' - } + const keyValues = { key1: 'value1', key2: 'value2' } beforeEach(async () => { await deleteAll() - await Promise.all(Object.keys(keyValues).map((k) => set(k, keyValues[k]))) + await Promise.all(Object.keys(keyValues).map(k => set(k, keyValues[k]))) }) test('delete keys array', async () => { @@ -230,13 +230,10 @@ describe('redisCache', () => { }) describe('deleteAll', () => { - const keyValues = { - key1: 'value1', - key2: 'value2' - } + const keyValues = { key1: 'value1', key2: 'value2' } beforeEach(() => - Promise.all(Object.keys(keyValues).map((k) => set(k, keyValues[k]))) + Promise.all(Object.keys(keyValues).map(k => set(k, keyValues[k]))) ) test('delete all the keys', async () => { diff --git a/lib/redisConnectionPool.js b/lib/redisConnectionPool.js index c371962..e06b93a 100644 --- a/lib/redisConnectionPool.js +++ b/lib/redisConnectionPool.js @@ -1,4 +1,4 @@ -const debug = require('debug')('nodeRedisConnectionPool') +const debug = require('debug')('nodeRedisPool') const util = require('util') const genericPool = require('generic-pool') const retry = require('retry-as-promised') @@ -7,12 +7,12 @@ const redis = require('redis') const createLogger = require('./createLogger') const genRandomStr = require('./genRandomStr') -const create = function (redisOptions) { +const createClient = redisOptions => { return new Promise((resolve, reject) => { debug('Start redis createClient', redisOptions) const client = redis.createClient(redisOptions) - client.on('error', (err) => { + client.on('error', err => { debug('Failed redis createClient', err) reject(err) }) @@ -23,9 +23,9 @@ const create = function (redisOptions) { }) } -const selectDB = function (client, db) { +const selectDB = (client, db) => { return new Promise((resolve, reject) => { - client.select(db, (err) => { + client.select(db, err => { if (err) reject(err) debug('DB selected: ', db) resolve(client) @@ -41,241 +41,237 @@ const selectDB = function (client, db) { * @param {object} options.poolOptions - opts from [node-pool#createpool]{@link https://github.com/coopernurse/node-pool#createpool} * @param {object} options.logger - Inject your custom logger */ -const RedisPool = (module.exports = function ({ - name, - redisOptions, - poolOptions, - logger -} = {}) { - this.name = name || `redisPool-${genRandomStr()}` - this.redisOptions = redisOptions - this.poolOptions = poolOptions || {} - this.logger = createLogger(logger) +module.exports = class RedisPool { + constructor({ name, redisOptions, poolOptions, logger } = {}) { + this.name = name || `redisPool-${genRandomStr()}` + this.redisOptions = redisOptions + this.poolOptions = poolOptions || {} + this.logger = createLogger(logger) - const factory = { - create: () => { - // for retry - let createAttempts = 0 + const factory = { + create: () => { + // for retry + let createAttempts = 0 - // this is due to the limitation of node-pool ATM - // https://github.com/coopernurse/node-pool/issues/161#issuecomment-261109882 - return retry( - () => { - createAttempts += 1 - if (createAttempts > 3) { - const err = new Error( - `Failed redis createClient, ${JSON.stringify(redisOptions || {})}` - ) - err.name = 'CONN_FAILED' + // this is due to the limitation of node-pool ATM + // https://github.com/coopernurse/node-pool/issues/161#issuecomment-261109882 + return retry( + () => { + createAttempts += 1 + if (createAttempts > 3) { + const err = new Error( + `Failed redis createClient, ${JSON.stringify( + redisOptions || {} + )}` + ) + err.name = 'CONN_FAILED' + debug( + 'Max conn createAttempts reached: %s, resolving to error:', + createAttempts, + err + ) + + // reset for next try + createAttempts = 0 + return Promise.resolve(err) + } + + return createClient(redisOptions) + }, + { + max: 10, + name: 'factory.create', + report: debug + } + ) + }, + destroy: client => + new Promise(resolve => { + try { + // Flush when closing. + client.end(true, () => resolve()) debug( - 'Max conn createAttempts reached: %s, resolving to error:', - createAttempts, - err + 'Client conn closed. Available count : %s. Pool size: %s', + this.availableCount(), + this.getPoolSize() ) + this.logger.log( + 'Client conn closed. Available count : %s. Pool size: %s', + this.availableCount(), + this.getPoolSize() + ) + } catch (err) { + debug('Failed to destroy connection', err) + this.logger.error('Failed to destroy connection', err) - // reset for next try - createAttempts = 0 - return Promise.resolve(err) + // throw error cause infinite event loop; limitation of node-pool + // throw err; } + }) + } - return create(redisOptions) - }, - { - max: 10, - name: 'factory.create', - report: debug - } - ) - }, - destroy: (client) => - new Promise((resolve) => { - try { - // Flush when closing. - client.end(true, () => resolve()) - debug( - 'Client conn closed. Available count : %s. Pool size: %s', - this.availableCount(), - this.getPoolSize() - ) - this.logger.log( - 'Client conn closed. Available count : %s. Pool size: %s', - this.availableCount(), - this.getPoolSize() - ) - } catch (err) { - debug('Failed to destroy connection', err) - this.logger.error('Failed to destroy connection', err) + // Now that the pool settings are ready create a pool instance. + debug('Creating pool', this.poolOptions) + this.pool = genericPool.createPool(factory, this.poolOptions) - // throw error cause infinite event loop; limitation of node-pool - // throw err; - } - }) + this.pool.on('factoryCreateError', e => { + debug('Errored while connecting Redis', e) + this.logger.error('Errored while connecting Redis', e) + }) + this.pool.on('factoryDestroyError', e => { + debug('Errored while destroying Redis conn', e) + this.logger.error('Errored while destroying Redis conn', e) + }) } - // Now that the pool settings are ready create a pool instance. - debug('Creating pool', this.poolOptions) - this.pool = genericPool.createPool(factory, this.poolOptions) + /** + * Send redis instructions + * + * @param {string} commandName - Name of the command + * @param {array} commandArgs - Args sent to the command + * @returns {promise} Promise resolve with the result or Error + */ + async sendCommand(commandName, commandArgs = []) { + const args = [].concat(commandArgs) + debug('Executing send_command', commandName, args) - this.pool.on('factoryCreateError', (e) => { - debug('Errored while connecting Redis', e) - this.logger.error('Errored while connecting Redis', e) - }) - this.pool.on('factoryDestroyError', (e) => { - debug('Errored while destroying Redis conn', e) - this.logger.error('Errored while destroying Redis conn', e) - }) -}) + const conn = await this.pool.acquire( + this.poolOptions.priorityRange || 1, + this.redisOptions.db + ) -/** - * Send redis instructions - * - * @param {string} commandName - Name of the command - * @param {array} commandArgs - Args sent to the command - * @returns {promise} Promise resolve with the result or Error - */ -RedisPool.prototype.sendCommand = async function ( - commandName, - commandArgs = [] -) { - const args = [].concat(commandArgs) - debug('Executing send_command', commandName, args) + const sendCommand = util.promisify(conn.send_command).bind(conn) + let result + try { + result = await sendCommand(commandName, args.length > 0 ? args : null) + this.pool.release(conn) + } catch (error) { + this.pool.release(conn) + this.logger.error('Errored send_command', error) + debug('Errored send_command', error) + throw error + } - const conn = await this.pool.acquire( - this.poolOptions.priorityRange || 1, - this.redisOptions.db - ) - - const sendCommand = util.promisify(conn.send_command).bind(conn) - let result - try { - result = await sendCommand(commandName, args.length > 0 ? args : null) - this.pool.release(conn) - } catch (error) { - this.pool.release(conn) - this.logger.error('Errored send_command', error) - debug('Errored send_command', error) - throw error + return result } - return result -} + /** + * Acquire a Redis connection and use an optional priority. + * + * @param {number} priority - priority list number + * @param {number} db - Use the db with range {0-16} + * @returns {promise} Promise resolve with the connection or Error + */ + async acquire(priority, db) { + const client = await this.pool.acquire(priority) + if (client instanceof Error) { + debug("Couldn't acquire connection to %j", this.redisOptions) + this.logger.error("Couldn't acquire connection to %j", this.redisOptions) + throw client + } -/** - * Acquire a Redis connection and use an optional priority. - * - * @param {number} priority - priority list number - * @param {number} db - Use the db with range {0-16} - * @returns {promise} Promise resolve with the connection or Error - */ -RedisPool.prototype.acquire = async function (priority, db) { - const client = await this.pool.acquire(priority) - if (client instanceof Error) { - debug("Couldn't acquire connection to %j", this.redisOptions) - this.logger.error("Couldn't acquire connection to %j", this.redisOptions) - throw client + if (db) { + this.logger.info('select DB:', db) + return selectDB(client, db) + } + return client } - if (db) { - this.logger.info('select DB:', db) - return selectDB(client, db) + /** + * Release a Redis connection to the pool. + * + * @param {object} client - Redis connection + * @returns {promise} Promise + */ + release(client) { + return this.pool.release(client) } - return client -} -/** - * Release a Redis connection to the pool. - * - * @param {object} client - Redis connection - * @returns {promise} Promise - */ -RedisPool.prototype.release = function (client) { - return this.pool.release(client) -} - -/** - * Destroy a Redis connection. - * - * @param {object} client - Redis connection - * @returns {promise} Promise - */ -RedisPool.prototype.destroy = function (client) { - return this.pool.destroy(client) -} + /** + * Destroy a Redis connection. + * + * @param {object} client - Redis connection + * @returns {promise} Promise + */ + destroy(client) { + return this.pool.destroy(client) + } -/** - * Drains the connection pool and call the callback id provided. - * - * @returns {promise} Promise - */ -RedisPool.prototype.drain = async function () { - await this.pool.drain() - this.pool.clear() -} + /** + * Drains the connection pool and call the callback id provided. + * + * @returns {promise} Promise + */ + async drain() { + await this.pool.drain() + this.pool.clear() + } -/** - * Returns factory.name for this pool - * - * @returns {string} Name of the pool - */ -RedisPool.prototype.getName = function () { - return this.name -} + /** + * Returns factory.name for this pool + * + * @returns {string} Name of the pool + */ + getName() { + return this.name + } -/** - * Returns this.redisOptions for this pool - * - * @returns {object} redis options given - */ -RedisPool.prototype.getRedisOptions = function () { - return this.redisOptions -} + /** + * Returns this.redisOptions for this pool + * + * @returns {object} redis options given + */ + getRedisOptions() { + return this.redisOptions + } -/** - * Returns this.poolOptions for this pool - * - * @returns {object} pool options given - */ -RedisPool.prototype.getPoolOptions = function () { - return this.poolOptions -} + /** + * Returns this.poolOptions for this pool + * + * @returns {object} pool options given + */ + getPoolOptions() { + return this.poolOptions + } -/** - * Returns size of the pool - * - * @returns {number} size of the pool - */ -RedisPool.prototype.getPoolSize = function () { - return this.pool.size -} + /** + * Returns size of the pool + * + * @returns {number} size of the pool + */ + getPoolSize() { + return this.pool.size + } -/** - * Returns available connections count of the pool - * - * @returns {number} available connections count of the pool - */ -RedisPool.prototype.availableCount = function () { - return this.pool.available -} + /** + * Returns available connections count of the pool + * + * @returns {number} available connections count of the pool + */ + availableCount() { + return this.pool.available + } -/** - * Returns pending connections count of the pool - * - * @returns {number} pending connections count of the pool - */ -RedisPool.prototype.pendingCount = function () { - return this.pool.pending -} + /** + * Returns pending connections count of the pool + * + * @returns {number} pending connections count of the pool + */ + pendingCount() { + return this.pool.pending + } -/** - * Returns pool status and stats - * - * @returns {object} pool status and stats - */ -RedisPool.prototype.status = function () { - return { - name: this.name, - size: this.pool.size, - available: this.pool.available, - pending: this.pool.pending + /** + * Returns pool status and stats + * + * @returns {object} pool status and stats + */ + status() { + return { + name: this.name, + size: this.pool.size, + available: this.pool.available, + pending: this.pool.pending + } } } diff --git a/lib/redisConnectionPool.test.js b/lib/redisConnectionPool.test.js index 8f1315c..9053846 100644 --- a/lib/redisConnectionPool.test.js +++ b/lib/redisConnectionPool.test.js @@ -6,15 +6,8 @@ describe('redisConnectionPool', () => { host: process.env.REDIS_HOST || '127.0.0.1', auth_pass: process.env.REDIS_AUTH } - const poolOptions = { - min: 2, - max: 4 - } - const options = { - name, - redisOptions, - poolOptions - } + const poolOptions = { min: 2, max: 4 } + const options = { name, redisOptions, poolOptions } let pool beforeEach(() => { @@ -85,10 +78,7 @@ describe('redisConnectionPool', () => { }) test('wait to acquire if all used up', async () => { - const localPoolOptions = { - min: 0, - max: 1 - } + const localPoolOptions = { min: 0, max: 1 } const localPool = new RedisPool({ ...options, poolOptions: localPoolOptions @@ -121,10 +111,7 @@ describe('redisConnectionPool', () => { test('not fail with many higher min connections', async () => { const localPool = new RedisPool({ ...options, - poolOptions: { - min: 5, - max: 10 - } + poolOptions: { min: 5, max: 10 } }) const client = await localPool.acquire() @@ -134,10 +121,7 @@ describe('redisConnectionPool', () => { test('invalid host fail acquire connection', async () => { const localPool = new RedisPool({ ...options, - redisOptions: { - ...redisOptions, - host: 'UNAVAILABLE_HOST' - } + redisOptions: { ...redisOptions, host: 'UNAVAILABLE_HOST' } }) await expect(localPool.acquire()).rejects.toThrowError( @@ -148,17 +132,14 @@ describe('redisConnectionPool', () => { test('conn timeout fail acquire connection', async () => { const localPool = new RedisPool({ ...options, - poolOptions: { - min: 1, - acquireTimeoutMillis: 1 - } + poolOptions: { min: 1, acquireTimeoutMillis: 1 } }) // make the conn is in-use - await new Promise((r) => setTimeout(r, 300)) + await new Promise(r => setTimeout(r, 300)) await localPool.acquire() - await new Promise((r) => setTimeout(r, 1500)) + await new Promise(r => setTimeout(r, 1500)) expect(() => localPool.acquire()).rejects.toThrowError( 'ResourceRequest timed out' ) @@ -168,7 +149,7 @@ describe('redisConnectionPool', () => { describe('release', () => { test('release connection with valid host', async () => { const localPool = new RedisPool(options) - await new Promise((r) => setTimeout(r, 300)) + await new Promise(r => setTimeout(r, 300)) expect(localPool.availableCount()).toBe(poolOptions.min) expect(localPool.getPoolSize()).toBe(poolOptions.min) @@ -185,10 +166,7 @@ describe('redisConnectionPool', () => { test('release connection with invalid host', async () => { const localPool = new RedisPool({ ...options, - redisOptions: { - ...redisOptions, - host: 'UNAVAILABLE_HOST' - } + redisOptions: { ...redisOptions, host: 'UNAVAILABLE_HOST' } }) expect(localPool.release()).rejects.toThrowError( @@ -200,7 +178,7 @@ describe('redisConnectionPool', () => { describe('destroy', () => { test('destroy connection with valid host', async () => { const localPool = new RedisPool(options) - await new Promise((r) => setTimeout(r, 300)) + await new Promise(r => setTimeout(r, 300)) expect(localPool.availableCount()).toBe(poolOptions.min) expect(localPool.getPoolSize()).toBe(poolOptions.min) @@ -211,7 +189,7 @@ describe('redisConnectionPool', () => { expect(localPool.availableCount()).toBe(poolOptions.min - 1) await localPool.destroy(client) - await new Promise((r) => setTimeout(r, 300)) + await new Promise(r => setTimeout(r, 300)) expect(localPool.availableCount()).toBe(poolOptions.min) }) }) @@ -219,7 +197,7 @@ describe('redisConnectionPool', () => { describe('drain', () => { test('drain all the coonections', async () => { const localPool = new RedisPool(options) - await new Promise((r) => setTimeout(r, 300)) + await new Promise(r => setTimeout(r, 300)) expect(localPool.availableCount()).toBe(poolOptions.min) expect(localPool.getPoolSize()).toBe(poolOptions.min) @@ -230,7 +208,7 @@ describe('redisConnectionPool', () => { await localPool.destroy(client) await localPool.drain() - await new Promise((r) => setTimeout(r, 300)) + await new Promise(r => setTimeout(r, 300)) expect(localPool.availableCount()).toBe(poolOptions.min) expect(localPool.getPoolSize()).toBe(0) }) diff --git a/lib/redisStore.js b/lib/redisStore.js index 5d4a504..ad76cca 100644 --- a/lib/redisStore.js +++ b/lib/redisStore.js @@ -14,238 +14,157 @@ const genRandomStr = require('./genRandomStr') * @param {object} options.logger - Inject your custom logger * @param {integer} options.ttlInSeconds - Number of seconds to store by default */ -const RedisStore = (module.exports = function ({ - name, - redisOptions, - poolOptions, - logger, - ttlInSeconds -} = {}) { - this.name = name || `redisStore-${genRandomStr()}` - this.redisOptions = redisOptions - this.poolOptions = poolOptions || {} - this.logger = createLogger(logger) - this.ttlInSeconds = ttlInSeconds - - this.pool = null - try { - this.pool = new RedisPool({ - name: this.name, - redisOptions: this.redisOptions, - poolOptions: this.poolOptions, - logger: this.logger - }) +module.exports = class RedisStore extends RedisPool { + constructor({ name, redisOptions, poolOptions, logger, ttlInSeconds } = {}) { + const options = { + name: name || `redisStore-${genRandomStr()}`, + redisOptions, + poolOptions, + logger: createLogger(logger), + ttlInSeconds + } - // // since pool factory events are not triggered due to retry issue; a workaround - // this.testConnection() - // .then((res) => { - // console.log("#########################", res) - // debug("Redis store created.", this.pool.status()) - // }); - // this.pool.acquire() - } catch (e) { - debug('Failed to create', e) - this.pool = null - throw e + super(options) } -}) -RedisStore.prototype.testConnection = async function () { - debug('PING to test connection') + /** + * Returns 'PONG' + * + * @param {string} str - string passed + * @returns {string} 'PONG'/string passed + */ + ping(str) { + return this.sendCommand('ping', str) + } - let result - try { - result = await this.ping() - if (result !== 'PONG') { - debug('expected PONG but got', result) - const err = new Error('UNKNOWN_PING_RESPONSE') - err.message = `expected PONG but got : ${result}` - throw err + /** + * Returns value or null when the key is missing - See [redis get]{@link https://redis.io/commands/get} + * + * @param {string} key - key for the value stored + * @returns {string} value or null when the key is missing + */ + async get(key) { + let result = await this.sendCommand('get', key) + + try { + result = JSON.parse(result) + } catch (e) { + // do nothing } - } catch (error) { - debug('Failed to PING', error) - this.logger.error('Test connection failed', error) - throw error + return result } - return result -} - -/** - * Returns factory.name for this pool - * - * @returns {string} Name of the pool - */ -RedisStore.prototype.getName = function () { - return this.pool.getName() -} - -/** - * Returns this.redisOptions for this pool - * - * @returns {object} redis options given - */ -RedisStore.prototype.getRedisOptions = function () { - return this.pool.getRedisOptions() -} - -/** - * Returns this.poolOptions for this pool - * - * @returns {object} pool options given - */ -RedisStore.prototype.getPoolOptions = function () { - return this.pool.getPoolOptions() -} -/** - * Send redis instructions - * - * @param {string} commandName - Name of the command - * @param {array} commandArgs - Args sent to the command - * @returns {promise} Promise resolve with the result or Error - */ -RedisStore.prototype.sendCommand = function (...args) { - return this.pool.sendCommand.apply(this, args) -} - -/** - * Returns 'PONG' - * - * @param {string} str - string passed - * @returns {string} 'PONG'/string passed - */ -RedisStore.prototype.ping = function (str) { - if (str) { - return this.sendCommand('ping', str) + /** + * Returns 'OK' if successful + * + * @param {string} key - key for the value stored + * @param {any} value - value to stored + * @param {number} ttlInSeconds - time to live in seconds + * @returns {string} 'OK' if successful + */ + set(key, value, ttlInSeconds) { + const str = + Array.isArray(value) || isJSON(value, true) + ? JSON.stringify(value) + : value + const ttlInS = ttlInSeconds || this.ttlInSeconds + + if (ttlInS) { + return this.sendCommand('setex', [key, ttlInS, str]) + } + return this.sendCommand('set', [key, str]) } - return this.sendCommand('ping') -} -/** - * Returns value or null when the key is missing - See [redis get]{@link https://redis.io/commands/get} - * - * @param {string} key - key for the value stored - * @returns {string} value or null when the key is missing - */ -RedisStore.prototype.get = async function (key) { - let result = await this.sendCommand('get', key) + /** + * Returns 'OK' if successful + * + * @param {string} key - key for the value stored + * @param {any} value - value to stored + * @param {number} ttlInSeconds - time to live in seconds + * @returns {any} + */ + async getset(key, value, ttlInSeconds) { + const str = + Array.isArray(value) || isJSON(value, true) + ? JSON.stringify(value) + : value + const ttlInS = ttlInSeconds || this.ttlInSeconds + + let result = await this.sendCommand('getset', [key, str]) + try { + result = JSON.parse(result) + } catch (e) { + // do nothing + } - try { - result = JSON.parse(result) - } catch (e) { - // do nothing + if (ttlInS) { + await this.sendCommand('expire', [key, ttlInS]) + } + return result } - return result -} - -/** - * Returns 'OK' if successful - * - * @param {string} key - key for the value stored - * @param {string} value - value to stored - * @param {number} ttlInSeconds - time to live in seconds - * @returns {string} 'OK' if successful - */ -RedisStore.prototype.set = function (key, value, ttlInSeconds) { - const str = - Array.isArray(value) || isJSON(value, true) ? JSON.stringify(value) : value - const ttlInS = ttlInSeconds || this.ttlInSeconds - if (ttlInS) { - return this.sendCommand('setex', [key, ttlInS, str]) + /** + * Returns the number of keys that were removed - See [redis del]{@link https://redis.io/commands/del} + * + * @param {array} keys - list of keys to delete + * @returns {number} The number of keys that were removed. + */ + del(keys) { + return this.sendCommand('del', keys) } - return this.sendCommand('set', [key, str]) -} - -RedisStore.prototype.getset = async function (key, value, ttlInSeconds) { - const str = - Array.isArray(value) || isJSON(value, true) ? JSON.stringify(value) : value - const ttlInS = ttlInSeconds || this.ttlInSeconds - let result = await this.sendCommand('getset', [key, str]) - try { - result = JSON.parse(result) - } catch (e) { - // do nothing + /** + * Returns 1 if the timeout was set/ 0 if key does not exist or the timeout could not be set - See [redis expire]{@link https://redis.io/commands/expire} + * + * @param {string} key - key to set expire + * @param {number} ttlInSeconds - time to live in seconds + * @returns {number} 1 if the timeout was set successfully; if not 0 + */ + expire(key, ttlInSeconds) { + return this.sendCommand('expire', [key, ttlInSeconds]) } - if (ttlInS) { - await this.sendCommand('expire', [key, ttlInS]) + /** + * Returns TTL in seconds, or a negative value in order to signal an error - See [redis ttl]{@link https://redis.io/commands/ttl} + * + * @param {string} key - list of keys to delete + * @returns {number} TTL in seconds, or a negative value in order to signal an error + */ + getTtl(key) { + return this.sendCommand('ttl', key) } - return result -} -/** - * Returns the number of keys that were removed - See [redis del]{@link https://redis.io/commands/del} - * - * @param {array} keys - list of keys to delete - * @returns {number} The number of keys that were removed. - */ -RedisStore.prototype.del = function (keys) { - return this.sendCommand('del', keys) -} - -/** - * Returns 1 if the timeout was set/ 0 if key does not exist or the timeout could not be set - See [redis expire]{@link https://redis.io/commands/expire} - * - * @param {string} key - key to set expire - * @param {number} ttlInSeconds - time to live in seconds - * @returns {number} 1 if the timeout was set successfully; if not 0 - */ -RedisStore.prototype.expire = function (key, ttlInSeconds) { - return this.sendCommand('expire', [key, ttlInSeconds]) -} - -/** - * Returns TTL in seconds, or a negative value in order to signal an error - See [redis ttl]{@link https://redis.io/commands/ttl} - * - * @param {string} key - list of keys to delete - * @returns {number} TTL in seconds, or a negative value in order to signal an error - */ -RedisStore.prototype.getTtl = function (key) { - return this.sendCommand('ttl', key) -} - -/** - * Returns all keys matching pattern - See [redis keys]{@link https://redis.io/commands/keys} - * - * @param {string} pattern - glob-style patterns/default '*' - * @returns {array} all keys matching pattern - */ -RedisStore.prototype.keys = function (pattern) { - return this.sendCommand('keys', pattern || '*') -} - -/** - * Deletes all keys matching pattern - * - * @param {string} pattern - glob-style patterns/default '*' - * @returns {number} The number of keys that were removed. - */ -RedisStore.prototype.deleteAll = function (pattern) { - debug('clearing redis keys: ', pattern || '*') - return this._executeDeleteAll(pattern || '*') -} + /** + * Returns all keys matching pattern - See [redis keys]{@link https://redis.io/commands/keys} + * + * @param {string} pattern - glob-style patterns/default '*' + * @returns {array} all keys matching pattern + */ + keys(pattern) { + return this.sendCommand('keys', pattern || '*') + } -/** - * Returns pool status and stats - * - * @returns {object} pool status and stats - */ -RedisStore.prototype.status = function () { - return this.pool.status() -} + /** + * Deletes all keys matching pattern + * + * @param {string} pattern - glob-style patterns/default '*' + * @returns {number} The number of keys that were removed. + */ + deleteAll(pattern) { + debug('clearing redis keys: ', pattern || '*') + return this._executeDeleteAll(pattern || '*') + } -/** - * Preloads delete all scripts into redis script cache - * (this script requires redis >= 4.0.0) - * @async - * @returns {Promise} sha1 hash of preloaded function - * @private - */ -RedisStore.prototype._loadDeleteAllScript = function () { - if (!this.__deleteScriptPromise) { - const deleteKeysScript = ` + /** + * Preloads delete all scripts into redis script cache + * (this script requires redis >= 4.0.0) + * @async + * @returns {Promise} sha1 hash of preloaded function + * @private + */ + _loadDeleteAllScript() { + if (!this.__deleteScriptPromise) { + const deleteKeysScript = ` local keys = {}; local done = false; local cursor = "0"; @@ -263,32 +182,33 @@ RedisStore.prototype._loadDeleteAllScript = function () { end until done return deleted;` - this.__deleteScriptPromise = this.sendCommand('SCRIPT', [ - 'LOAD', - deleteKeysScript - ]) + this.__deleteScriptPromise = this.sendCommand('SCRIPT', [ + 'LOAD', + deleteKeysScript + ]) + } + return this.__deleteScriptPromise } - return this.__deleteScriptPromise -} -/** - * Preloads and execute delete all script - * @async - * @param {string} pattern - glob-style patterns/default '*' - * @returns {Promise} The number of keys that were removed. - * @private - */ -RedisStore.prototype._executeDeleteAll = async function (pattern) { - let sha1 - try { - sha1 = await this._loadDeleteAllScript() - } catch (error) { - if (error.code === 'NOSCRIPT') { - // We can get here only if server is restarted somehow and cache is deleted - this.__deleteScriptPromise = null - return this._executeDeleteAll(pattern) + /** + * Preloads and execute delete all script + * @async + * @param {string} pattern - glob-style patterns/default '*' + * @returns {Promise} The number of keys that were removed. + * @private + */ + async _executeDeleteAll(pattern) { + let sha1 + try { + sha1 = await this._loadDeleteAllScript() + } catch (error) { + if (error.code === 'NOSCRIPT') { + // We can get here only if server is restarted somehow and cache is deleted + this.__deleteScriptPromise = null + return this._executeDeleteAll(pattern) + } + throw error } - throw error + return this.sendCommand('EVALSHA', [sha1, 0, pattern, 1000]) } - return this.sendCommand('EVALSHA', [sha1, 0, pattern, 1000]) } diff --git a/lib/redisStore.test.js b/lib/redisStore.test.js index 0574e18..1903257 100644 --- a/lib/redisStore.test.js +++ b/lib/redisStore.test.js @@ -6,15 +6,8 @@ describe('redisStore', () => { host: process.env.REDIS_HOST || '127.0.0.1', auth_pass: process.env.REDIS_AUTH } - const poolOptions = { - min: 2, - max: 4 - } - const options = { - name, - redisOptions, - poolOptions - } + const poolOptions = { min: 2, max: 4 } + const options = { name, redisOptions, poolOptions } let store beforeEach(() => { @@ -99,9 +92,7 @@ describe('redisStore', () => { test('retrieve parsed json', async () => { const key = 'chuck-norris' - const value = { - type: 'superman' - } + const value = { type: 'superman' } await store.set(key, value) @@ -134,9 +125,7 @@ describe('redisStore', () => { test('store json', async () => { const key = 'key' - const value = { - type: 'json' - } + const value = { type: 'json' } const result = await store.set(key, value) expect(result).toBe('OK') @@ -164,7 +153,7 @@ describe('redisStore', () => { expect(store.get(key)).resolves.toBe(value) - await new Promise((r) => setTimeout(r, 1100)) + await new Promise(r => setTimeout(r, 1100)) expect(store.get(key)).resolves.toBeNull() }) }) @@ -195,7 +184,7 @@ describe('redisStore', () => { const result = await store.expire(key, ttlInSeconds) expect(result).toBe(1) - await new Promise((r) => setTimeout(r, 1100)) + await new Promise(r => setTimeout(r, 1100)) expect(store.get(key)).resolves.toBeNull() }) @@ -220,14 +209,11 @@ describe('redisStore', () => { }) describe('keys', () => { - const keyValues = { - key1: 'value1', - key2: 'value2' - } + const keyValues = { key1: 'value1', key2: 'value2' } beforeEach(async () => { await Promise.all( - Object.keys(keyValues).map((key) => store.set(key, keyValues[key])) + Object.keys(keyValues).map(key => store.set(key, keyValues[key])) ) }) @@ -242,15 +228,12 @@ describe('redisStore', () => { }) describe('deleteAll', () => { - const keyValues = { - key1: 'value1', - key2: 'value2' - } + const keyValues = { key1: 'value1', key2: 'value2' } beforeEach(async () => { await store.deleteAll() await Promise.all( - Object.keys(keyValues).map((key) => store.set(key, keyValues[key])) + Object.keys(keyValues).map(key => store.set(key, keyValues[key])) ) })