From 6da6273d946b581c5f2dcf575f367efded61e07a Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Wed, 18 Jul 2018 20:20:11 +0200 Subject: [PATCH 1/8] feat: reconnect logic implemented --- README.md | 33 ++- src/connection.js | 210 +++++++++------- src/connector.js | 473 +++++++++++++++++++----------------- src/table-manager.js | 163 +++++++------ test/cache-connectorSpec.js | 144 ++++++----- test/connection-handling.js | 130 ++++++++++ test/fast-insertionSpec.js | 2 +- test/multi-tableSpec.js | 2 +- 8 files changed, 696 insertions(+), 461 deletions(-) create mode 100644 test/connection-handling.js diff --git a/README.md b/README.md index 993a739..c4d5573 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # deepstream.io-storage-rethinkdb + [![Coverage Status](https://coveralls.io/repos/github/deepstreamIO/deepstream.io-storage-rethinkdb/badge.svg?branch=master)](https://coveralls.io/github/deepstreamIO/deepstream.io-storage-rethinkdb?branch=master) [![npm](https://img.shields.io/npm/v/deepstream.io-storage-rethinkdb.svg)](https://www.npmjs.com/package/deepstream.io-storage-rethinkdb) [![Dependency Status](https://david-dm.org/deepstreamIO/deepstream.io-storage-rethinkdb.svg)](https://david-dm.org/deepstreamIO/deepstream.io-storage-rethinkdb) @@ -9,9 +10,10 @@ This connector uses [the npm rethinkdb package](https://www.npmjs.com/package/rethinkdb). Please have a look there for detailed options. -**Warning**: This plugin will automatically create a table, if it doesn't exist yet. But be aware, in case you create a table manually, use "ds_id" as the primary key. Otherwise the plugin won't be able to find your records. +**Warning**: This plugin will automatically create a table, if it doesn't exist yet. But be aware, in case you create a table manually, use "ds_id" as the primary key. Otherwise the plugin won't be able to find your records. ## Configuration Options + ```yaml plugins: storage: @@ -22,6 +24,8 @@ plugins: database: 'someDb' defaultTable: 'someTable' splitChar: '/' + reconnectTimeout: 2000 + reconnectCount: 0 ``` ```javascript @@ -49,21 +53,30 @@ plugins: * would create a table called 'books' and store the record under the name * 'dream-of-the-red-chamber' */ - splitChar: '/' + splitChar: '/', + + // (Optional, defaults to 0) If the rhe rethinkdb connection is lost, try to reconnect max. this number of times + reconnectCount: 2, + + // (Optional, defaults to 2000) Timeout between reconnect attempts in milliseconds + reconnectTimeout: 3000 } ``` ## Basic Setup + ```javascript -var Deepstream = require( 'deepstream.io' ), - RethinkDBStorageConnector = require( 'deepstream.io-storage-rethinkdb' ), - server = new Deepstream(); +var Deepstream = require('deepstream.io'), + RethinkDBStorageConnector = require('deepstream.io-storage-rethinkdb'), + server = new Deepstream(); -server.set( 'storage', new RethinkDBStorageConnector( { - port: 5672, - host: 'localhost' -})); +server.set( + 'storage', + new RethinkDBStorageConnector({ + port: 5672, + host: 'localhost' + }) +); server.start(); ``` - diff --git a/src/connection.js b/src/connection.js index 30979f7..82664da 100644 --- a/src/connection.js +++ b/src/connection.js @@ -1,102 +1,136 @@ "use strict" +var events = require('events'); -const rethinkdb = require( 'rethinkdb' ) +const rethinkdb = require('rethinkdb') class Connection { - /** - * This class provides a wrapper around the rethinkdb connection. It creates - * the database - if it doesn't exist - and maintains a cache of all available - * tables to decrease lookup times for write operations - * - * @param {Object} options {database: } + RethinkDB connect options (http://rethinkdb.com/api/javascript/#connect) - * @param {Function} callback function that will be called once the conenctions has been established and the database had been created - * - * @constructor - */ - constructor( options, callback ) { - this._options = options - this._callback = callback - this._connection = null - this._database = options.database || 'deepstream' - options.db = this._database - rethinkdb.connect( options, this._fn( this._onConnection ) ) - } + /** + * This class provides a wrapper around the rethinkdb connection. It creates + * the database - if it doesn't exist - and maintains a cache of all available + * tables to decrease lookup times for write operations + * + * @param {Object} options {database: } + RethinkDB connect options (http://rethinkdb.com/api/javascript/#connect) + * @param {Function} callback function that will be called once the conenctions has been established and the database had been created + * + * @constructor + */ + constructor(options, callback) { + this._options = options + this._callback = callback + this._connection = null + this._database = options.database || 'deepstream' + options.db = this._database + rethinkdb.connect(options, this._fn(this._onConnection)) + } + + /** + * Returns the underlying rethinkdb connection + * + * @public + * @returns {RethinkDB Connection} connection + */ + get connection() { + return this._connection + } - /** - * Returns the underlying rethinkdb connection - * - * @public - * @returns {RethinkDB Connection} connection - */ - get() { - return this._connection - } + /** + * Returns the configured number of maximum reconnections + * + * @public + * @returns {Number} number of max. reconnections + */ + get reconnectCount() { + return this._options.reconnectCount || 0; + } - /** - * Callback for established connections. Retrieves the list - * of available databases - * - * @param {RethinkDB Connection} connection - * - * @private - * @returns {void} - */ - _onConnection( connection ) { - this._connection = connection - rethinkdb.dbList().run( connection, this._fn( this._onDbList ) ) - } + /** + * Returns the configured timeouts between two reconnect attempts in ms. Default value: 2000 ms + * + * @public + * @returns {Number} the timeout in ms + */ + get reconnectTimeout() { + return this._options.reconnectTimeout >= 0 ? this._options.reconnectTimeout : 2000; + } - /** - * Callback for retrieved database lists. Will check if the deepstream - * database exists and - if not - create it. - * - * @param {Array} dbList A list of all available databases - * - * @private - * @returns {void} - */ - _onDbList( dbList ) { - if( dbList.indexOf( this._database ) === -1 ) { - rethinkdb.dbCreate( this._database ).run( this._connection, this._fn( this._onDb ) ) - } else { - this._onDb() + /** + * When connection is lost try to reconnect. Reconnection during first/initialization stage is not handled! + * So, it does not recreate tables, etc. + * + * + * @public + * @returns {RethinkDB Connection} connection as promise + */ + reconnect() { + if (this.reconnectCount) + return rethinkdb.connect(this._options).then(conn => this._connection = conn) } - } - /** - * Callback once the database becomes available, either as a result of a create operation - * or because it already existed. - * - * Will retrieve a list of tables from the database - * - * @private - * @returns {void} - */ - _onDb() { - this._connection.use( this._database ) - this._callback( null ) - } + /** + * Callback for established connections. Retrieves the list + * of available databases + * + * @param {RethinkDB Connection} connection + * + * @private + * @returns {void} + */ + _onConnection(connection) { + this._connection = connection + rethinkdb.dbList().run(connection, this._fn(this._onDbList)) + } - /** - * Utility method. Wraps a function into another function - * that has the right context and handles errors. Gets around - * the endless if( error !== null ) {...} - * - * @param {Function} fn - * - * @private - * @returns {Function} - */ - _fn( fn ) { - return function( error, result ) { - if( error ) { - this._callback( error ) - } else { - fn.call( this, result ) - } - }.bind( this ) - } + /** + * Callback for retrieved database lists. Will check if the deepstream + * database exists and - if not - create it. + * + * @param {Array} dbList A list of all available databases + * + * @private + * @returns {void} + */ + _onDbList(dbList) { + if (dbList.indexOf(this._database) === -1) { + rethinkdb.dbCreate(this._database).run(this._connection, this._fn(this._onDb)) + } else { + this._onDb() + } + } + + /** + * Callback once the database becomes available, either as a result of a create operation + * or because it already existed. + * + * Will retrieve a list of tables from the database + * + * @private + * @returns {void} + */ + _onDb() { + this._connection.use(this._database) + this._callback(null) + } + + /** + * Utility method. Wraps a function into another function + * that has the right context and handles errors. Gets around + * the endless if( error !== null ) {...} + * + * @param {Function} fn + * + * @private + * @returns {Function} + */ + _fn(fn) { + return function(error, result) { + if (error) { + this._callback(error) + } else { + fn.call(this, result) + } + }.bind(this) + } } module.exports = Connection \ No newline at end of file diff --git a/src/connector.js b/src/connector.js index 4552ee7..f28f08b 100644 --- a/src/connector.js +++ b/src/connector.js @@ -1,236 +1,275 @@ "use strict" -const EventEmitter = require( 'events' ).EventEmitter -const util = require( 'util' ) -const rethinkdb = require( 'rethinkdb' ) -const Connection = require( './connection' ) -const TableManager = require( './table-manager' ) -const dataTransform = require( './transform-data' ) -const pckg = require( '../package.json' ) -const PRIMARY_KEY = require( './primary-key') -const crypto = require( 'crypto' ) +const crypto = require('crypto') +const EventEmitter = require('events').EventEmitter +const rethinkdb = require('rethinkdb') +const Connection = require('./connection') +const TableManager = require('./table-manager') +const dataTransform = require('./transform-data') +const pckg = require('../package.json') +const PRIMARY_KEY = require('./primary-key') class Connector extends EventEmitter { - /** - * Connects deepstream to a rethinkdb. RethinksDB is a great fit for deepstream due to its realtime capabilities. - * - * Similar to other storage connectors (e.g. MongoDB), this connector supports saving records to multiple tables. - * In order to do this, specify a splitChar, e.g. '/' and use it in your record names. Naming your record - * - * user/i4vcg5j1-16n1qrnziuog - * - * for instance will create a user table and store it in it. This will allow for more sophisticated queries as - * well as speed up read operations since there are less entries to look through - * - * @param {Object} options rethinkdb driver options. See rethinkdb.com/api/javascript/#connect - * - * e.g. - * - * { - * host: 'localhost', - * port: 28015, - * authKey: 'someString' - * database: 'deepstream', - * defaultTable: 'deepstream_records', - * splitChar: '/' - * } - * - * Please note the three additional, optional keys: - * - * database specifies which database to use. Defaults to 'deepstream' - * defaultTable specifies which table records will be stored in that don't specify a table. Defaults to deepstream_records - * splitChar specifies a character that separates the record's id from the table it should be stored in. defaults to null - * - * @constructor - */ - constructor( options ) { - super() - this.isReady = false - this.name = pckg.name - this.version = pckg.version - this._checkOptions( options ) - this._options = options - this._connection = new Connection( options, this._onConnection.bind( this ) ) - this._tableManager = new TableManager( this._connection ) - this._defaultTable = options.defaultTable || 'deepstream_records' - this._splitChar = options.splitChar || null - this._tableMatch = this._splitChar - ? new RegExp( '^(\\w+)' + this._escapeRegExp( this._splitChar ) ) - : null - this._primaryKey = options.primaryKey || PRIMARY_KEY - } - - /** - * Writes a value to the database. If the specified table doesn't exist yet, it will be created - * before the write is excecuted. If a table creation is already in progress, create table will - * only add the method to its array of callbacks - * - * @param {String} key - * @param {Object} value - * @param {Function} callback Will be called with null for successful set operations or with an error message string - * - * @public - * @returns {void} - */ - set( key, value, callback ) { - const params = this._getParams( key ) - const entry = dataTransform.transformValueForStorage( value ) - const insert = this._insert.bind( this, params, entry, callback ) - - if( this._tableManager.hasTable( params.table ) ) { - insert() - } else { - this._tableManager.createTable( params.table, this._primaryKey, insert ) + /** + * Connects deepstream to a rethinkdb. RethinksDB is a great fit for deepstream due to its realtime capabilities. + * + * Similar to other storage connectors (e.g. MongoDB), this connector supports saving records to multiple tables. + * In order to do this, specify a splitChar, e.g. '/' and use it in your record names. Naming your record + * + * user/i4vcg5j1-16n1qrnziuog + * + * for instance will create a user table and store it in it. This will allow for more sophisticated queries as + * well as speed up read operations since there are less entries to look through + * + * @param {Object} options rethinkdb driver options. See rethinkdb.com/api/javascript/#connect + * + * e.g. + * + * { + * host: 'localhost', + * port: 28015, + * authKey: 'someString' + * database: 'deepstream', + * defaultTable: 'deepstream_records', + * splitChar: '/' + * } + * + * Please note the three additional, optional keys: + * + * database specifies which database to use. Defaults to 'deepstream' + * defaultTable specifies which table records will be stored in that don't specify a table. Defaults to deepstream_records + * splitChar specifies a character that separates the record's id from the table it should be stored in. defaults to null + * + * @constructor + */ + constructor(options) { + super() + this.isReady = false + this.name = pckg.name + this.version = pckg.version + this._checkOptions(options) + this._options = options + this._connection = new Connection(options, this._onConnection.bind(this)) + this._reconnectCount = this._connection.reconnectCount + this._tableManager = new TableManager(this._connection) + this._defaultTable = options.defaultTable || 'deepstream_records' + this._splitChar = options.splitChar || null + this._tableMatch = this._splitChar ? + new RegExp('^(\\w+)' + this._escapeRegExp(this._splitChar)) : + null + this._primaryKey = options.primaryKey || PRIMARY_KEY } - } - - /** - * Retrieves a value from the cache - * - * @param {String} key - * @param {Function} callback Will be called with null and the stored object - * for successful operations or with an error message string - * - * @public - * @returns {void} - */ - get( key, callback ) { - const params = this._getParams( key ) - - if( this._tableManager.hasTable( params.table ) ) { - rethinkdb.table( params.table ).get( params.id ).run( this._connection.get(), ( error, entry ) => { - if( entry ) { - delete entry[ this._primaryKey ] - delete entry.__key // in case is set - entry = dataTransform.transformValueFromStorage( entry ) + + /** + * Writes a value to the database. If the specified table doesn't exist yet, it will be created + * before the write is excecuted. If a table creation is already in progress, create table will + * only add the method to its array of callbacks + * + * @param {String} key + * @param {Object} value + * @param {Function} callback Will be called with null for successful set operations or with an error message string + * + * @public + * @returns {void} + */ + set(key, value, callback) { + const params = this._getParams(key) + const entry = dataTransform.transformValueForStorage(value) + const insert = this._insert.bind(this, params, entry, callback) + + if (this._tableManager.hasTable(params.table)) { + insert() + } else { + this._tableManager.createTable(params.table, this._primaryKey, insert) } - callback( error, entry ) - } ) - } else { - callback( null, null ) } - } - - /** - * Deletes an entry from the cache. - * - * @param {String} key - * @param {Function} callback Will be called with null for successful deletions or with - * an error message string - * - * @public - * @returns {void} - */ - delete( key, callback ) { - const params = this._getParams( key ) - - if( this._tableManager.hasTable( params.table ) ) { - rethinkdb.table( params.table ).get( params.id ).delete().run( this._connection.get(), callback ) - } else { - callback( new Error( 'Table \'' + params.table + '\' does not exist' ) ) + + /** + * Retrieves a value from the cache + * + * @param {String} key + * @param {Function} callback Will be called with null and the stored object + * for successful operations or with an error message string + * + * @public + * @returns {void} + */ + get(key, callback) { + const params = this._getParams(key) + + if (this._tableManager.hasTable(params.table)) { + rethinkdb.table(params.table).get(params.id).run(this._connection.connection, (error, entry) => { + if (entry) { + delete entry[this._primaryKey] + delete entry.__key // in case is set + entry = dataTransform.transformValueFromStorage(entry) + } + this._errorHandler(callback, this.get, arguments)(error, entry); + }) + } else { + callback(null, null) + } } - } - - /** - * Callback for established connections - * - * @param {Error} error - * - * @private - * @returns {void} - */ - _onConnection( error ) { - if( error ) { - this.emit( 'error', error ) - } else { - this._tableManager.refreshTables( () => { - this.isReady = true - this.emit( 'ready' ) - } ) + + /** + * Deletes an entry from the cache. + * + * @param {String} key + * @param {Function} callback Will be called with null for successful deletions or with + * an error message string + * + * @public + * @returns {void} + */ + delete(key, callback) { + const params = this._getParams(key) + + if (this._tableManager.hasTable(params.table)) { + rethinkdb.table(params.table).get(params.id).delete().run(this._connection.connection, callback); + } else { + callback(new Error('Table \'' + params.table + '\' does not exist')); + } + } + + /** + * Callback for established connections + * + * @param {Error} error + * + * @private + * @returns {void} + */ + _onConnection(error) { + if (error) { + this.emit('error', error); + } else { + this._tableManager.refreshTables(() => { + this.isReady = true + this.emit('ready') + }); + } } - } - - /** - * Parses the provided record name and returns an object - * containing a table name and a record name - * - * @param {String} key the name of the record - * - * @private - * @returns {Object} params - */ - _getParams( key ) { - const table = key.match( this._tableMatch ) - var params = { table: this._defaultTable, id: key } - - if( table ) { - params.table = table[1] - params.id = key.substr(table[1].length + 1) + + /** + * Parses the provided record name and returns an object + * containing a table name and a record name + * + * @param {String} key the name of the record + * + * @private + * @returns {Object} params + */ + _getParams(key) { + const table = key.match(this._tableMatch) + var params = { + table: this._defaultTable, + id: key + } + + if (table) { + params.table = table[1] + params.id = key.substr(table[1].length + 1) + } + + // rethink can't have a key > 127 bytes; hash key and store alongside + if (params.id.length > 127) { + params.fullKey = params.id; + params.id = crypto.createHash('sha256').update(params.id).digest('hex'); + } + + return params } - // rethink can't have a key > 127 bytes; hash key and store alongside - if( params.id.length > 127 ) { - params.fullKey = params.id; - params.id = crypto.createHash( 'sha256' ).update( params.id ).digest( 'hex' ); + /** + * Augments a value with a primary key and writes it to the database + * + * @param {Object} params Map in the format { table: String, id: String } + * @param {Object} value The value that will be written + * @param {Function} callback called with error or null + * + * @private + * @returns {void} + */ + _insert(params, value, callback) { + value[this._primaryKey] = params.id + if (params.fullKey) { + value.__key = params.fullKey + } + + rethinkdb + .table(params.table) + .insert(value, { + returnChanges: false, + conflict: 'replace' + }) + .run(this._connection.connection, this._errorHandler(callback, this._insert, arguments)) } - return params - } - - /** - * Augments a value with a primary key and writes it to the database - * - * @param {Object} params Map in the format { table: String, id: String } - * @param {Object} value The value that will be written - * @param {Function} callback called with error or null - * - * @private - * @returns {void} - */ - _insert( params, value, callback ) { - value[ this._primaryKey ] = params.id - if( params.fullKey ) { - value.__key = params.fullKey + /** + * Makes sure that the options object contains all mandatory + * settings + * + * @param {Object} options + * + * @private + * @returns {void} + */ + _checkOptions(options) { + if (typeof options.host !== 'string') { + throw new Error('Missing option host') + } + if (isNaN(options.port)) { + throw new Error('Missing option port') + } } - rethinkdb - .table( params.table ) - .insert( value, { returnChanges: false, conflict: 'replace' } ) - .run( this._connection.get(), callback ) - } - - /** - * Makes sure that the options object contains all mandatory - * settings - * - * @param {Object} options - * - * @private - * @returns {void} - */ - _checkOptions( options ) { - if( typeof options.host !== 'string' ) { - throw new Error( 'Missing option host' ) + /** + * Escapes user input for use in a regular expression + * + * @param {String} string the user input + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + * @copyright public domain + * + * @private + * @returns {String} escaped user input + */ + _escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string } - if( isNaN( options.port ) ) { - throw new Error( 'Missing option port' ) + + /** + * Handle reconnection: checks if the error is for lost connection, if so, it reconnects for te configured times. + * + * @param {Function} callback The "normal" user callback that must be passed with/without error + * @param {Function} fv The method of this class to be retried + * @param {Array} args The argument array of fv + * + * @private + * @returns {void} + */ + _errorHandler(callback, fv, args) { + return (error, entry) => { + // If there is NO error, then we can safely reset the reconnect count to the original value + if (!error) { + this._reconnectCount = this._connection.reconnectCount + } + + if (error && error.msg === 'Connection is closed.' && !!this._reconnectCount) { + setTimeout(() => { + return this._connection.reconnect() + .then(() => { + this._reconnectCount-- + fv.apply(this, args) + }) + .catch(err => callback(error, entry)) + }, this._connection.reconnectTimeout) + } else { + callback(error, entry); + } + } } - } - - /** - * Escapes user input for use in a regular expression - * - * @param {String} string the user input - * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - * @copyright public domain - * - * @private - * @returns {String} escaped user input - */ - _escapeRegExp( string ) { - return string.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ) // $& means the whole matched string - } } -module.exports = Connector + +module.exports = Connector; \ No newline at end of file diff --git a/src/table-manager.js b/src/table-manager.js index 85936f7..3bb6da2 100644 --- a/src/table-manager.js +++ b/src/table-manager.js @@ -1,98 +1,101 @@ "use strict" -var EventEmitter = require( 'events' ).EventEmitter, - rethinkdb = require( 'rethinkdb' ) +var EventEmitter = require('events').EventEmitter, + rethinkdb = require('rethinkdb') class TableManager extends EventEmitter { - constructor( connection ) { - super() - this._connection = connection - this._tables = [] - this._eventEmitter = new EventEmitter() - this._eventEmitter.setMaxListeners( 0 ) - } + constructor(connection) { + super() + this._connection = connection + this._tables = [] + this._eventEmitter = new EventEmitter() + this._eventEmitter.setMaxListeners(0) + } - /** - * Creates the table if it doesn't exist yet - * - * @private - * @returns {void} - */ - createTable( table, primary_key, callback ) { - this._eventEmitter.once( table, callback ) + /** + * Creates the table if it doesn't exist yet + * + * @private + * @returns {void} + */ + createTable(table, primary_key, callback) { + this._eventEmitter.once(table, callback) - if( this._eventEmitter.listeners( table ).length === 1 ) { - rethinkdb - .tableCreate( table, { primaryKey: primary_key, durability: 'soft' } ) - .run( this._connection.get(), this._onTableCreated.bind( this, table ) ) + if (this._eventEmitter.listeners(table).length === 1) { + rethinkdb + .tableCreate(table, { + primaryKey: primary_key, + durability: 'soft' + }) + .run(this._connection.connection, this._onTableCreated.bind(this, table)) + } } - } - /** - * Checks if a specific table name exists. The list of tables is retrieved - * on initialisation and can be updated at runtime using refreshTables - * - * @param {String} table the name of the table - * - * @public - * @returns {Boolean} hasTable - */ - hasTable( table ) { - return this._tables.indexOf( table ) !== -1 - } + /** + * Checks if a specific table name exists. The list of tables is retrieved + * on initialisation and can be updated at runtime using refreshTables + * + * @param {String} table the name of the table + * + * @public + * @returns {Boolean} hasTable + */ + hasTable(table) { + return this._tables.indexOf(table) !== -1 + } - /** - * Called whenever the list of tables has gotten out of sync. E.g. after - * receiving a "table exists" - * - * @public - * @returns {void} - */ - refreshTables( callback ) { - rethinkdb - .tableList() - .run( this._connection.get() ) - .bind( this ) - .then( function( tables ) { - this._tables = tables + /** + * Called whenever the list of tables has gotten out of sync. E.g. after + * receiving a "table exists" + * + * @public + * @returns {void} + */ + refreshTables(callback) { + rethinkdb + .tableList() + .run(this._connection.connection) + .bind(this) + .then(function(tables) { + this._tables = tables - if( callback ) { - callback() - } - }) - } + if (callback) { + callback() + } + }) + } - /** - * Callback for tableCreate. If an error is received and it is a "table already exists" - * error, tell the connection to refresh its table-cache, otherwise propagate the error - * - * If everything worked, insert the entry - * - * @private - * @returns {void} - */ - _onTableCreated( table, error ) { - this.refreshTables() + /** + * Callback for tableCreate. If an error is received and it is a "table already exists" + * error, tell the connection to refresh its table-cache, otherwise propagate the error + * + * If everything worked, insert the entry + * + * @private + * @returns {void} + */ + _onTableCreated(table, error) { + this.refreshTables() - if( error && this._isTableExistsError( error ) === false ) { - this._eventEmitter.emit( table, error ) - } else { - this._eventEmitter.emit( table, null ) + if (error && this._isTableExistsError(error) === false) { + this._eventEmitter.emit(table, error) + } else { + this._eventEmitter.emit(table, null) + } } - } - /** - * If tableCreate is called for an existing table, rethinkdb returns a - * RqlRuntimeError. This error unfortunately doesn't come with a code or constant to check - * its type, so this method tries to parse its error message instead - * - * @private - * @returns {void} - */ - _isTableExistsError( error ) { - return error.msg.indexOf( 'already exists' ) !== -1 - } + /** + * If tableCreate is called for an existing table, rethinkdb returns a + * RqlRuntimeError. This error unfortunately doesn't come with a code or constant to check + * its type, so this method tries to parse its error message instead + * + * @private + * @returns {void} + */ + _isTableExistsError(error) { + return error.msg.indexOf('already exists') !== -1 + } } module.exports = TableManager \ No newline at end of file diff --git a/test/cache-connectorSpec.js b/test/cache-connectorSpec.js index 44aa44c..0bfb176 100644 --- a/test/cache-connectorSpec.js +++ b/test/cache-connectorSpec.js @@ -2,87 +2,103 @@ 'use strict' const expect = require('chai').expect -const CacheConnector = require( '../src/connector' ) -const EventEmitter = require( 'events' ).EventEmitter -const rethinkdb = require( 'rethinkdb' ) -const connectionParams = require( './connection-params' ) -const MESSAGE_TIME = 20 +const CacheConnector = require('../src/connector') +const EventEmitter = require('events').EventEmitter +const connectionParams = require('./connection-params') -describe( 'the message connector has the correct structure', () => { - var cacheConnector +describe('the message connector has the correct structure', () => { + var cacheConnector - it( 'throws an error if required connection parameters are missing', () => { - expect(() => { new CacheConnector( 'gibberish' ) }).to.throw() - }) + it('throws an error if required connection parameters are missing', () => { + expect(() => { + new CacheConnector('gibberish') + }).to.throw() + }) - it( 'creates the cacheConnector', ( done ) => { - cacheConnector = new CacheConnector( connectionParams ) - expect( cacheConnector.isReady ).to.equal( false ) - cacheConnector.on( 'ready', done ) - cacheConnector.on( 'error', ( error ) => { - console.log( error ) + it('creates the cacheConnector', (done) => { + cacheConnector = new CacheConnector(connectionParams) + expect(cacheConnector.isReady).to.equal(false) + cacheConnector.on('ready', done) + cacheConnector.on('error', (error) => { + console.log(error) + }) }) - }) - it( 'implements the cache/storage connector interface', () => { - expect( typeof cacheConnector.name ).to.equal( 'string' ) - expect( typeof cacheConnector.version ).to.equal( 'string' ) - expect( typeof cacheConnector.get ).to.equal( 'function' ) - expect( typeof cacheConnector.set ).to.equal( 'function' ) - expect( typeof cacheConnector.delete ).to.equal( 'function' ) - expect( cacheConnector instanceof EventEmitter ).to.equal( true ) - }) + it('implements the cache/storage connector interface', () => { + expect(typeof cacheConnector.name).to.equal('string') + expect(typeof cacheConnector.version).to.equal('string') + expect(typeof cacheConnector.get).to.equal('function') + expect(typeof cacheConnector.set).to.equal('function') + expect(typeof cacheConnector.delete).to.equal('function') + expect(cacheConnector instanceof EventEmitter).to.equal(true) + }) - it( 'retrieves a non existing value', ( done ) => { - cacheConnector.get( 'someValue', ( error, value ) => { - expect( error ).to.equal( null ) - expect( value ).to.equal( null ) - done() + it('retrieves a non existing value', (done) => { + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.equal(null) + done() + }) }) - }) - it( 'sets a value', ( done ) => { - cacheConnector.set( 'someValue', { _d: { firstname: 'Wolfram' } }, ( error ) => { - expect( error ).to.equal( null ) - done() + it('sets a value', (done) => { + cacheConnector.set('someValue', { + _d: { + firstname: 'Wolfram' + } + }, (error) => { + expect(error).to.equal(null) + done() + }) }) - }) - it( 'retrieves an existing value', ( done ) => { - cacheConnector.get( 'someValue', ( error, value ) => { - expect( error ).to.equal( null ) - expect( value ).to.deep.equal( { _d: { firstname: 'Wolfram' } } ) - done() + it('retrieves an existing value', (done) => { + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.deep.equal({ + _d: { + firstname: 'Wolfram' + } + }) + done() + }) }) - }) - it( 'updates an existing value', ( done ) => { - cacheConnector.set( 'someValue', { _d: { firstname: 'Egon' } }, ( error ) => { - expect( error ).to.equal( null ) - done() + it('updates an existing value', (done) => { + cacheConnector.set('someValue', { + _d: { + firstname: 'Egon' + } + }, (error) => { + expect(error).to.equal(null) + done() + }) }) - }) - it( 'retrieves the updated value', ( done ) => { - cacheConnector.get( 'someValue', ( error, value ) => { - expect( error ).to.equal( null ) - expect( value ).to.deep.equal( { _d: { firstname: 'Egon' } } ) - done() + it('retrieves the updated value', (done) => { + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.deep.equal({ + _d: { + firstname: 'Egon' + } + }) + done() + }) }) - }) - it( 'deletes a value', ( done ) => { - cacheConnector.delete( 'someValue', ( error ) => { - expect( error ).to.equal( null ) - done() + it('deletes a value', (done) => { + cacheConnector.delete('someValue', (error) => { + expect(error).to.equal(null) + done() + }) }) - }) - it( 'Can\'t retrieve a deleted value', ( done ) => { - cacheConnector.get( 'someValue', ( error, value ) => { - expect( error ).to.equal( null ) - expect( value ).to.equal( null ) - done() + it('Can\'t retrieve a deleted value', (done) => { + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.equal(null) + done() + }) }) - }) }) \ No newline at end of file diff --git a/test/connection-handling.js b/test/connection-handling.js new file mode 100644 index 0000000..eeb6635 --- /dev/null +++ b/test/connection-handling.js @@ -0,0 +1,130 @@ +/* global describe, expect, it, jasmine */ +'use strict' + +const expect = require('chai').expect + +const CacheConnector = require('../src/connector') +const connectionParams = require('./connection-params') + +describe('It can recover from a lost connection', () => { + var cacheConnector + it('creates the cacheConnector', (done) => { + const cp = Object.assign({}, connectionParams); + cp.reconnectCount = 1 + cp.reconnectTimeout = 100 + cacheConnector = new CacheConnector(cp) + expect(cacheConnector.isReady).to.equal(false) + cacheConnector.on('ready', done) + cacheConnector.on('error', (error) => { + console.log(error) + }) + }) + + it('shuts down connection then trying to write', (done) => { + cacheConnector._connection._connection.close({ + noreplyWait: true + }).then(() => { + cacheConnector.set('someValue', { + _d: { + firstname: 'Wolfram' + } + }, (error) => { + expect(error).to.equal(null) + done() + }) + }) + }) + + it('shuts down connection then trying to read', (done) => { + cacheConnector._connection._connection.close({ + noreplyWait: true + }).then(() => { + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.deep.equal({ + _d: { + firstname: 'Wolfram' + } + }) + done() + }) + }) + }) + + it('shuts down connection then trying to delete', (done) => { + cacheConnector._connection._connection.close({ + noreplyWait: true + }).then(() => { + cacheConnector.get('someValue', (error, value) => { + cacheConnector.delete('someValue', (error) => { + expect(error).to.equal(null) + + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.equal(null) + done() + }) + }) + }) + }) + }) + + it('should not reconnect if there is no reconnect count defined', (done) => { + // The original connectionParams has no reconnectCount + let cc = new CacheConnector(connectionParams) + cc.on('ready', () => { + cc._connection._connection.close({ + noreplyWait: true + }).then(() => { + cc.set('someValue', { + _d: { + firstname: 'Wolfram' + } + }, (error) => { + expect(error.msg).to.equal('Connection is closed.') + done() + }) + }) + }) + }) + + it('should not report error if the desired reconnection exceeded', (done) => { + const cp = Object.assign({}, connectionParams); + cp.reconnectCount = 0 + let cc = new CacheConnector(cp) + cc.on('ready', () => { + cc._connection._connection.close({ + noreplyWait: true + }).then(() => { + cc.set('someValue', { + _d: { + firstname: 'Wolfram' + } + }, (error) => { + expect(error.msg).to.equal('Connection is closed.') + done() + }) + }) + }) + }) + + it('should handle the default timeout', (done) => { + const cp = Object.assign({}, connectionParams); + cp.reconnectCount = 1 + let cc = new CacheConnector(cp) + cc.on('ready', () => { + cc._connection._connection.close({ + noreplyWait: true + }).then(() => { + cc.set('someValue', { + _d: { + firstname: 'Wolfram' + } + }, (error) => { + expect(error).to.equal(null) + done() + }) + }) + }) + }).timeout(3000) +}) \ No newline at end of file diff --git a/test/fast-insertionSpec.js b/test/fast-insertionSpec.js index 05c1cea..dac39aa 100644 --- a/test/fast-insertionSpec.js +++ b/test/fast-insertionSpec.js @@ -53,7 +53,7 @@ describe( 'Is able to insert a larger number of values in quick succession', () }) it( 'deletes dsTestA', ( done ) => { - const conn = storageConnector._connection.get() + const conn = storageConnector._connection.connection rethinkdb.tableDrop( 'quickInsertTestTable' ).run(conn, () => { done() } ) }) }) diff --git a/test/multi-tableSpec.js b/test/multi-tableSpec.js index befd89d..15d08ab 100644 --- a/test/multi-tableSpec.js +++ b/test/multi-tableSpec.js @@ -29,7 +29,7 @@ describe( 'it distributes records between multiple tables', () => { }) it( 'deletes dsTestA', ( done ) => { - conn = cacheConnector._connection.get() + conn = cacheConnector._connection.connection rethinkdb.tableDrop( 'dsTestA' ).run(conn, () => { done() } ) }) From 3ac44a6ba8b5a3024992968e72e554455e427404 Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Wed, 18 Jul 2018 20:22:55 +0200 Subject: [PATCH 2/8] chore: doc formatting fix --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c4d5573..89d5a85 100644 --- a/README.md +++ b/README.md @@ -30,36 +30,36 @@ plugins: ```javascript { - //The host that RethinkDb is listening on - host: 'localhost', + //The host that RethinkDb is listening on + host: 'localhost', - //The port that RethinkDb is listening on - port: 28015, + //The port that RethinkDb is listening on + port: 28015, - //(Optional) Authentication key for RethinkDb - authKey: 'someString', + //(Optional) Authentication key for RethinkDb + authKey: 'someString', - //(Optional, defaults to 'deepstream') - database: 'someDb', + //(Optional, defaults to 'deepstream') + database: 'someDb', - //(Optional, defaults to 'deepstream_records') - defaultTable: 'someTable', + //(Optional, defaults to 'deepstream_records') + defaultTable: 'someTable', - /* (Optional) A character that's used as part of the - * record names to split it into a tabel and an id part, e.g. - * - * books/dream-of-the-red-chamber - * - * would create a table called 'books' and store the record under the name - * 'dream-of-the-red-chamber' - */ + /* (Optional) A character that's used as part of the + * record names to split it into a tabel and an id part, e.g. + * + * books/dream-of-the-red-chamber + * + * would create a table called 'books' and store the record under the name + * 'dream-of-the-red-chamber' + */ splitChar: '/', // (Optional, defaults to 0) If the rhe rethinkdb connection is lost, try to reconnect max. this number of times reconnectCount: 2, // (Optional, defaults to 2000) Timeout between reconnect attempts in milliseconds - reconnectTimeout: 3000 + reconnectTimeout: 3000 } ``` From d578885533c94fdcb3005b1d4e46c65c5f721bf3 Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Wed, 18 Jul 2018 22:18:18 +0200 Subject: [PATCH 3/8] fix: reconnect/retry logic improved --- src/connector.js | 4 ++-- test/connection-handling.js | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/connector.js b/src/connector.js index f28f08b..c0bfc7d 100644 --- a/src/connector.js +++ b/src/connector.js @@ -256,14 +256,14 @@ class Connector extends EventEmitter { this._reconnectCount = this._connection.reconnectCount } - if (error && error.msg === 'Connection is closed.' && !!this._reconnectCount) { + if (error && (error.msg === 'Connection is closed.' || error.msg.match(/Could not connect to/)) && !!this._reconnectCount) { setTimeout(() => { return this._connection.reconnect() .then(() => { this._reconnectCount-- fv.apply(this, args) }) - .catch(err => callback(error, entry)) + .catch(err => fv.apply(this, args)) }, this._connection.reconnectTimeout) } else { callback(error, entry); diff --git a/test/connection-handling.js b/test/connection-handling.js index eeb6635..5905e0e 100644 --- a/test/connection-handling.js +++ b/test/connection-handling.js @@ -8,10 +8,11 @@ const connectionParams = require('./connection-params') describe('It can recover from a lost connection', () => { var cacheConnector + it('creates the cacheConnector', (done) => { const cp = Object.assign({}, connectionParams); - cp.reconnectCount = 1 - cp.reconnectTimeout = 100 + cp.reconnectCount = 10000 + cp.reconnectTimeout = 1000 cacheConnector = new CacheConnector(cp) expect(cacheConnector.isReady).to.equal(false) cacheConnector.on('ready', done) From 02ed9223834b8298b95f7af5d280a15352180d5a Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Wed, 18 Jul 2018 22:26:36 +0200 Subject: [PATCH 4/8] fix: reconnect/retry logic improved --- src/connector.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/connector.js b/src/connector.js index c0bfc7d..d01a846 100644 --- a/src/connector.js +++ b/src/connector.js @@ -257,6 +257,7 @@ class Connector extends EventEmitter { } if (error && (error.msg === 'Connection is closed.' || error.msg.match(/Could not connect to/)) && !!this._reconnectCount) { + console.log("Lost rethinkdb connection, will reconnect...", error) setTimeout(() => { return this._connection.reconnect() .then(() => { From 6924fec06a02bc5001b9215d98a8993921b85ed7 Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Wed, 18 Jul 2018 23:29:33 +0200 Subject: [PATCH 5/8] fix: reconnect/retry logic improved --- src/connection.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/connection.js b/src/connection.js index 82664da..bb35b84 100644 --- a/src/connection.js +++ b/src/connection.js @@ -22,6 +22,7 @@ class Connection { this._database = options.database || 'deepstream' options.db = this._database rethinkdb.connect(options, this._fn(this._onConnection)) + console.log("CONNECTION ESTABLISED") } /** From 5ce9219f347e4ab0f2abee12d4480d801cc27733 Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Thu, 19 Jul 2018 00:53:30 +0200 Subject: [PATCH 6/8] fix: reconnect logic fix --- README.md | 2 +- src/connection.js | 2 +- src/connector.js | 17 ++++++++--------- test/connection-handling.js | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 89d5a85..480ee3d 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ plugins: // (Optional, defaults to 0) If the rhe rethinkdb connection is lost, try to reconnect max. this number of times reconnectCount: 2, - // (Optional, defaults to 2000) Timeout between reconnect attempts in milliseconds + // (Optional, defaults to 2000) Timeout between reconnect attempts in milliseconds. It cannot be less than 1000, if you specify less than 1000, then it will be set to 1000! reconnectTimeout: 3000 } ``` diff --git a/src/connection.js b/src/connection.js index bb35b84..0ed691e 100644 --- a/src/connection.js +++ b/src/connection.js @@ -52,7 +52,7 @@ class Connection { * @returns {Number} the timeout in ms */ get reconnectTimeout() { - return this._options.reconnectTimeout >= 0 ? this._options.reconnectTimeout : 2000; + return this._options.reconnectTimeout >= 1000 ? this._options.reconnectTimeout : 2000; } /** diff --git a/src/connector.js b/src/connector.js index d01a846..707f978 100644 --- a/src/connector.js +++ b/src/connector.js @@ -257,15 +257,14 @@ class Connector extends EventEmitter { } if (error && (error.msg === 'Connection is closed.' || error.msg.match(/Could not connect to/)) && !!this._reconnectCount) { - console.log("Lost rethinkdb connection, will reconnect...", error) - setTimeout(() => { - return this._connection.reconnect() - .then(() => { - this._reconnectCount-- - fv.apply(this, args) - }) - .catch(err => fv.apply(this, args)) - }, this._connection.reconnectTimeout) + console.log("Lost rethinkdb connection, will reconnect ", this._reconnectCount, " times") + this._reconnectCount-- + + setTimeout(() => { + return this._connection.reconnect() + .then(() => fv.apply(this, args)) + .catch(err => fv.apply(this, args)) + }, this._connection.reconnectTimeout) } else { callback(error, entry); } diff --git a/test/connection-handling.js b/test/connection-handling.js index 5905e0e..0eaa280 100644 --- a/test/connection-handling.js +++ b/test/connection-handling.js @@ -11,7 +11,7 @@ describe('It can recover from a lost connection', () => { it('creates the cacheConnector', (done) => { const cp = Object.assign({}, connectionParams); - cp.reconnectCount = 10000 + cp.reconnectCount = 3 cp.reconnectTimeout = 1000 cacheConnector = new CacheConnector(cp) expect(cacheConnector.isReady).to.equal(false) From a7b6bb2ed29b81c1806f4da2ab01ea8dad575876 Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Thu, 19 Jul 2018 01:06:06 +0200 Subject: [PATCH 7/8] fix: reconnect logic fix --- src/connection.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/connection.js b/src/connection.js index 0ed691e..a43d0fb 100644 --- a/src/connection.js +++ b/src/connection.js @@ -22,7 +22,6 @@ class Connection { this._database = options.database || 'deepstream' options.db = this._database rethinkdb.connect(options, this._fn(this._onConnection)) - console.log("CONNECTION ESTABLISED") } /** From ee5e770fecd68f799c6957cae2a783f0382540a3 Mon Sep 17 00:00:00 2001 From: Zsolt Molnar Date: Thu, 19 Jul 2018 13:41:15 +0200 Subject: [PATCH 8/8] fix: retry logic based on rxjs sared observables --- package-lock.json | 1320 +++++++++++++++++++++++ package.json | 3 +- src/connection.js | 7 +- src/connector.js | 45 +- test/cache-connectorSpec-primary-key.js | 152 +-- test/cache-connectorSpec.js | 2 +- test/connection-handling.js | 176 ++- 7 files changed, 1570 insertions(+), 135 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c33529f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1320 @@ +{ + "name": "deepstream.io-storage-rethinkdb", + "version": "1.0.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chai": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true, + "requires": { + "assertion-error": "^1.0.1", + "deep-eql": "^0.1.3", + "type-detect": "^1.0.0" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", + "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "coveralls": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.3.tgz", + "integrity": "sha512-iiAmn+l1XqRwNLXhW8Rs5qHZRFMYp9ZIPjEOVRpC/c4so6Y/f4/lFi0FfR5B9cCqgyhkJ5cZmbvcVRfP8MHchw==", + "dev": true, + "requires": { + "js-yaml": "3.6.1", + "lcov-parse": "0.0.10", + "log-driver": "1.2.5", + "minimist": "1.2.0", + "request": "2.79.0" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.x.x" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "optional": true + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + }, + "dependencies": { + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", + "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "js-yaml": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "log-driver": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", + "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "dev": true, + "requires": { + "mime-db": "~1.35.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", + "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "he": "1.1.1", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "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": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~2.0.6", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "qs": "~6.3.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "~0.4.1", + "uuid": "^3.0.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "rethinkdb": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/rethinkdb/-/rethinkdb-2.3.3.tgz", + "integrity": "sha1-PcZYbiL6HavuDSVOZL0ON5+tL3I=", + "requires": { + "bluebird": ">= 2.3.2 < 3" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rxjs": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz", + "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "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 2ab6945..23607e2 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ }, "homepage": "http://deepstream.io", "dependencies": { - "rethinkdb": "2.3.3" + "rethinkdb": "2.3.3", + "rxjs": "^6.2.2" }, "devDependencies": { "chai": "^3.5.0", diff --git a/src/connection.js b/src/connection.js index a43d0fb..c8f91b9 100644 --- a/src/connection.js +++ b/src/connection.js @@ -63,8 +63,13 @@ class Connection { * @returns {RethinkDB Connection} connection as promise */ reconnect() { - if (this.reconnectCount) + if (this.reconnectCount) { + console.log("Lost rethinkdb connection, reconnecting...") return rethinkdb.connect(this._options).then(conn => this._connection = conn) + } else { + console.log("Lost rethinkdb connection, no reconnection allowed...") + return Promise.resolve() + } } /** diff --git a/src/connector.js b/src/connector.js index 707f978..e2e076c 100644 --- a/src/connector.js +++ b/src/connector.js @@ -3,6 +3,20 @@ const crypto = require('crypto') const EventEmitter = require('events').EventEmitter const rethinkdb = require('rethinkdb') +const { + from, + of, + pipe, +} = require('rxjs') + +const { + switchMap, + retryWhen, + share, + delay, + take, +} = require('rxjs/operators') + const Connection = require('./connection') const TableManager = require('./table-manager') const dataTransform = require('./transform-data') @@ -51,7 +65,6 @@ class Connector extends EventEmitter { this._checkOptions(options) this._options = options this._connection = new Connection(options, this._onConnection.bind(this)) - this._reconnectCount = this._connection.reconnectCount this._tableManager = new TableManager(this._connection) this._defaultTable = options.defaultTable || 'deepstream_records' this._splitChar = options.splitChar || null @@ -59,6 +72,16 @@ class Connector extends EventEmitter { new RegExp('^(\\w+)' + this._escapeRegExp(this._splitChar)) : null this._primaryKey = options.primaryKey || PRIMARY_KEY + + // The reconnection logic. Failing operations must subscribe to it to trigger reconnection. + this._reconnect$ = of(true).pipe( + delay(this._connection.reconnectTimeout), + switchMap(() => from(this._connection.reconnect()).pipe( + retryWhen(errors => errors.pipe( + delay(this._connection.reconnectTimeout), + take(this._connection.reconnectCount) + )))), + share()) } /** @@ -126,7 +149,7 @@ class Connector extends EventEmitter { const params = this._getParams(key) if (this._tableManager.hasTable(params.table)) { - rethinkdb.table(params.table).get(params.id).delete().run(this._connection.connection, callback); + rethinkdb.table(params.table).get(params.id).delete().run(this._connection.connection, (error, entry) => this._errorHandler(callback, this.delete, arguments)(error, entry)); } else { callback(new Error('Table \'' + params.table + '\' does not exist')); } @@ -251,23 +274,11 @@ class Connector extends EventEmitter { */ _errorHandler(callback, fv, args) { return (error, entry) => { - // If there is NO error, then we can safely reset the reconnect count to the original value - if (!error) { - this._reconnectCount = this._connection.reconnectCount + if (error && (error.msg === 'Connection is closed.' || error.msg.match(/Could not connect to/))) { + this._reconnect$.subscribe() } - if (error && (error.msg === 'Connection is closed.' || error.msg.match(/Could not connect to/)) && !!this._reconnectCount) { - console.log("Lost rethinkdb connection, will reconnect ", this._reconnectCount, " times") - this._reconnectCount-- - - setTimeout(() => { - return this._connection.reconnect() - .then(() => fv.apply(this, args)) - .catch(err => fv.apply(this, args)) - }, this._connection.reconnectTimeout) - } else { - callback(error, entry); - } + callback(error, entry); } } } diff --git a/test/cache-connectorSpec-primary-key.js b/test/cache-connectorSpec-primary-key.js index 802104f..7277580 100644 --- a/test/cache-connectorSpec-primary-key.js +++ b/test/cache-connectorSpec-primary-key.js @@ -2,91 +2,109 @@ 'use strict' const expect = require('chai').expect -const CacheConnector = require( '../src/connector' ) -const EventEmitter = require( 'events' ).EventEmitter -const connectionParams = require( './connection-params' ) +const CacheConnector = require('../src/connector') +const EventEmitter = require('events').EventEmitter +const connectionParams = require('./connection-params') const MESSAGE_TIME = 20 -describe( 'the message connector has the correct structure', () => { - var cacheConnector +describe('the message connector has the correct structure', () => { + var cacheConnector - it( 'throws an error if required connection parameters are missing', () => { - expect(() => { new CacheConnector( 'gibberish' ) }).to.throw() - }) + it('throws an error if required connection parameters are missing', () => { + expect(() => { + new CacheConnector('gibberish') + }).to.throw() + }) - it( 'creates the cacheConnector', ( done ) => { - cacheConnector = new CacheConnector( { - host: connectionParams.host, - port: connectionParams.port, - database: connectionParams.database + 2, - primaryKey: 'own-primary-key' - } ) - expect( cacheConnector.isReady ).to.equal( false ) - cacheConnector.on( 'ready', done ) - cacheConnector.on( 'error', ( error ) => { - console.log( error ) + it('creates the cacheConnector', (done) => { + cacheConnector = new CacheConnector({ + host: connectionParams.host, + port: connectionParams.port, + database: connectionParams.database + 2, + primaryKey: 'own-primary-key' + }) + expect(cacheConnector.isReady).to.equal(false) + cacheConnector.on('ready', done) + cacheConnector.on('error', (error) => { + console.log(error) + }) }) - }) - it( 'implements the cache/storage connector interface', () => { - expect( typeof cacheConnector.name ).to.equal( 'string' ) - expect( typeof cacheConnector.version ).to.equal( 'string' ) - expect( typeof cacheConnector.get ).to.equal( 'function' ) - expect( typeof cacheConnector.set ).to.equal( 'function' ) - expect( typeof cacheConnector.delete ).to.equal( 'function' ) - expect( cacheConnector instanceof EventEmitter ).to.equal( true ) - }) + it('implements the cache/storage connector interface', () => { + expect(typeof cacheConnector.name).to.equal('string') + expect(typeof cacheConnector.version).to.equal('string') + expect(typeof cacheConnector.get).to.equal('function') + expect(typeof cacheConnector.set).to.equal('function') + expect(typeof cacheConnector.delete).to.equal('function') + expect(cacheConnector instanceof EventEmitter).to.equal(true) + }) - it( 'retrieves a non existing value', ( done ) => { - cacheConnector.get( 'someValue', ( error, value ) => { - expect( error ).to.equal( null ) - expect( value ).to.equal( null ) - done() + it('retrieves a non existing value', (done) => { + cacheConnector.get('someNonexistingValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.equal(null) + done() + }) }) - }) - it( 'sets a value', ( done ) => { - cacheConnector.set( 'someValue', { _d: { firstname: 'Wolfram' } }, ( error ) => { - expect( error ).to.equal( null ) - done() + it('sets a value', (done) => { + cacheConnector.set('someValue', { + _d: { + firstname: 'Wolfram' + } + }, (error) => { + expect(error).to.equal(null) + done() + }) }) - }) - it( 'retrieves an existing value', ( done ) => { - cacheConnector.get( 'someValue', ( error, value ) => { - expect( error ).to.equal( null ) - expect( value ).to.deep.equal( { _d: { firstname: 'Wolfram' } } ) - done() + it('retrieves an existing value', (done) => { + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.deep.equal({ + _d: { + firstname: 'Wolfram' + } + }) + done() + }) }) - }) - it( 'updates an existing value', ( done ) => { - cacheConnector.set( 'someValue', { _d: { firstname: 'Egon' } }, ( error ) => { - expect( error ).to.equal( null ) - done() + it('updates an existing value', (done) => { + cacheConnector.set('someValue', { + _d: { + firstname: 'Egon' + } + }, (error) => { + expect(error).to.equal(null) + done() + }) }) - }) - it( 'retrieves the updated value', ( done ) => { - cacheConnector.get( 'someValue', ( error, value ) => { - expect( error ).to.equal( null ) - expect( value ).to.deep.equal( { _d : { firstname: 'Egon' } } ) - done() + it('retrieves the updated value', (done) => { + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.deep.equal({ + _d: { + firstname: 'Egon' + } + }) + done() + }) }) - }) - it( 'deletes a value', ( done ) => { - cacheConnector.delete( 'someValue', ( error ) => { - expect( error ).to.equal( null ) - done() + it('deletes a value', (done) => { + cacheConnector.delete('someValue', (error) => { + expect(error).to.equal(null) + done() + }) }) - }) - it( 'Can\'t retrieve a deleted value', ( done ) => { - cacheConnector.get( 'someValue', ( error, value ) => { - expect( error ).to.equal( null ) - expect( value ).to.equal( null ) - done() + it('Can\'t retrieve a deleted value', (done) => { + cacheConnector.get('someValue', (error, value) => { + expect(error).to.equal(null) + expect(value).to.equal(null) + done() + }) }) - }) }) \ No newline at end of file diff --git a/test/cache-connectorSpec.js b/test/cache-connectorSpec.js index 0bfb176..fe4e263 100644 --- a/test/cache-connectorSpec.js +++ b/test/cache-connectorSpec.js @@ -34,7 +34,7 @@ describe('the message connector has the correct structure', () => { }) it('retrieves a non existing value', (done) => { - cacheConnector.get('someValue', (error, value) => { + cacheConnector.get('someNonexistingValue', (error, value) => { expect(error).to.equal(null) expect(value).to.equal(null) done() diff --git a/test/connection-handling.js b/test/connection-handling.js index 0eaa280..ea7a4c2 100644 --- a/test/connection-handling.js +++ b/test/connection-handling.js @@ -2,6 +2,14 @@ 'use strict' const expect = require('chai').expect +const { + Subject, + pipe +} = require('rxjs') + +const { + take +} = require('rxjs/operators') const CacheConnector = require('../src/connector') const connectionParams = require('./connection-params') @@ -9,6 +17,12 @@ const connectionParams = require('./connection-params') describe('It can recover from a lost connection', () => { var cacheConnector + const data = { + _d: { + firstname: 'Wolfram' + } + } + it('creates the cacheConnector', (done) => { const cp = Object.assign({}, connectionParams); cp.reconnectCount = 3 @@ -24,51 +38,59 @@ describe('It can recover from a lost connection', () => { it('shuts down connection then trying to write', (done) => { cacheConnector._connection._connection.close({ noreplyWait: true - }).then(() => { - cacheConnector.set('someValue', { - _d: { - firstname: 'Wolfram' - } - }, (error) => { + }).then(cacheConnector.set('someValue', data, (error) => { + expect(error.msg).to.equal("Connection is closed.") + setTimeout(() => cacheConnector.set('someValue', data, (error) => { expect(error).to.equal(null) done() - }) - }) - }) + }), 1500) + })) + }).timeout(3000) it('shuts down connection then trying to read', (done) => { cacheConnector._connection._connection.close({ noreplyWait: true - }).then(() => { - cacheConnector.get('someValue', (error, value) => { + }).then(cacheConnector.get('someValue', (error) => { + expect(error.msg).to.equal("Connection is closed.") + setTimeout(() => cacheConnector.get('someValue', (error, value) => { expect(error).to.equal(null) - expect(value).to.deep.equal({ - _d: { - firstname: 'Wolfram' - } - }) + expect(value).to.deep.equal(data) done() - }) - }) - }) + }), 1500) + })) + }).timeout(3000) it('shuts down connection then trying to delete', (done) => { cacheConnector._connection._connection.close({ noreplyWait: true - }).then(() => { - cacheConnector.get('someValue', (error, value) => { - cacheConnector.delete('someValue', (error) => { - expect(error).to.equal(null) + }).then(cacheConnector.delete('someValue', (error) => { + expect(error.msg).to.equal("Connection is closed.") + setTimeout(() => cacheConnector.delete('someValue', (err) => { + expect(err).to.equal(null) - cacheConnector.get('someValue', (error, value) => { - expect(error).to.equal(null) - expect(value).to.equal(null) - done() - }) + cacheConnector.get('someValue', (e, value) => { + expect(e).to.equal(null) + expect(value).to.equal(null) + done() }) - }) - }) - }) + }), 1500) + })) + }).timeout(3000) + + // it('shuts down connection then trying to write', (done) => { + // let x = () => { + // cacheConnector.set('someValue', { + // _d: { + // firstname: 'Wolfram' + // } + // }, (error) => { + // setTimeout(x, 1000) + // console.log(error) + // }) + // } + + // x() + // }).timeout(0) it('should not reconnect if there is no reconnect count defined', (done) => { // The original connectionParams has no reconnectCount @@ -77,11 +99,7 @@ describe('It can recover from a lost connection', () => { cc._connection._connection.close({ noreplyWait: true }).then(() => { - cc.set('someValue', { - _d: { - firstname: 'Wolfram' - } - }, (error) => { + cc.set('someValue', data, (error) => { expect(error.msg).to.equal('Connection is closed.') done() }) @@ -97,11 +115,7 @@ describe('It can recover from a lost connection', () => { cc._connection._connection.close({ noreplyWait: true }).then(() => { - cc.set('someValue', { - _d: { - firstname: 'Wolfram' - } - }, (error) => { + cc.set('someValue', data, (error) => { expect(error.msg).to.equal('Connection is closed.') done() }) @@ -117,15 +131,81 @@ describe('It can recover from a lost connection', () => { cc._connection._connection.close({ noreplyWait: true }).then(() => { - cc.set('someValue', { - _d: { - firstname: 'Wolfram' - } - }, (error) => { - expect(error).to.equal(null) - done() + cc.set('someValue', data, (error) => { + expect(error.msg).to.equal("Connection is closed.") + setTimeout(() => cc.set('someValue', data, (error) => { + expect(error).to.equal(null) + done() + }), 2500) }) }) }) }).timeout(3000) + + it('should handle parallel operations as well', (done) => { + let subj$ = new Subject() + subj$.pipe(take(4)).subscribe(undefined, undefined, done) + + cacheConnector._connection._connection.close({ + noreplyWait: true + }).then(() => { + cacheConnector.get('someValue', (error) => { + expect(error.msg).to.equal("Connection is closed.") + subj$.next() + }) + cacheConnector.set('someValue', data, (error) => { + expect(error.msg).to.equal("Connection is closed.") + subj$.next() + }) + + setTimeout(() => { + cacheConnector.get('someValue', (error) => { + expect(error).to.equal(null) + subj$.next() + }) + cacheConnector.set('someValue', data, (error) => { + expect(error).to.equal(null) + subj$.next() + }) + }, 1500) + }) + }).timeout(3000) + + it('should be be able to reconnect multiple times', (done) => { + let subj$ = new Subject() + subj$.pipe(take(4)).subscribe(undefined, undefined, done) + + cacheConnector._connection._connection.close({ + noreplyWait: true + }).then(() => { + cacheConnector.get('someValue', (error) => { + expect(error.msg).to.equal("Connection is closed.") + subj$.next() + }) + + setTimeout(() => { + cacheConnector.get('someValue', (error) => { + expect(error).to.equal(null) + subj$.next() + + cacheConnector._connection._connection.close({ + noreplyWait: true + }) + .then(() => { + cacheConnector.get('someValue', (error) => { + expect(error.msg).to.equal("Connection is closed.") + subj$.next() + }) + + setTimeout(() => { + cacheConnector.get('someValue', (error) => { + expect(error).to.equal(null) + subj$.next() + }) + }, 1500) + }) + }) + }, 1500) + }) + }).timeout(4000) }) \ No newline at end of file