From 2fd3d946455d157210bb27cd8753045e2e7eb2e1 Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 7 Oct 2018 01:11:04 +0800 Subject: [PATCH] fix(cluster): robust solution for pub/sub in cluster Previously (v3 & v4.0.0), ioredis reuse the existing connection for subscription, which will cause problem when executing commands on the reused connection. From now on, a specialized connection will be created when any subscription has made. This solves the problem above perfectly. Close #696 --- lib/cluster/ClusterSubscriber.ts | 122 +++++++ lib/cluster/ConnectionPool.ts | 81 +++-- lib/cluster/index.js | 116 ++----- lib/cluster/util.ts | 9 + lib/errors/MaxRetriesPerRequestError.js | 15 - lib/errors/MaxRetriesPerRequestError.ts | 14 + lib/errors/index.js | 1 - lib/errors/index.ts | 5 + lib/redis.js | 38 ++- lib/redis/event_handler.js | 2 +- package-lock.json | 406 ++++++++++++------------ package.json | 2 +- test/functional/cluster/pub_sub.js | 10 +- test/helpers/mock_server.js | 11 + 14 files changed, 470 insertions(+), 362 deletions(-) create mode 100644 lib/cluster/ClusterSubscriber.ts create mode 100644 lib/cluster/util.ts delete mode 100644 lib/errors/MaxRetriesPerRequestError.js create mode 100644 lib/errors/MaxRetriesPerRequestError.ts delete mode 100644 lib/errors/index.js create mode 100644 lib/errors/index.ts diff --git a/lib/cluster/ClusterSubscriber.ts b/lib/cluster/ClusterSubscriber.ts new file mode 100644 index 000000000..eadb4d1e8 --- /dev/null +++ b/lib/cluster/ClusterSubscriber.ts @@ -0,0 +1,122 @@ +import {EventEmitter} from 'events' +import ConnectionPool from './ConnectionPool' +import {sample, noop} from '../utils/lodash' +import {getNodeKey} from './util' + +const Redis = require('../redis') +const debug = require('../utils/debug')('ioredis:cluster:subscriber') + +const SUBSCRIBER_CONNECTION_NAME = 'ioredisClusterSubscriber' + +export default class ClusterSubscriber { + private started: boolean = false + private subscriber: any = null + private lastActiveSubscriber: any + + constructor (private connectionPool: ConnectionPool, private emitter: EventEmitter) { + this.connectionPool.on('-node', (_, key: string) => { + if (!this.started || !this.subscriber) { + return + } + if (getNodeKey(this.subscriber.options) === key) { + debug('subscriber has left, selecting a new one...') + this.selectSubscriber() + } + }) + this.connectionPool.on('+node', () => { + if (!this.started || this.subscriber) { + return + } + debug('a new node is discovered and there is no subscriber, selecting a new one...') + this.selectSubscriber() + }) + } + + getInstance (): any { + return this.subscriber + } + + private selectSubscriber () { + const lastActiveSubscriber = this.lastActiveSubscriber + + // Disconnect the previous subscriber even if there + // will not be a new one. + if (lastActiveSubscriber) { + lastActiveSubscriber.disconnect() + } + + const sampleNode = sample(this.connectionPool.getNodes()) + if (!sampleNode) { + debug('selecting subscriber failed since there is no node discovered in the cluster yet') + this.subscriber = null + return + } + + const {port, host} = sampleNode.options + debug('selected a subscriber %s:%s', host, port) + + // Create a specialized Redis connection for the subscription. + // Note that auto reconnection is enabled here. + // `enableReadyCheck` is disabled because subscription is allowed + // when redis is loading data from the disk. + this.subscriber = new Redis({ + port, + host, + enableReadyCheck: false, + connectionName: SUBSCRIBER_CONNECTION_NAME, + lazyConnect: true + }) + + // Re-subscribe previous channels + var previousChannels = { subscribe: [], psubscribe: [] } + if (lastActiveSubscriber) { + const condition = lastActiveSubscriber.condition || lastActiveSubscriber.prevCondition + if (condition && condition.subscriber) { + previousChannels.subscribe = condition.subscriber.channels('subscribe') + previousChannels.psubscribe = condition.subscriber.channels('psubscribe') + } + } + if (previousChannels.subscribe.length || previousChannels.psubscribe.length) { + var pending = 0 + for (const type of ['subscribe', 'psubscribe']) { + var channels = previousChannels[type] + if (channels.length) { + pending += 1 + debug('%s %d channels', type, channels.length) + this.subscriber[type](channels).then(() => { + if (!--pending) { + this.lastActiveSubscriber = this.subscriber + } + }).catch(noop) + } + } + } else { + this.lastActiveSubscriber = this.subscriber + } + for (const event of ['message', 'messageBuffer']) { + this.subscriber.on(event, (arg1, arg2) => { + this.emitter.emit(event, arg1, arg2) + }) + } + for (const event of ['pmessage', 'pmessageBuffer']) { + this.subscriber.on(event, (arg1, arg2, arg3) => { + this.emitter.emit(event, arg1, arg2, arg3) + }) + } + } + + start (): void { + this.started = true + this.selectSubscriber() + debug('started') + } + + stop (): void { + this.started = false + if (this.subscriber) { + this.subscriber.disconnect() + this.subscriber = null + } + debug('stopped') + } +} diff --git a/lib/cluster/ConnectionPool.ts b/lib/cluster/ConnectionPool.ts index ebc035c00..e6b98e095 100644 --- a/lib/cluster/ConnectionPool.ts +++ b/lib/cluster/ConnectionPool.ts @@ -1,20 +1,13 @@ import {parseURL} from '../utils' import {EventEmitter} from 'events' -import {noop, defaults} from '../utils/lodash' +import {noop, defaults, values} from '../utils/lodash' +import {IRedisOptions, getNodeKey} from './util' const Redis = require('../redis') const debug = require('../utils/debug')('ioredis:cluster:connectionPool') type NODE_TYPE = 'all' | 'master' | 'slave' -interface IRedisOptions { - [key: string]: any -} - -interface IRedisOptionsWithKey extends IRedisOptions { - key: string -} - export default class ConnectionPool extends EventEmitter { // master + slave = all private nodes: {[key in NODE_TYPE]: {[key: string]: any}} = { @@ -29,6 +22,10 @@ export default class ConnectionPool extends EventEmitter { super() } + public getNodes(role: 'all' | 'master' | 'slave' = 'all'): any[] { + return values(this.nodes[role]) + } + /** * Find or create a connection to the node * @@ -37,33 +34,34 @@ export default class ConnectionPool extends EventEmitter { * @returns {*} * @memberof ConnectionPool */ - public findOrCreate (node: IRedisOptions, readOnly: boolean = false): any { - setKey(node) + public findOrCreate(node: IRedisOptions, readOnly: boolean = false): any { + fillDefaultOptions(node) + const key = getNodeKey(node) readOnly = Boolean(readOnly) - if (this.specifiedOptions[node.key]) { - Object.assign(node, this.specifiedOptions[node.key]) + if (this.specifiedOptions[key]) { + Object.assign(node, this.specifiedOptions[key]) } else { - this.specifiedOptions[node.key] = node + this.specifiedOptions[key] = node } let redis - if (this.nodes.all[node.key]) { - redis = this.nodes.all[node.key] + if (this.nodes.all[key]) { + redis = this.nodes.all[key] if (redis.options.readOnly !== readOnly) { redis.options.readOnly = readOnly - debug('Change role of %s to %s', node.key, readOnly ? 'slave' : 'master') + debug('Change role of %s to %s', key, readOnly ? 'slave' : 'master') redis[readOnly ? 'readonly' : 'readwrite']().catch(noop) if (readOnly) { - delete this.nodes.master[node.key] - this.nodes.slave[node.key] = redis + delete this.nodes.master[key] + this.nodes.slave[key] = redis } else { - delete this.nodes.slave[node.key] - this.nodes.master[node.key] = redis + delete this.nodes.slave[key] + this.nodes.master[key] = redis } } } else { - debug('Connecting to %s as %s', node.key, readOnly ? 'slave' : 'master') + debug('Connecting to %s as %s', key, readOnly ? 'slave' : 'master') redis = new Redis(defaults({ // Never try to reconnect when a node is lose, // instead, waiting for a `MOVED` error and @@ -75,23 +73,23 @@ export default class ConnectionPool extends EventEmitter { enableOfflineQueue: true, readOnly: readOnly }, node, this.redisOptions, { lazyConnect: true })) - this.nodes.all[node.key] = redis - this.nodes[readOnly ? 'slave' : 'master'][node.key] = redis + this.nodes.all[key] = redis + this.nodes[readOnly ? 'slave' : 'master'][key] = redis redis.once('end', () => { - delete this.nodes.all[node.key] - delete this.nodes.master[node.key] - delete this.nodes.slave[node.key] - this.emit('-node', redis) + delete this.nodes.all[key] + delete this.nodes.master[key] + delete this.nodes.slave[key] + this.emit('-node', redis, key) if (!Object.keys(this.nodes.all).length) { this.emit('drain') } }) - this.emit('+node', redis) + this.emit('+node', redis, key) redis.on('error', function (error) { - this.emit('nodeError', error) + this.emit('nodeError', error, key) }) } @@ -105,14 +103,15 @@ export default class ConnectionPool extends EventEmitter { * @param {(Array)} nodes * @memberof ConnectionPool */ - public reset (nodes: Array): void { + public reset(nodes: Array): void { + debug('Reset with %O', nodes); const newNodes = {} nodes.forEach((node) => { - const options: {port?: number | string, db?: number, key?: string} = {} + const options: IRedisOptions = {} if (typeof node === 'object') { - defaults(options, node) + Object.assign(options, node) } else if (typeof node === 'string') { - defaults(options, parseURL(node)) + Object.assign(options, parseURL(node)) } else if (typeof node === 'number') { options.port = node } else { @@ -123,8 +122,8 @@ export default class ConnectionPool extends EventEmitter { } delete options.db - setKey(options) - newNodes[options.key] = options + fillDefaultOptions(options) + newNodes[getNodeKey(options)] = options }, this) Object.keys(this.nodes.all).forEach((key) => { @@ -140,15 +139,7 @@ export default class ConnectionPool extends EventEmitter { } } -/** - * Set key property - * - * @private - */ -function setKey(node: IRedisOptions): IRedisOptionsWithKey { - node = node || {} +function fillDefaultOptions(node: IRedisOptions): void { node.port = node.port || 6379 node.host = node.host || '127.0.0.1' - node.key = node.key || node.host + ':' + node.port - return node } diff --git a/lib/cluster/index.js b/lib/cluster/index.js index 45960c2ba..8b4a471be 100644 --- a/lib/cluster/index.js +++ b/lib/cluster/index.js @@ -13,8 +13,10 @@ var Command = require('../command'); var commands = require('redis-commands'); var asCallback = require('standard-as-callback'); var ConnectionPool = require('./ConnectionPool').default; +var ClusterSubscriber = require('./ClusterSubscriber').default; var DelayQueue = require('./DelayQueue').default; var PromiseContainer = require('../promiseContainer'); +var {AbortError} = require('redis-errors'); /** * Creates a Redis Cluster instance @@ -59,21 +61,17 @@ function Cluster(startupNodes, options) { this.connectionPool = new ConnectionPool(this.options.redisOptions); this.startupNodes = startupNodes; - var _this = this; - this.connectionPool.on('-node', function (redis) { - if (_this.status !== 'disconnecting' && _this.subscriber === redis) { - _this.selectSubscriber(); - } - _this.emit('-node', redis); + this.connectionPool.on('-node', (redis, key) => { + this.emit('-node', redis); }); - this.connectionPool.on('+node', function (redis) { - _this.emit('+node', redis); + this.connectionPool.on('+node', (redis) => { + this.emit('+node', redis); }); - this.connectionPool.on('drain', function () { - _this.setStatus('close'); + this.connectionPool.on('drain', () => { + this.setStatus('close'); }); - this.connectionPool.on('nodeError', function (error) { - _this.emit('node error', error); + this.connectionPool.on('nodeError', (error) => { + this.emit('node error', error); }); this.slots = []; @@ -82,12 +80,14 @@ function Cluster(startupNodes, options) { this.resetOfflineQueue(); this.delayQueue = new DelayQueue(); - this.subscriber = null; + this.subscriber = new ClusterSubscriber(this.connectionPool, this) if (this.options.lazyConnect) { this.setStatus('wait'); } else { - this.connect().catch(_.noop); + this.connect().catch((err) => { + debug('connecting failed: %s', err) + }); } } @@ -193,7 +193,7 @@ Cluster.prototype.connect = function () { this.connectionPool.reset([]); } }.bind(this)); - this.selectSubscriber(); + this.subscriber.start(); }.bind(this)); }; @@ -244,6 +244,7 @@ Cluster.prototype.disconnect = function (reconnect) { this.slotsTimer = null; } + this.subscriber.stop(); if (status === 'wait') { this.setStatus('close'); this._handleCloseEvent(); @@ -309,59 +310,7 @@ Cluster.prototype.nodes = function (role) { if (role !== 'all' && role !== 'master' && role !== 'slave') { throw new Error('Invalid role "' + role + '". Expected "all", "master" or "slave"'); } - return _.values(this.connectionPool.nodes[role]); -}; - -/** - * Select a subscriber from the cluster - * - * @private - */ -Cluster.prototype.selectSubscriber = function () { - this.subscriber = _.sample(this.nodes()); - if (!this.subscriber) { - return; - } - // Re-subscribe previous channels - var previousChannels = { subscribe: [], psubscribe: [] }; - if (this.lastActiveSubscriber && this.lastActiveSubscriber.prevCondition) { - var subscriber = this.lastActiveSubscriber.prevCondition.subscriber; - if (subscriber) { - previousChannels.subscribe = subscriber.channels('subscribe'); - previousChannels.psubscribe = subscriber.channels('psubscribe'); - } - } - var _this = this; - if (previousChannels.subscribe.length || previousChannels.psubscribe.length) { - var pending = 0; - _.forEach(['subscribe', 'psubscribe'], function (type) { - var channels = previousChannels[type]; - if (channels.length) { - pending += 1; - debug('%s %d channels', type, channels.length); - _this.subscriber[type](channels).then(function () { - if (!--pending) { - _this.lastActiveSubscriber = _this.subscriber; - } - }).catch(_.noop); - } - }); - } else { - if (this.subscriber.status === 'wait') { - this.subscriber.connect().catch(_.noop); - } - this.lastActiveSubscriber = this.subscriber; - } - _.forEach(['message', 'messageBuffer'], function (event) { - _this.subscriber.on(event, function (arg1, arg2) { - _this.emit(event, arg1, arg2); - }); - }); - _.forEach(['pmessage', 'pmessageBuffer'], function (event) { - _this.subscriber.on(event, function (arg1, arg2, arg3) { - _this.emit(event, arg1, arg2, arg3); - }); - }); + return this.connectionPool.getNodes(role) }; /** @@ -409,16 +358,8 @@ Cluster.prototype.refreshSlotsCache = function (callback) { error.lastNodeError = lastNodeError; return wrapper(error); } - const key = keys[index] - const node = _this.connectionPool.nodes.all[keys[index]] - debug('getting slot cache from %s', key); - if (_this.subscriber === node) { - debug('skipping because the node is the subscriber'); - lastNodeError = new Error('Node is the subscriber'); - tryNode(index + 1); - return; - } - _this.getInfoFromNode(node, function (err) { + debug('getting slot cache from %s', keys[index]); + _this.getInfoFromNode(_this.connectionPool.nodes.all[keys[index]], function (err) { if (_this.status === 'end') { return wrapper(new Error('Cluster is disconnected.')); } @@ -521,7 +462,7 @@ Cluster.prototype.sendCommand = function (command, stream, node) { function tryConnection(random, asking) { if (_this.status === 'end') { - command.reject(new Error('Cluster is ended.')); + command.reject(new AbortError('Cluster is ended.')); return; } var redis; @@ -530,7 +471,11 @@ Cluster.prototype.sendCommand = function (command, stream, node) { redis = node.redis; } else if (Command.checkFlag('ENTER_SUBSCRIBER_MODE', command.name) || Command.checkFlag('EXIT_SUBSCRIBER_MODE', command.name)) { - redis = _this.subscriber; + redis = _this.subscriber.getInstance(); + if (!redis) { + command.reject(new AbortError('No subscriber for the cluster')); + return; + } } else { if (!random) { if (typeof targetSlot === 'number' && _this.slots[targetSlot]) { @@ -629,14 +574,15 @@ Cluster.prototype.getInfoFromNode = function (redis, callback) { if (!redis) { return callback(new Error('Node is disconnected')); } - var _this = this; - redis.cluster('slots', utils.timeout(function (err, result) { + redis.cluster('slots', utils.timeout((err, result) => { if (err) { redis.disconnect(); return callback(err); } var nodes = []; + debug('cluster slots result count: %d', result.length) + for (var i = 0; i < result.length; ++i) { var items = result[i]; var slotRangeStart = items[0]; @@ -650,12 +596,14 @@ Cluster.prototype.getInfoFromNode = function (redis, callback) { keys.push(items[j].host + ':' + items[j].port); } + debug('cluster slots result [%d]: slots %d~%d served by %s', i, slotRangeStart, slotRangeEnd, keys) + for (var slot = slotRangeStart; slot <= slotRangeEnd; slot++) { - _this.slots[slot] = keys; + this.slots[slot] = keys; } } - _this.connectionPool.reset(nodes); + this.connectionPool.reset(nodes); callback(); }, this.options.slotsRefreshTimeout)); }; diff --git a/lib/cluster/util.ts b/lib/cluster/util.ts new file mode 100644 index 000000000..9ae9e8372 --- /dev/null +++ b/lib/cluster/util.ts @@ -0,0 +1,9 @@ +export interface IRedisOptions { + [key: string]: any +} + +export function getNodeKey(node: IRedisOptions): string { + node.port = node.port || 6379 + node.host = node.host || '127.0.0.1' + return node.host + ':' + node.port +} diff --git a/lib/errors/MaxRetriesPerRequestError.js b/lib/errors/MaxRetriesPerRequestError.js deleted file mode 100644 index 35ea3bf5b..000000000 --- a/lib/errors/MaxRetriesPerRequestError.js +++ /dev/null @@ -1,15 +0,0 @@ -const {AbortError} = require('redis-errors') - -module.exports = class MaxRetriesPerRequestError extends AbortError { - constructor (maxRetriesPerRequest) { - var message = `Reached the max retries per request limit (which is ${maxRetriesPerRequest}). Refer to "maxRetriesPerRequest" option for details.`; - - super(message); - Error.captureStackTrace(this, this.constructor); - } - - get name () { - return this.constructor.name; - } -}; - diff --git a/lib/errors/MaxRetriesPerRequestError.ts b/lib/errors/MaxRetriesPerRequestError.ts new file mode 100644 index 000000000..d38b503c7 --- /dev/null +++ b/lib/errors/MaxRetriesPerRequestError.ts @@ -0,0 +1,14 @@ +import {AbortError} from 'redis-errors' + +export default class MaxRetriesPerRequestError extends AbortError { + constructor (maxRetriesPerRequest: number) { + const message = `Reached the max retries per request limit (which is ${maxRetriesPerRequest}). Refer to "maxRetriesPerRequest" option for details.` + + super(message) + Error.captureStackTrace(this, this.constructor) + } + + get name (): string { + return this.constructor.name + } +} diff --git a/lib/errors/index.js b/lib/errors/index.js deleted file mode 100644 index 6a342fd48..000000000 --- a/lib/errors/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.MaxRetriesPerRequestError = require('./MaxRetriesPerRequestError') diff --git a/lib/errors/index.ts b/lib/errors/index.ts new file mode 100644 index 000000000..52f86ab7b --- /dev/null +++ b/lib/errors/index.ts @@ -0,0 +1,5 @@ +import MaxRetriesPerRequestError from './MaxRetriesPerRequestError' + +export { + MaxRetriesPerRequestError +} diff --git a/lib/redis.js b/lib/redis.js index 93517148a..1737326ac 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -228,16 +228,9 @@ Redis.prototype.parseOptions = function () { * @private */ Redis.prototype.setStatus = function (status, arg) { - var address; - if (this.options.path) { - address = this.options.path; - } else if (this.stream && this.stream.remoteAddress && this.stream.remotePort) { - address = this.stream.remoteAddress + ':' + this.stream.remotePort; - } else { - address = this.options.host + ':' + this.options.port; + if (debug.enabled) { + debug('status[%s]: %s -> %s', this._getDescription(), this.status || '[empty]', status); } - - debug('status[%s]: %s -> %s', address, this.status || '[empty]', status); this.status = status; process.nextTick(this.emit.bind(this, status, arg)); }; @@ -602,7 +595,9 @@ Redis.prototype.sendCommand = function (command, stream) { } if (writable) { - debug('write command[%d] -> %s(%s)', this.condition.select, command.name, command.args); + if (debug.enabled) { + debug('write command[%s]: %d -> %s(%o)', this._getDescription(), this.condition.select, command.name, command.args); + } (stream || this.stream).write(command.toWritable()); this.commandQueue.push({ @@ -615,7 +610,9 @@ Redis.prototype.sendCommand = function (command, stream) { this.manuallyClosing = true; } } else if (this.options.enableOfflineQueue) { - debug('queue command[%d] -> %s(%s)', this.condition.select, command.name, command.args); + if (debug.enabled) { + debug('queue command[%s]: %d -> %s(%o)', this._getDescription(), this.condition.select, command.name, command.args); + } this.offlineQueue.push({ command: command, stream: stream, @@ -635,6 +632,25 @@ Redis.prototype.sendCommand = function (command, stream) { return command.promise; }; +/** + * Get description of the connection. Used for debugging. + * @private + */ +Redis.prototype._getDescription = function () { + let description; + if (this.options.path) { + description = this.options.path; + } else if (this.stream && this.stream.remoteAddress && this.stream.remotePort) { + description = this.stream.remoteAddress + ':' + this.stream.remotePort; + } else { + description = this.options.host + ':' + this.options.port; + } + if (this.options.connectionName) { + description += ` (${this.options.connectionName})` + } + return description +}; + ['scan', 'sscan', 'hscan', 'zscan', 'scanBuffer', 'sscanBuffer', 'hscanBuffer', 'zscanBuffer'] .forEach(function (command) { Redis.prototype[command + 'Stream'] = function (key, options) { diff --git a/lib/redis/event_handler.js b/lib/redis/event_handler.js index 778055941..e7e2140eb 100644 --- a/lib/redis/event_handler.js +++ b/lib/redis/event_handler.js @@ -157,7 +157,7 @@ exports.readyHandler = function (self) { if (self.options.connectionName) { debug('set the connection name [%s]', self.options.connectionName); - self.client('setname', self.options.connectionName); + self.client('setname', self.options.connectionName).catch(_.noop); } if (self.options.readOnly) { diff --git a/package-lock.json b/package-lock.json index b773f69ea..c54be721b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,9 +22,9 @@ "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" } }, "amdefine": { @@ -39,7 +39,7 @@ "integrity": "sha512-v+0wW9Wezwsyb0uF4aBVCjmSqit3Ru7PZFziGF0o2KwTvN2zWfTi3BRLq9EkJFdg3eBbyERXGTntVpBxH1J68Q==", "dev": true, "requires": { - "array-back": "2.0.0" + "array-back": "^2.0.0" } }, "argparse": { @@ -48,7 +48,7 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" } }, "argv-tools": { @@ -57,8 +57,8 @@ "integrity": "sha512-Cc0dBvx4dvrjjKpyDA6w8RlNAw8Su30NvZbWl/Tv9ZALEVlLVkWQiHMi84Q0xNfpVuSaiQbYkdmWK8g1PLGhKw==", "dev": true, "requires": { - "array-back": "2.0.0", - "find-replace": "2.0.1" + "array-back": "^2.0.0", + "find-replace": "^2.0.1" } }, "array-back": { @@ -67,7 +67,7 @@ "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", "dev": true, "requires": { - "typical": "2.6.1" + "typical": "^2.6.1" } }, "arrify": { @@ -112,7 +112,7 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -134,9 +134,9 @@ "integrity": "sha512-4TgWfe9SF+bUy5cCql8gWHqKNrviufNwSYxLjf2utB0pY4+bdcuFwMmY1hDB+67Gz/L1vmhFNhePAjJTFBtV+Q==", "dev": true, "requires": { - "array-back": "2.0.0", - "fs-then-native": "2.0.0", - "mkdirp2": "1.0.4" + "array-back": "^2.0.0", + "fs-then-native": "^2.0.0", + "mkdirp2": "^1.0.3" } }, "camelcase": { @@ -152,7 +152,7 @@ "integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=", "dev": true, "requires": { - "underscore-contrib": "0.3.0" + "underscore-contrib": "~0.3.0" } }, "center-align": { @@ -162,8 +162,8 @@ "dev": true, "optional": true, "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" } }, "chai": { @@ -172,9 +172,9 @@ "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", "dev": true, "requires": { - "assertion-error": "1.1.0", - "deep-eql": "0.1.3", - "type-detect": "1.0.0" + "assertion-error": "^1.0.1", + "deep-eql": "^0.1.3", + "type-detect": "^1.0.0" } }, "cliui": { @@ -184,8 +184,8 @@ "dev": true, "optional": true, "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", + "center-align": "^0.1.1", + "right-align": "^0.1.1", "wordwrap": "0.0.2" }, "dependencies": { @@ -209,8 +209,8 @@ "integrity": "sha512-0y0rBgoX8IzIjBAUnO73SEtSb4Mhk3IoceWJq5zZSxb9mWORhWH8xLYo4EDSOE1jRBk1LhmfjqWFFt10h/+MEA==", "dev": true, "requires": { - "stream-connect": "1.0.2", - "stream-via": "1.0.4" + "stream-connect": "^1.0.2", + "stream-via": "^1.0.4" } }, "command-line-args": { @@ -219,11 +219,11 @@ "integrity": "sha512-/qPcbL8zpqg53x4rAaqMFlRV4opN3pbla7I7k9x8kyOBMQoGT6WltjN6sXZuxOXw6DgdK7Ad+ijYS5gjcr7vlA==", "dev": true, "requires": { - "argv-tools": "0.1.1", - "array-back": "2.0.0", - "find-replace": "2.0.1", - "lodash.camelcase": "4.3.0", - "typical": "2.6.1" + "argv-tools": "^0.1.1", + "array-back": "^2.0.0", + "find-replace": "^2.0.1", + "lodash.camelcase": "^4.3.0", + "typical": "^2.6.1" } }, "command-line-tool": { @@ -232,11 +232,11 @@ "integrity": "sha512-Xw18HVx/QzQV3Sc5k1vy3kgtOeGmsKIqwtFFoyjI4bbcpSgnw2CWVULvtakyw4s6fhyAdI6soQQhXc2OzJy62g==", "dev": true, "requires": { - "ansi-escape-sequences": "4.0.0", - "array-back": "2.0.0", - "command-line-args": "5.0.2", - "command-line-usage": "4.1.0", - "typical": "2.6.1" + "ansi-escape-sequences": "^4.0.0", + "array-back": "^2.0.0", + "command-line-args": "^5.0.0", + "command-line-usage": "^4.1.0", + "typical": "^2.6.1" } }, "command-line-usage": { @@ -245,10 +245,10 @@ "integrity": "sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g==", "dev": true, "requires": { - "ansi-escape-sequences": "4.0.0", - "array-back": "2.0.0", - "table-layout": "0.4.4", - "typical": "2.6.1" + "ansi-escape-sequences": "^4.0.0", + "array-back": "^2.0.0", + "table-layout": "^0.4.2", + "typical": "^2.6.1" } }, "commander": { @@ -275,7 +275,7 @@ "integrity": "sha1-ZnZjWQUFooO/JqSE1oSJ10xUhdo=", "dev": true, "requires": { - "walk-back": "2.0.1" + "walk-back": "^2.0.1" }, "dependencies": { "walk-back": { @@ -298,11 +298,11 @@ "integrity": "sha1-L0vHOQ4yROTfKT5ro1Hkx0Cnx2Q=", "dev": true, "requires": { - "conventional-commit-types": "2.2.0", - "lodash.map": "4.6.0", - "longest": "1.0.1", - "right-pad": "1.0.1", - "word-wrap": "1.2.3" + "conventional-commit-types": "^2.0.0", + "lodash.map": "^4.5.1", + "longest": "^1.0.1", + "right-pad": "^1.0.1", + "word-wrap": "^1.0.3" } }, "debug": { @@ -366,18 +366,18 @@ "integrity": "sha512-79w644JdsB2TthYpVl2bDurX7i9Abaegg2E7X46Ajc135aASTMXxrHzJ9mOa5X5nbmnXwlBYiF68K+1baX+BzQ==", "dev": true, "requires": { - "array-back": "2.0.0", - "cache-point": "0.4.1", - "common-sequence": "1.0.2", - "file-set": "2.0.0", - "handlebars": "4.0.11", - "marked": "0.3.19", - "object-get": "2.1.0", - "reduce-flatten": "1.0.1", - "reduce-unique": "1.0.0", - "reduce-without": "1.0.1", - "test-value": "3.0.0", - "walk-back": "3.0.0" + "array-back": "^2.0.0", + "cache-point": "^0.4.1", + "common-sequence": "^1.0.2", + "file-set": "^2.0.0", + "handlebars": "^4.0.11", + "marked": "^0.3.16", + "object-get": "^2.1.0", + "reduce-flatten": "^1.0.1", + "reduce-unique": "^1.0.0", + "reduce-without": "^1.0.1", + "test-value": "^3.0.0", + "walk-back": "^3.0.0" } }, "escape-string-regexp": { @@ -392,11 +392,11 @@ "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", "dev": true, "requires": { - "esprima": "2.7.3", - "estraverse": "1.9.3", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.2.0" + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" } }, "esprima": { @@ -429,8 +429,8 @@ "integrity": "sha512-cCWXfw+nrYoIoUVmEF7Xsw91lGWuObtSnTEZ7AmdvZou1A/6Xx237HfxdQyC/ayKRvQSMbNOBwg62OjN5JxbXw==", "dev": true, "requires": { - "array-back": "2.0.0", - "glob": "7.1.2" + "array-back": "^2.0.0", + "glob": "^7.1.2" }, "dependencies": { "glob": { @@ -439,12 +439,12 @@ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } } } @@ -455,8 +455,8 @@ "integrity": "sha512-LzDo3Fpa30FLIBsh6DCDnMN1KW2g4QKkqKmejlImgWY67dDFPX/x9Kh/op/GK522DchQXEvDi/wD48HKW49XOQ==", "dev": true, "requires": { - "array-back": "2.0.0", - "test-value": "3.0.0" + "array-back": "^2.0.0", + "test-value": "^3.0.0" } }, "flexbuffer": { @@ -470,7 +470,7 @@ "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", "dev": true, "requires": { - "samsam": "1.1.2" + "samsam": "~1.1" } }, "fs-then-native": { @@ -491,11 +491,11 @@ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "dev": true, "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "graceful-fs": { @@ -516,10 +516,10 @@ "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", "dev": true, "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" }, "dependencies": { "source-map": { @@ -528,7 +528,7 @@ "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } } } @@ -551,8 +551,8 @@ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -579,20 +579,20 @@ "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", "dev": true, "requires": { - "abbrev": "1.0.9", - "async": "1.5.2", - "escodegen": "1.8.1", - "esprima": "2.7.3", - "glob": "5.0.15", - "handlebars": "4.0.11", - "js-yaml": "3.12.0", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "once": "1.4.0", - "resolve": "1.1.7", - "supports-color": "3.2.3", - "which": "1.3.1", - "wordwrap": "1.0.0" + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" } }, "js-yaml": { @@ -601,8 +601,8 @@ "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", "dev": true, "requires": { - "argparse": "1.0.10", - "esprima": "4.0.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, "dependencies": { "esprima": { @@ -619,7 +619,7 @@ "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", "dev": true, "requires": { - "xmlcreate": "1.0.2" + "xmlcreate": "^1.0.1" } }, "jsdoc": { @@ -629,17 +629,17 @@ "dev": true, "requires": { "babylon": "7.0.0-beta.19", - "bluebird": "3.5.1", - "catharsis": "0.8.9", - "escape-string-regexp": "1.0.5", - "js2xmlparser": "3.0.0", - "klaw": "2.0.0", - "marked": "0.3.19", - "mkdirp": "0.5.1", - "requizzle": "0.2.1", - "strip-json-comments": "2.0.1", + "bluebird": "~3.5.0", + "catharsis": "~0.8.9", + "escape-string-regexp": "~1.0.5", + "js2xmlparser": "~3.0.0", + "klaw": "~2.0.0", + "marked": "~0.3.6", + "mkdirp": "~0.5.1", + "requizzle": "~0.2.1", + "strip-json-comments": "~2.0.1", "taffydb": "2.6.2", - "underscore": "1.8.3" + "underscore": "~1.8.3" } }, "jsdoc-api": { @@ -648,15 +648,15 @@ "integrity": "sha512-dfYq9JgB+XahY0XfSEw93PmXmocjwYcvJ5aMuQUJ/OdDRGWamf2SSOk3W06Bsj8qdjp/UdefzqpP/mpwsvHuvA==", "dev": true, "requires": { - "array-back": "2.0.0", - "cache-point": "0.4.1", - "collect-all": "1.0.3", - "file-set": "2.0.0", - "fs-then-native": "2.0.0", - "jsdoc": "3.5.5", - "object-to-spawn-args": "1.1.1", - "temp-path": "1.0.0", - "walk-back": "3.0.0" + "array-back": "^2.0.0", + "cache-point": "^0.4.1", + "collect-all": "^1.0.3", + "file-set": "^2.0.0", + "fs-then-native": "^2.0.0", + "jsdoc": "~3.5.5", + "object-to-spawn-args": "^1.1.1", + "temp-path": "^1.0.0", + "walk-back": "^3.0.0" } }, "jsdoc-parse": { @@ -665,12 +665,12 @@ "integrity": "sha512-btZLp4wYl90vcAfgk4hoGQbO17iBVrhh3LJRMKZNtZgniO3F8H2CjxXld0owBIB1XxN+j3bAcWZnZKMnSj3iMA==", "dev": true, "requires": { - "array-back": "2.0.0", - "lodash.omit": "4.5.0", - "lodash.pick": "4.4.0", - "reduce-extract": "1.0.0", - "sort-array": "2.0.0", - "test-value": "3.0.0" + "array-back": "^2.0.0", + "lodash.omit": "^4.5.0", + "lodash.pick": "^4.4.0", + "reduce-extract": "^1.0.0", + "sort-array": "^2.0.0", + "test-value": "^3.0.0" } }, "jsdoc-to-markdown": { @@ -679,13 +679,13 @@ "integrity": "sha512-LHJRoLoLyDdxNcColgkLoB/rFG5iRP+PNJjMILI0x+95IdEAtyjSt0wJ6ZlKxRpkhBYtQXTQQ119hMqPIUZzTQ==", "dev": true, "requires": { - "array-back": "2.0.0", - "command-line-tool": "0.8.0", - "config-master": "3.1.0", - "dmd": "3.0.12", - "jsdoc-api": "4.0.3", - "jsdoc-parse": "3.0.1", - "walk-back": "3.0.0" + "array-back": "^2.0.0", + "command-line-tool": "^0.8.0", + "config-master": "^3.1.0", + "dmd": "^3.0.10", + "jsdoc-api": "^4.0.1", + "jsdoc-parse": "^3.0.1", + "walk-back": "^3.0.0" } }, "kind-of": { @@ -694,7 +694,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } }, "klaw": { @@ -703,7 +703,7 @@ "integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=", "dev": true, "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.9" } }, "lazy-cache": { @@ -719,8 +719,8 @@ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, "lodash.bind": { @@ -842,7 +842,7 @@ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -899,12 +899,12 @@ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "has-flag": { @@ -919,7 +919,7 @@ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } @@ -935,7 +935,7 @@ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { - "abbrev": "1.0.9" + "abbrev": "1" } }, "object-get": { @@ -956,7 +956,7 @@ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "optimist": { @@ -965,8 +965,8 @@ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "dev": true, "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" }, "dependencies": { "wordwrap": { @@ -983,12 +983,12 @@ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", "dev": true, "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" } }, "path-is-absolute": { @@ -1018,7 +1018,7 @@ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", "requires": { - "redis-errors": "1.2.0" + "redis-errors": "^1.0.0" } }, "reduce-extract": { @@ -1027,7 +1027,7 @@ "integrity": "sha1-Z/I4W+2mUGG19fQxJmLosIDKFSU=", "dev": true, "requires": { - "test-value": "1.1.0" + "test-value": "^1.0.1" }, "dependencies": { "array-back": { @@ -1036,7 +1036,7 @@ "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", "dev": true, "requires": { - "typical": "2.6.1" + "typical": "^2.6.0" } }, "test-value": { @@ -1045,8 +1045,8 @@ "integrity": "sha1-oJE29y7AQ9J8iTcHwrFZv6196T8=", "dev": true, "requires": { - "array-back": "1.0.4", - "typical": "2.6.1" + "array-back": "^1.0.2", + "typical": "^2.4.2" } } } @@ -1069,7 +1069,7 @@ "integrity": "sha1-aK0OrRGFXJo31OglbBW7+Hly/Iw=", "dev": true, "requires": { - "test-value": "2.1.0" + "test-value": "^2.0.0" }, "dependencies": { "array-back": { @@ -1078,7 +1078,7 @@ "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", "dev": true, "requires": { - "typical": "2.6.1" + "typical": "^2.6.0" } }, "test-value": { @@ -1087,8 +1087,8 @@ "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=", "dev": true, "requires": { - "array-back": "1.0.4", - "typical": "2.6.1" + "array-back": "^1.0.3", + "typical": "^2.6.0" } } } @@ -1105,7 +1105,7 @@ "integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=", "dev": true, "requires": { - "underscore": "1.6.0" + "underscore": "~1.6.0" }, "dependencies": { "underscore": { @@ -1129,7 +1129,7 @@ "dev": true, "optional": true, "requires": { - "align-text": "0.1.4" + "align-text": "^0.1.1" } }, "right-pad": { @@ -1159,7 +1159,7 @@ "formatio": "1.1.1", "lolex": "1.3.2", "samsam": "1.1.2", - "util": "0.11.0" + "util": ">=0.10.3 <1" } }, "sort-array": { @@ -1168,9 +1168,9 @@ "integrity": "sha1-OKnG2if9fRR7QuYFVPKBGHtN9HI=", "dev": true, "requires": { - "array-back": "1.0.4", - "object-get": "2.1.0", - "typical": "2.6.1" + "array-back": "^1.0.4", + "object-get": "^2.1.0", + "typical": "^2.6.0" }, "dependencies": { "array-back": { @@ -1179,7 +1179,7 @@ "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", "dev": true, "requires": { - "typical": "2.6.1" + "typical": "^2.6.0" } } } @@ -1191,7 +1191,7 @@ "dev": true, "optional": true, "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } }, "source-map-support": { @@ -1200,8 +1200,8 @@ "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", "dev": true, "requires": { - "buffer-from": "1.1.0", - "source-map": "0.6.1" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" }, "dependencies": { "source-map": { @@ -1229,7 +1229,7 @@ "integrity": "sha1-GLyB8u2zW4tdmoAJIAqYUxRCipc=", "dev": true, "requires": { - "array-back": "1.0.4" + "array-back": "^1.0.2" }, "dependencies": { "array-back": { @@ -1238,7 +1238,7 @@ "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=", "dev": true, "requires": { - "typical": "2.6.1" + "typical": "^2.6.0" } } } @@ -1261,7 +1261,7 @@ "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", "dev": true, "requires": { - "has-flag": "1.0.0" + "has-flag": "^1.0.0" } }, "table-layout": { @@ -1270,11 +1270,11 @@ "integrity": "sha512-uNaR3SRMJwfdp9OUr36eyEi6LLsbcTqTO/hfTsNviKsNeyMBPICJCC7QXRF3+07bAP6FRwA8rczJPBqXDc0CkQ==", "dev": true, "requires": { - "array-back": "2.0.0", - "deep-extend": "0.6.0", - "lodash.padend": "4.6.1", - "typical": "2.6.1", - "wordwrapjs": "3.0.0" + "array-back": "^2.0.0", + "deep-extend": "~0.6.0", + "lodash.padend": "^4.6.1", + "typical": "^2.6.1", + "wordwrapjs": "^3.0.0" } }, "taffydb": { @@ -1295,8 +1295,8 @@ "integrity": "sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ==", "dev": true, "requires": { - "array-back": "2.0.0", - "typical": "2.6.1" + "array-back": "^2.0.0", + "typical": "^2.6.1" } }, "ts-node": { @@ -1305,14 +1305,14 @@ "integrity": "sha512-klJsfswHP0FuOLsvBZ/zzCfUvakOSSxds78mVeK7I+qP76YWtxf16hEZsp3U+b0kIo82R5UatGFeblYMqabb2Q==", "dev": true, "requires": { - "arrify": "1.0.1", - "buffer-from": "1.1.0", - "diff": "3.5.0", - "make-error": "1.3.4", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "source-map-support": "0.5.6", - "yn": "2.0.0" + "arrify": "^1.0.0", + "buffer-from": "^1.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" }, "dependencies": { "minimist": { @@ -1329,7 +1329,7 @@ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, "requires": { - "prelude-ls": "1.1.2" + "prelude-ls": "~1.1.2" } }, "type-detect": { @@ -1339,9 +1339,9 @@ "dev": true }, "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "version": "3.1.1", + "resolved": "http://registry.npm.taobao.org/typescript/download/typescript-3.1.1.tgz", + "integrity": "sha1-M2K6ndHkguuyNVsC3+i80ZosfJY=", "dev": true }, "typical": { @@ -1357,9 +1357,9 @@ "dev": true, "optional": true, "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" }, "dependencies": { "source-map": { @@ -1422,7 +1422,7 @@ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "window-size": { @@ -1450,8 +1450,8 @@ "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==", "dev": true, "requires": { - "reduce-flatten": "1.0.1", - "typical": "2.6.1" + "reduce-flatten": "^1.0.1", + "typical": "^2.6.1" } }, "wrappy": { @@ -1473,9 +1473,9 @@ "dev": true, "optional": true, "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", "window-size": "0.1.0" } }, diff --git a/package.json b/package.json index 30aa698e6..4fd9ccfc4 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "server-destroy": "^1.0.1", "sinon": "^1.17.3", "ts-node": "^7.0.0", - "typescript": "^2.9.2" + "typescript": "^3.1.1" }, "engines": { "node": ">=4" diff --git a/test/functional/cluster/pub_sub.js b/test/functional/cluster/pub_sub.js index 95143283d..e356d650d 100644 --- a/test/functional/cluster/pub_sub.js +++ b/test/functional/cluster/pub_sub.js @@ -15,7 +15,7 @@ describe('cluster:pub/sub', function () { var sub = new Redis.Cluster(options); sub.subscribe('test cluster', function () { - node1.write(node1.clients[0], ['message', 'test channel', 'hi']); + node1.write(node1.findClientByName('ioredisClusterSubscriber'), ['message', 'test channel', 'hi']); }); sub.on('message', function (channel, message) { expect(channel).to.eql('test channel'); @@ -78,5 +78,13 @@ describe('cluster:pub/sub', function () { client.disconnect(); }); }); + + it.only('should throw when not providing startup nodes', function (done) { + var client = new Redis.Cluster([]); + client.subscribe('abc', (a, b) => { + console.log('~', a, b) + done(); + }) + }) }); diff --git a/test/helpers/mock_server.js b/test/helpers/mock_server.js index 991301875..08f7032bb 100644 --- a/test/helpers/mock_server.js +++ b/test/helpers/mock_server.js @@ -56,6 +56,9 @@ MockServer.prototype.connect = function () { returnBuffers: true, returnReply: function (reply) { reply = utils.convertBufferToString(reply); + if (reply.length === 3 && reply[0].toLowerCase() === 'client' && reply[1].toLowerCase() === 'setname') { + c._connectionName = reply[2] + } _this.write(c, _this.handler && _this.handler(reply)); }, returnError: function () { } @@ -119,6 +122,14 @@ MockServer.prototype.write = function (c, data) { } }; +MockServer.prototype.findClientByName = function (name) { + for (const client of this.clients) { + if (client._connectionName === name) { + return client + } + } +} + MockServer.REDIS_OK = '+OK'; module.exports = MockServer;