From df8267bb234b990474b56bad1dd0ff17e6cd9bf6 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Sat, 23 Nov 2024 02:30:48 +0100 Subject: [PATCH 01/12] Add files via upload --- README.md | 8 +- benchmark/box.lua | 14 +- benchmark/read.js | 271 +++++++++------- lib/autopipelining.js | 17 + lib/buffer-processor.js | 90 ++++++ lib/commands.js | 636 +++++++++++++++++++++++++------------- lib/connection.js | 145 ++++++--- lib/const.js | 48 ++- lib/event-handler.js | 43 ++- lib/msgpack-extensions.js | 22 +- lib/parser.js | 96 ++++-- lib/transaction.js | 29 ++ package.json | 6 +- test/app.js | 86 +++--- test/box.lua | 7 +- 15 files changed, 1041 insertions(+), 477 deletions(-) create mode 100644 lib/autopipelining.js create mode 100644 lib/buffer-processor.js create mode 100644 lib/transaction.js diff --git a/README.md b/README.md index ccab1e2..34e8880 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Node tarantool driver for 1.7+ support Node.js v.4+. Based on [go-tarantool](https://github.com/tarantool/go-tarantool) and implements [Tarantool’s binary protocol](http://tarantool.org/doc/dev_guide/box-protocol.html), for more information you can read them or basic documentation at [Tarantool manual](http://tarantool.org/doc/). -Code architecture and some features in version 3 borrowed from the [ioredis](https://github.com/luin/ioredis). +Code architecture and some features in version 3 borrowed from the [ioTarantool](https://github.com/luin/ioTarantool). [msgpack-lite](https://github.com/kawanet/msgpack-lite) package used as MsgPack encoder/decoder. @@ -93,7 +93,7 @@ except when the connection is closed manually by `tarantool.disconnect()`. It's very flexible to control how long to wait to reconnect after disconnection using the `retryStrategy` option: -```javascript +```Javascript var tarantool = new Tarantool({ // This is the default value of `retryStrategy` retryStrategy: function (times) { @@ -110,12 +110,12 @@ the return value represents how long (in ms) to wait to reconnect. When the return value isn't a number, node-tarantool-driver will stop trying to reconnect, and the connection will be lost forever if the user doesn't call `tarantool.connect()` manually. -**This feature is borrowed from the [ioredis](https://github.com/luin/ioredis)** +**This feature is borrowed from the [ioTarantool](https://github.com/luin/ioTarantool)** ## Usage example We use TarantoolConnection instance and connect before other operations. Methods call return promise(https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Promise). Available methods with some testing: select, update, replace, insert, delete, auth, destroy. -```javascript +```Javascript var TarantoolConnection = require('tarantool-driver'); var conn = new TarantoolConnection('notguest:sesame@mail.ru:3301'); diff --git a/benchmark/box.lua b/benchmark/box.lua index 7b614ef..54d888f 100755 --- a/benchmark/box.lua +++ b/benchmark/box.lua @@ -1,4 +1,7 @@ -box.cfg{listen=3301} +box.cfg{ + listen=3301, + memtx_use_mvcc_engine=true +} if not box.schema.user.exists('test') then box.schema.user.create('test') @@ -16,14 +19,19 @@ end) c = box.space.counter if not c then c = box.schema.space.create('counter') - pr = c:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'STR'}}) + c:format({ + {name = 'primary', type = 'string'}, + {name = 'num', type = 'unsigned'}, + {name = 'text', type = 'string'} + }) + pr = c:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'string'}}) c:insert({'test', 1337, 'Some text.'}) end s = box.space.bench if not s then s = box.schema.space.create('bench') - p = s:create_index('primary', {type = 'hash', parts = {1, 'num'}}) + p = s:create_index('primary', {type = 'hash', parts = {1, 'unsigned'}}) end function clear() diff --git a/benchmark/read.js b/benchmark/read.js index d9d11ec..107d153 100755 --- a/benchmark/read.js +++ b/benchmark/read.js @@ -1,127 +1,176 @@ /* global Promise */ 'use strict'; -var exec = require('child_process').exec; var Benchmark = require('benchmark'); var suite = new Benchmark.Suite(); var Driver = require('../lib/connection.js'); +var noop = require('lodash/noop'); var promises; +var preparedSelectStmtId; -var conn = new Driver(process.argv[process.argv.length - 1], {lazyConnect: true}); +var connectionArg = process.argv[process.argv.length - 1] -conn.connect() - .then(function(){ - suite.add('select cb', {defer: true, fn: function(defer){ - function callback(){ - defer.resolve(); - } - conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], callback, console.error); - }}); - - suite.add('select promise', {defer: true, fn: function(defer){ - conn.select('counter', 0, 1, 0, 'eq', ['test']) - .then(function(){ defer.resolve();}); - }}); - - suite.add('paralell 500', {defer: true, fn: function(defer){ - try{ - promises = []; - for (let l=0;l<500;l++){ - promises.push(conn.select('counter', 0, 1, 0, 'eq', ['test'])); - } - var chain = Promise.all(promises); - chain.then(function(){ defer.resolve(); }) - .catch(function(e){ - console.error(e, e.stack); - defer.reject(e); - }); - } catch(e){ - defer.reject(e); - console.error(e, e.stack); - } - }}); - - suite.add('paralel by 10', {defer: true, fn: function(defer){ - var chain = Promise.resolve(); - try{ - for (var i=0;i<50;i++) - { - chain = chain.then(function(){ - promises = []; - for (var l=0;l<10;l++){ - promises.push( - conn.select('counter', 0, 1, 0, 'eq', ['test']) - ); - } - return Promise.all(promises); - }); - } - - chain.then(function(){ defer.resolve(); }) - .catch(function(e){ - console.error(e, e.stack); - }); - } catch(e){ - console.error(e, e.stack); - } - }}); - - suite.add('paralel by 50', {defer: true, fn: function(defer){ - var chain = Promise.resolve(); - try{ - for (var i=0;i<10;i++) - { - chain = chain.then(function(){ - promises = []; - for (var l=0;l<50;l++){ - promises.push( - conn.select('counter', 0, 1, 0, 'eq', ['test']) - ); - } - return Promise.all(promises); - }); - } - - chain.then(function(){ defer.resolve(); }) - .catch(function(e){ - console.error(e, e.stack); - }); - } catch(e){ - console.error(e, e.stack); +var conn = new Driver(connectionArg, { + lazyConnect: true, + tuplesToObjects: true +}); + +var connAutoPipelined = new Driver(connectionArg, { + lazyConnect: true, + autoPipeliningPeriod: 10 // just a 1 millisecond window! +}); + +Promise.all([ + conn.connect(), + connAutoPipelined.connect() +]) +// preload schema and create a prepared SQL statement +.then(function () { + return Promise.all([ + conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, noop), + connAutoPipelined.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, noop), + conn.prepare('SELECT * FROM "counter" WHERE "primary" = ? LIMIT 1 OFFSET 0') + .then(function (result) { + preparedSelectStmtId = result.id + }) + ]) +}) +.then(function(){ + // non-deferred benchmarks measures the real performance of code, e.g. not awaiting for the response to be received + suite.add('non-deferred select', {defer: false, fn: function(){ + conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, console.error); + }}); + + suite.add('non-deferred select, tuplesToObjects', {defer: false, fn: function(){ + conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, console.error, { + tuplesToObjects: true + }); + }}); + + // show the performance improvement when using an autopipelining + suite.add('non-deferred select + autopipelining window of 1ms', {defer: false, fn: function(){ + connAutoPipelined.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, console.error); + }}); + + suite.add('non-deferred sql select', {defer: false, fn: function(){ + conn.sql('SELECT * FROM "counter" WHERE "primary" = ? LIMIT 1 OFFSET 0', ['test']); + }}); + + suite.add('non-deferred sql prepared select', {defer: false, fn: function(){ + conn.sql(preparedSelectStmtId, ['test']); + }}); + + suite.add('select cb', {defer: true, fn: function(defer){ + function callback(){ + defer.resolve(); + } + conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], callback, console.error); + }}); + + suite.add('select promise', {defer: true, fn: function(defer){ + conn.select('counter', 0, 1, 0, 'eq', ['test']) + .then(function(){ defer.resolve();}); + }}); + + suite.add('paralell 500', {defer: true, fn: function(defer){ + try{ + promises = []; + for (let l=0;l<500;l++){ + promises.push(conn.select('counter', 0, 1, 0, 'eq', ['test'])); } - }}); + var chain = Promise.all(promises); + chain.then(function(){ defer.resolve(); }) + .catch(function(e){ + console.error(e, e.stack); + defer.reject(e); + }); + } catch(e){ + defer.reject(e); + console.error(e, e.stack); + } + }}); - suite.add('pipelined select by 10', {defer: true, fn: function(defer){ - var pipelinedConn = conn.pipeline() - - for (var i=0;i<10;i++) { - pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']); + suite.add('paralel by 10', {defer: true, fn: function(defer){ + var chain = Promise.resolve(); + try{ + for (var i=0;i<50;i++) + { + chain = chain.then(function(){ + promises = []; + for (var l=0;l<10;l++){ + promises.push( + conn.select('counter', 0, 1, 0, 'eq', ['test']) + ); + } + return Promise.all(promises); + }); } - pipelinedConn.exec() - .then(function(){ defer.resolve(); }) - .catch(function(e){ defer.reject(e); }); - }}); + chain.then(function(){ defer.resolve(); }) + .catch(function(e){ + console.error(e, e.stack); + }); + } catch(e){ + console.error(e, e.stack); + } + }}); - suite.add('pipelined select by 50', {defer: true, fn: function(defer){ - var pipelinedConn = conn.pipeline() - - for (var i=0;i<50;i++) { - pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']); + suite.add('paralel by 50', {defer: true, fn: function(defer){ + var chain = Promise.resolve(); + try{ + for (var i=0;i<10;i++) + { + chain = chain.then(function(){ + promises = []; + for (var l=0;l<50;l++){ + promises.push( + conn.select('counter', 0, 1, 0, 'eq', ['test']) + ); + } + return Promise.all(promises); + }); } - pipelinedConn.exec() - .then(function(){ defer.resolve(); }) - .catch(function(e){ defer.reject(e); }); - }}); - - suite - .on('cycle', function(event) { - console.log(String(event.target)); - }) - .on('complete', function() { - console.log('complete'); - process.exit(); - }) - .run({ 'async': true, 'queued': true }); - }); + chain.then(function(){ defer.resolve(); }) + .catch(function(e){ + console.error(e, e.stack); + }); + } catch(e){ + console.error(e, e.stack); + } + }}); + + suite.add('pipelined select by 10', {defer: true, fn: function(defer){ + var pipelinedConn = conn.pipeline() + + for (var i=0;i<10;i++) { + pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']); + } + + pipelinedConn.exec() + .then(function(){ defer.resolve(); }) + .catch(function(e){ defer.reject(e); }); + }}); + + suite.add('pipelined select by 50', {defer: true, fn: function(defer){ + var pipelinedConn = conn.pipeline() + + for (var i=0;i<50;i++) { + pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']); + } + + pipelinedConn.exec() + .then(function(){ defer.resolve(); }) + .catch(function(e){ defer.reject(e); }); + }}); + + suite + .on('cycle', function(event) { + console.log(String(event.target)); + }) + .on('complete', function() { + console.log('complete'); + process.exit(); + }) + .run({ 'async': true, 'queued': true }); +}); diff --git a/lib/autopipelining.js b/lib/autopipelining.js new file mode 100644 index 0000000..44476f9 --- /dev/null +++ b/lib/autopipelining.js @@ -0,0 +1,17 @@ +function AutoPipeline() {} + +AutoPipeline.prototype._processAutoPipeliningQueue = function (self) { + var concatenatedBuffer = Buffer.concat(self.autoPipelineQueue); + self.autoPipelineQueue = []; + self.autoPipeliningId = 0; + self.socket.write(concatenatedBuffer); +}; + +AutoPipeline.prototype._addToAutoPipeliningQueue = function (buffer) { + this.autoPipelineQueue.push(buffer); + if (!this.autoPipeliningId) { + this.autoPipeliningId = setTimeout(this._processAutoPipeliningQueue, this.options.autoPipeliningPeriod, this); + } +}; + +module.exports = AutoPipeline; \ No newline at end of file diff --git a/lib/buffer-processor.js b/lib/buffer-processor.js new file mode 100644 index 0000000..50847a1 --- /dev/null +++ b/lib/buffer-processor.js @@ -0,0 +1,90 @@ +var { + createBuffer, + TarantoolError, +} = require('./utils'); +var msgpackr = require('msgpackr'); +var packr = new msgpackr.Packr(); + +function _getScaleByNumber (num) { + if (num <= 255) { + return 8; + } else if (num <= 65535) { + return 16; + } else if (num <= 4294967295) { + return 32; + } else if (num <= Number.MAX_SAFE_INTEGER) { + return 64; + } else { + throw new TarantoolError('Exeeded max supported number for scale'); + } +}; + +function _getBufferLenghtByNumber (num) { + if (num <= 255) return 2; + if (num <= 65535) return 3; + if (num <= 4294967295) return 5; + if (num <= Number.MAX_SAFE_INTEGER) return 9; + + throw new TarantoolError('Exeeded max supported buffer length'); +}; + +function _getFbyteByNumber (num) { + if (num <= 255) return 0xcc; + if (num <= 65535) return 0xcd; + if (num <= 4294967295) return 0xce; + if (num <= Number.MAX_SAFE_INTEGER) return 0xcf; + + throw new TarantoolError('Exeeded max supported buffer length'); +}; + +function _getWriteMethodByNumber (num) { + if (num <= 255) return 'writeUInt8'; + if (num <= 65535) return 'writeUInt16BE'; + if (num <= 4294967295) return 'writeUInt32BE'; + if (num <= Number.MAX_SAFE_INTEGER) return 'writeBigUInt64BE'; + + throw new TarantoolError('Exeeded max supported buffer length'); +} + +// reuse buffers and pre-write the first byte +var buffer2bytes = createBuffer(2); +buffer2bytes[0] = 0xcc; +var buffer3bytes = createBuffer(3); +buffer3bytes[0] = 0xcd; +var buffer5bytes = createBuffer(5); +buffer5bytes[0] = 0xce; +var buffer9bytes = createBuffer(9); +buffer9bytes[0] = 0xcf; +function _encodeMsgpackNumber (num, scale = 0) { + if (scale === 0) scale = _getScaleByNumber(num); + + switch (scale) { + case 8: + buffer2bytes.writeUInt8(num, 1); + return buffer2bytes; + case 16: + buffer3bytes.writeUInt16BE(num, 1); + return buffer3bytes; + case 32: + buffer5bytes.writeUInt32BE(num, 1); + return buffer5bytes; + case 64: + buffer9bytes.writeBigUInt64BE(num, 1); + return buffer9bytes; + default: + throw new TarantoolError('Unsupported scale provided: ' + scale); + }; +}; + +var bufferCache = new Map(); + +module.exports.getCachedMsgpackBuffer = function (value) { + var search = bufferCache.get(value); + if (search) { + return search; + } else { + var encoded = packr.encode(value); + bufferCache.set(value, encoded); + return encoded; + }; +}; \ No newline at end of file diff --git a/lib/commands.js b/lib/commands.js index bd1b9fa..0b96e37 100755 --- a/lib/commands.js +++ b/lib/commands.js @@ -7,37 +7,89 @@ var { TarantoolError, bufferSubarrayPoly } = require('./utils'); -var { encode: msgpackEncode } = require('msgpack-lite'); +var { encode: msgpackEncode, Decoder: msgpackDecoder, decode: msgpackDecode } = require('msgpack-lite'); var { codec } = require('./msgpack-extensions'); +var { getCachedMsgpackBuffer } = require('./buffer-processor'); +var msgpackr = require('msgpackr'); +var packr = new msgpackr.Packr(); function Commands() {} -Commands.prototype.sendCommand = function () {}; +// Commands.prototype.sendCommand = function () {}; var maxSmi = 1<<30 Commands.prototype._getRequestId = function(){ - if (this._id > maxSmi) - this._id =0; - return this._id++; + var _id = this._id + if (_id[0] > maxSmi) + _id[0] = 1; + return _id[0]++; +}; + +function _getScaleByNumber (num) { + if (num <= 255) { + return 8; + } else if (num <= 65535) { + return 16; + } else if (num <= 4294967295) { + return 32; + } else if (num <= Number.MAX_SAFE_INTEGER) { + return 64; + } else { + throw new TarantoolError('Exeeded max supported number for scale'); + } +}; + +// reuse buffers and pre-write the first byte +var buffer2bytes = createBuffer(2); +buffer2bytes[0] = 0xcc; +var buffer3bytes = createBuffer(3); +buffer3bytes[0] = 0xcd; +var buffer5bytes = createBuffer(5); +buffer5bytes[0] = 0xce; +var buffer9bytes = createBuffer(9); +buffer9bytes[0] = 0xcf; +function _encodeMsgpackNumber (num, scale = 0) { + if (scale === 0) scale = _getScaleByNumber(num); + + switch (scale) { + case 8: + buffer2bytes.writeUInt8(num, 1); + return buffer2bytes; + case 16: + buffer3bytes.writeUInt16BE(num, 1); + return buffer3bytes; + case 32: + buffer5bytes.writeUInt32BE(num, 1); + return buffer5bytes; + case 64: + buffer9bytes.writeBigUInt64BE(num, 1); + return buffer9bytes; + default: + throw new TarantoolError('Unsupported scale provided: ' + scale); + } }; Commands.prototype._getSpaceId = function(name){ var _this = this; return this.select(tarantoolConstants.Space.space, tarantoolConstants.IndexSpace.name, 1, 0, - 'eq', [name]) + 'eq', [name], { + tuplesToObjects: false, + autoPipeline: false + }) .then(function(value){ if (value && value.length && value[0]) { var spaceId = value[0][0]; - _this.namespace[name] = { - id: spaceId, - name: name, - indexes: {} - }; - _this.namespace[spaceId] = { + var tupleKeys = value[0][6]; + tupleKeys.map(function (value, index) { + tupleKeys[index] = value.name + }) + + _this.namespace[name] = _this.namespace[spaceId] = { id: spaceId, name: name, - indexes: {} + indexes: {}, + tupleKeys }; return spaceId; } @@ -50,7 +102,10 @@ Commands.prototype._getSpaceId = function(name){ Commands.prototype._getIndexId = function(spaceId, indexName){ var _this = this; return this.select(tarantoolConstants.Space.index, tarantoolConstants.IndexSpace.indexName, 1, 0, - 'eq', [spaceId, indexName]) + 'eq', [spaceId, indexName], { + tuplesToObjects: false, + autoPipeline: false + }) .then(function(value) { if (value && value[0] && value[0].length>1) { var indexId = value[0][1]; @@ -65,67 +120,13 @@ Commands.prototype._getIndexId = function(spaceId, indexName){ throw new TarantoolError('Cannot read a space name indexes or index is not defined'); }); }; -Commands.prototype.select = function(spaceId, indexId, limit, offset, iterator, key, _isPipelined) { + +Commands.prototype.select = function select (spaceId, indexId, limit, offset, iterator, key, opts = {}) { var _this = this; - if (!(key instanceof Array)) - key = [key]; return new Promise(function(resolve, reject){ - if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) - spaceId = _this.namespace[spaceId].id; - if (typeof(indexId)=='string' && _this.namespace[spaceId] && _this.namespace[spaceId].indexes[indexId]) - indexId = _this.namespace[spaceId].indexes[indexId]; - if (typeof(spaceId)=='string' || typeof(indexId)=='string') - { - return _this._getMetadata(spaceId, indexId) - .then(function(info){ - return _this.select(info[0], info[1], limit, offset, iterator, key, _isPipelined); - }) - .then(resolve) - .catch(reject); - } - var reqId = _this._getRequestId(); - - if (iterator == 'all') - key = []; - var bufKey = msgpackEncode(key, {codec}); - var len = 31+bufKey.length; - var buffer = createBuffer(5+len); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqSelect; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x86; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); - buffer[16] = 0xcd; - buffer.writeUInt16BE(spaceId, 17); - buffer[19] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 20); - buffer[21] = tarantoolConstants.KeysCode.limit; - buffer[22] = 0xce; - buffer.writeUInt32BE(limit, 23); - buffer[27] = tarantoolConstants.KeysCode.offset; - buffer[28] = 0xce; - buffer.writeUInt32BE(offset, 29); - buffer[33] = tarantoolConstants.KeysCode.iterator; - buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 34); - buffer[35] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 36); - - _this.sendCommand([ - tarantoolConstants.RequestCode.rqSelect, - reqId, - {resolve: resolve, reject: reject} - ], - buffer, - _isPipelined === true - ); + _this.selectCb(spaceId, indexId, limit, offset, iterator, key, resolve, reject, opts); }); -}; +} Commands.prototype._getMetadata = function(spaceName, indexName){ var _this = this; @@ -173,20 +174,121 @@ Commands.prototype.ping = function(){ buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); - _this.sendCommand([ + _this.sendCommand( tarantoolConstants.RequestCode.rqPing, - reqId, - {resolve: resolve, reject: reject} - ], buffer); + reqId, + buffer, + [resolve, reject] + ); }); }; -Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator, key, success, error){ +Commands.prototype.begin = function(transactionTimeout, isolationLevel, opts = {}) { + var _this = this; + var _arguments = arguments; + return new Promise(function (resolve, reject) { + if (!_this.streamId) { + reject( + new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') + ); + } + var reqId = _this._getRequestId(); + + var headersMap = new Map(); + headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqBegin) + headersMap.set(tarantoolConstants.KeysCode.sync, reqId) + headersMap.set(tarantoolConstants.KeysCode.iproto_stream_id, _this.streamId) + var headersBuffer = packr.encode(headersMap) + + var bodyMap = new Map(); + if (transactionTimeout) { + bodyMap.set(tarantoolConstants.KeysCode.iproto_timeout, transactionTimeout) + } + if (isolationLevel) { + bodyMap.set(tarantoolConstants.KeysCode.iproto_txn_isolation, isolationLevel) + } + var bodyBuffer = packr.encode(bodyMap) + + var dataLengthBuffer = _encodeMsgpackNumber(headersBuffer.length + bodyBuffer.length); + var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer, bodyBuffer]) + + _this.sendCommand( + tarantoolConstants.RequestCode.rqBegin, + reqId, + concatenatedBuffers, + [resolve, reject], + _arguments, + opts._pipelined === true + ); + }); +}; + +Commands.prototype.commit = function() { + var _this = this; + var _arguments = arguments; + return new Promise(function (resolve, reject) { + if (!_this.streamId) { + reject( + new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') + ); + } + var reqId = _this._getRequestId(); + + var headersMap = new Map(); + headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqCommit) + headersMap.set(tarantoolConstants.KeysCode.sync, reqId) + headersMap.set(tarantoolConstants.KeysCode.iproto_stream_id, _this.streamId) + var headersBuffer = packr.encode(headersMap) + + var dataLengthBuffer = _encodeMsgpackNumber(headersBuffer.length); + var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer]) + + _this.sendCommand( + tarantoolConstants.RequestCode.rqCommit, + reqId, + concatenatedBuffers, + [resolve, reject], + _arguments + ); + }); +}; + +Commands.prototype.rollback = function() { + var _this = this; + var _arguments = arguments; + return new Promise(function (resolve, reject) { + if (!_this.streamId) { + reject( + new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') + ); + } + var reqId = _this._getRequestId(); + + var headersMap = new Map(); + headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqRollback) + headersMap.set(tarantoolConstants.KeysCode.sync, reqId) + headersMap.set(tarantoolConstants.KeysCode.iproto_stream_id, _this.streamId) + var headersBuffer = packr.encode(headersMap) + + var dataLengthBuffer = _encodeMsgpackNumber(headersBuffer.length); + var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer]) + + _this.sendCommand( + tarantoolConstants.RequestCode.rqRollback, + reqId, + concatenatedBuffers, + [resolve, reject], + _arguments + ); + }); +}; + +Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator, key, success, error, opts = {}){ if (!(key instanceof Array)) key = [key]; var _this = this; - + if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) spaceId = _this.namespace[spaceId].id; if (typeof(indexId)=='string' && _this.namespace[spaceId] && _this.namespace[spaceId].indexes[indexId]) @@ -195,7 +297,7 @@ Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator { return _this._getMetadata(spaceId, indexId) .then(function(info){ - return _this.selectCb(info[0], info[1], limit, offset, iterator, key, success, error); + return _this.selectCb(info[0], info[1], limit, offset, iterator, key, success, error, opts); }) .catch(error); } @@ -203,44 +305,52 @@ Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator var reqId = this._getRequestId(); if (iterator == 'all') key = []; - var bufKey = msgpackEncode(key, {codec}); - var len = 31+bufKey.length; - var buffer = createBuffer(5+len); + var bufKey = msgpackr.pack(key); + + var bufferLength = 42+bufKey.length; + var buffer = createBuffer(bufferLength); buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; + buffer.writeUInt32BE(37+bufKey.length, 1); + buffer[5] = 0x83; buffer[6] = tarantoolConstants.KeysCode.code; buffer[7] = tarantoolConstants.RequestCode.rqSelect; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x86; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); - buffer[16] = 0xcd; - buffer.writeUInt16BE(spaceId, 17); - buffer[19] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 20); - buffer[21] = tarantoolConstants.KeysCode.limit; - buffer[22] = 0xce; - buffer.writeUInt32BE(limit, 23); - buffer[27] = tarantoolConstants.KeysCode.offset; + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x86; + buffer[21] = tarantoolConstants.KeysCode.space_id; + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = tarantoolConstants.KeysCode.limit; buffer[28] = 0xce; - buffer.writeUInt32BE(offset, 29); - buffer[33] = tarantoolConstants.KeysCode.iterator; - buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 34); - buffer[35] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 36); - - this.sendCommand([ + buffer.writeUInt32BE(limit, 29); + buffer[33] = tarantoolConstants.KeysCode.offset; + buffer[34] = 0xce; + buffer.writeUInt32BE(offset || 0, 35); + buffer[39] = tarantoolConstants.KeysCode.iterator; + buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 40); + buffer[41] = tarantoolConstants.KeysCode.key; + bufKey.copy(buffer, 42); + + this.sendCommand( tarantoolConstants.RequestCode.rqSelect, - reqId, - {resolve: success, reject: error} - ], buffer); + reqId, + buffer, + [success, error], + arguments, + opts + ); }; Commands.prototype.delete = function(spaceId, indexId, key){ var _this = this; + var _arguments = arguments; if (Number.isInteger(key)) key = [key]; return new Promise(function (resolve, reject) { @@ -258,31 +368,37 @@ Commands.prototype.delete = function(spaceId, indexId, key){ var reqId = _this._getRequestId(); var bufKey = msgpackEncode(key, {codec}); - var len = 17+bufKey.length; + var len = 23+bufKey.length; var buffer = createBuffer(5+len); buffer[0] = 0xce; buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; + buffer[5] = 0x83; buffer[6] = tarantoolConstants.KeysCode.code; buffer[7] = tarantoolConstants.RequestCode.rqDelete; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x83; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); - buffer[16] = 0xcd; - buffer.writeUInt16BE(spaceId, 17); - buffer[19] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 20); - buffer[21] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 22); - - _this.sendCommand([ + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(_this.streamId || 0, 16); + buffer[20] = 0x83; + buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = tarantoolConstants.KeysCode.key; + bufKey.copy(buffer, 28); + + _this.sendCommand( tarantoolConstants.RequestCode.rqDelete, - reqId, - {resolve: resolve, reject: reject} - ], buffer); + reqId, + buffer, + [resolve, reject], + _arguments, + _this._pipelined === true + ); } else reject(new TarantoolError('need array')); @@ -291,12 +407,12 @@ Commands.prototype.delete = function(spaceId, indexId, key){ Commands.prototype.update = function(spaceId, indexId, key, ops){ var _this = this; + var _arguments = arguments; if (Number.isInteger(key)) key = [key]; return new Promise(function (resolve, reject) { if (Array.isArray(ops) && Array.isArray(key)){ - if (typeof(spaceId)=='string' || typeof(indexId)=='string') - { + if (typeof(spaceId)=='string' || typeof(indexId)=='string') { return _this._getMetadata(spaceId, indexId) .then(function(info){ return _this.update(info[0], info[1], key, ops); @@ -308,33 +424,39 @@ Commands.prototype.update = function(spaceId, indexId, key, ops){ var bufKey = msgpackEncode(key, {codec}); var bufOps = msgpackEncode(ops, {codec}); - var len = 18+bufKey.length+bufOps.length; + var len = 24+bufKey.length+bufOps.length; var buffer = createBuffer(len+5); buffer[0] = 0xce; buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; + buffer[5] = 0x83; buffer[6] = tarantoolConstants.KeysCode.code; buffer[7] = tarantoolConstants.RequestCode.rqUpdate; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x84; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); - buffer[16] = 0xcd; - buffer.writeUInt16BE(spaceId, 17); - buffer[19] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 20); - buffer[21] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 22); - buffer[22+bufKey.length] = tarantoolConstants.KeysCode.tuple; - bufOps.copy(buffer, 23+bufKey.length); - - _this.sendCommand([ - tarantoolConstants.RequestCode.rqUpdate, - reqId, - {resolve: resolve, reject: reject} - ], buffer); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(_this.streamId || 0, 16); + buffer[20] = 0x84; + buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = tarantoolConstants.KeysCode.key; + bufKey.copy(buffer, 28); + buffer[28+bufKey.length] = tarantoolConstants.KeysCode.tuple; + bufOps.copy(buffer, 29+bufKey.length); + + _this.sendCommand( + tarantoolConstants.RequestCode.rqUpdate, + reqId, + buffer, + [resolve, reject], + _arguments, + _this._pipelined === true + ); } else reject(new TarantoolError('need array')); @@ -343,6 +465,7 @@ Commands.prototype.update = function(spaceId, indexId, key, ops){ Commands.prototype.upsert = function(spaceId, ops, tuple){ var _this = this; + var _arguments = arguments; return new Promise(function (resolve, reject) { if (Array.isArray(ops)){ if (typeof(spaceId)=='string') @@ -358,131 +481,222 @@ Commands.prototype.upsert = function(spaceId, ops, tuple){ var bufTuple = msgpackEncode(tuple, {codec}); var bufOps = msgpackEncode(ops, {codec}); - var len = 16+bufTuple.length+bufOps.length; + var len = 22+bufTuple.length+bufOps.length; var buffer = createBuffer(len+5); buffer[0] = 0xce; buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; + buffer[5] = 0x83; buffer[6] = tarantoolConstants.KeysCode.code; buffer[7] = tarantoolConstants.RequestCode.rqUpsert; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x83; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); - buffer[16] = 0xcd; - buffer.writeUInt16BE(spaceId, 17); - buffer[19] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 20); - buffer[20+bufTuple.length] = tarantoolConstants.KeysCode.def_tuple; - bufOps.copy(buffer, 21+bufTuple.length); - - _this.sendCommand([ + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(_this.streamId || 0, 16); + buffer[20] = 0x83; + buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.tuple; + bufTuple.copy(buffer, 26); + buffer[26+bufTuple.length] = tarantoolConstants.KeysCode.def_tuple; + bufOps.copy(buffer, 27+bufTuple.length); + + _this.sendCommand( tarantoolConstants.RequestCode.rqUpsert, - reqId, - {resolve: resolve, reject: reject} - ], buffer); + reqId, + buffer, + [resolve, reject], + _arguments, + _this._pipelined === true + ); } else reject(new TarantoolError('need ops array')); }); }; - Commands.prototype.eval = function(expression){ var _this = this; + var _arguments = arguments; var tuple = Array.prototype.slice.call(arguments, 1); return new Promise(function (resolve, reject) { var reqId = _this._getRequestId(); var bufExp = msgpackEncode(expression); var bufTuple = msgpackEncode(tuple ? tuple : [], {codec}); - var len = 12+bufExp.length + bufTuple.length; + var len = 18+bufExp.length + bufTuple.length; var buffer = createBuffer(len+5); buffer[0] = 0xce; buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; + buffer[5] = 0x83; buffer[6] = tarantoolConstants.KeysCode.code; buffer[7] = tarantoolConstants.RequestCode.rqEval; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.expression, 15); - bufExp.copy(buffer, 16); - buffer[16+bufExp.length] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 17+bufExp.length); - - _this.sendCommand([ + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(_this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(tarantoolConstants.KeysCode.expression, 21); + bufExp.copy(buffer, 22); + buffer[22+bufExp.length] = tarantoolConstants.KeysCode.tuple; + bufTuple.copy(buffer, 23+bufExp.length); + + _this.sendCommand( tarantoolConstants.RequestCode.rqEval, - reqId, - {resolve: resolve, reject: reject} - ], buffer); + reqId, + buffer, + [resolve, reject], + _arguments, + _this._pipelined === true + ); }); }; Commands.prototype.call = function(functionName){ var _this = this; + var _arguments = arguments; var tuple = arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : []; return new Promise(function (resolve, reject) { var reqId = _this._getRequestId(); var bufName = msgpackEncode(functionName); var bufTuple = msgpackEncode(tuple ? tuple : [], {codec}); - var len = 12+bufName.length + bufTuple.length; + var len = 18+bufName.length + bufTuple.length; var buffer = createBuffer(len+5); buffer[0] = 0xce; buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; + buffer[5] = 0x83; buffer[6] = tarantoolConstants.KeysCode.code; buffer[7] = tarantoolConstants.RequestCode.rqCall; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.function_name, 15); - bufName.copy(buffer, 16); - buffer[16+bufName.length] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 17+bufName.length); - - _this.sendCommand([ + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(_this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(tarantoolConstants.KeysCode.function_name, 21); + bufName.copy(buffer, 22); + buffer[22+bufName.length] = tarantoolConstants.KeysCode.tuple; + bufTuple.copy(buffer, 23+bufName.length); + + _this.sendCommand( tarantoolConstants.RequestCode.rqCall, - reqId, - {resolve: resolve, reject: reject} - ], buffer); + reqId, + buffer, + [resolve, reject], + _arguments, + _this._pipelined === true + ); }); }; Commands.prototype.sql = function(query, bindParams = []){ var _this = this; + var _arguments = arguments; + return new Promise(function (resolve, reject) { + var reqId = _this._getRequestId(); + // var bufParams = msgpackEncode(bindParams, {codec}); + var bufParams = msgpackr.pack(bindParams); + var isPreparedStatement = (typeof query === 'number') // in case of the statement ID being passed to 'query' param + var bufQuery = isPreparedStatement ? getCachedMsgpackBuffer(query) : msgpackr.pack(query); // cache only prepared queries, considering them frequently used + + var len = 18+bufQuery.length + bufParams.length; + var buffer = createBuffer(len+5); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqExecute; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(_this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(isPreparedStatement ? tarantoolConstants.KeysCode.stmt_id : tarantoolConstants.KeysCode.sql_text, 21); + bufQuery.copy(buffer, 22); + buffer[22+bufQuery.length] = tarantoolConstants.KeysCode.sql_bind; + bufParams.copy(buffer, 23+bufQuery.length); + + _this.sendCommand( + tarantoolConstants.RequestCode.rqExecute, + reqId, + buffer, + [resolve, reject], + _arguments, + _this._pipelined === true + ); + }); +}; + +Commands.prototype.prepare = function(query, opts = {}){ + var _this = this; + var _arguments = arguments; return new Promise(function (resolve, reject) { var reqId = _this._getRequestId(); var bufQuery = msgpackEncode(query); - var bufParams = msgpackEncode(bindParams, {codec}); - var len = 12+bufQuery.length + bufParams.length; + var len = 13+bufQuery.length; var buffer = createBuffer(len+5); buffer[0] = 0xce; buffer.writeUInt32BE(len, 1); buffer[5] = 0x82; buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqExecute; + buffer[7] = tarantoolConstants.RequestCode.rqPrepare; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); buffer[14] = 0x82; buffer.writeUInt8(tarantoolConstants.KeysCode.sql_text, 15); bufQuery.copy(buffer, 16); - buffer[16+bufQuery.length] = tarantoolConstants.KeysCode.sql_bind; - bufParams.copy(buffer, 17+bufQuery.length); - _this.sendCommand([ - tarantoolConstants.RequestCode.rqExecute, - reqId, - {resolve: resolve, reject: reject} - ], buffer); + _this.sendCommand( + tarantoolConstants.RequestCode.rqPrepare, + reqId, + buffer, + [resolve, reject], + _arguments, + opts + ); + }); +}; + +Commands.prototype.id = function(version = 3, features = [1], auth_type = 'chap-sha1'){ + var _this = this; + var _arguments = arguments; + return new Promise(function (resolve, reject) { + var reqId = _this._getRequestId(); + + var headersMap = new Map(); + headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqId) + headersMap.set(tarantoolConstants.KeysCode.sync, reqId) + var headersBuffer = packr.encode(headersMap) + + var bodyMap = new Map(); + bodyMap.set(tarantoolConstants.KeysCode.iproto_version, version) + bodyMap.set(tarantoolConstants.KeysCode.iproto_features, features) + bodyMap.set(tarantoolConstants.KeysCode.iproto_auth_type, auth_type) + var bodyBuffer = packr.encode(bodyMap) + + var dataLengthBuffer = _encodeMsgpackNumber(headersBuffer.length + bodyBuffer.length); + var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer, bodyBuffer]) + + _this.sendCommand( + tarantoolConstants.RequestCode.rqId, + reqId, + concatenatedBuffers, + [resolve, reject], + _arguments + ); }); }; @@ -496,8 +710,9 @@ Commands.prototype.replace = function(spaceId, tuple){ return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, reqId, spaceId, tuple); }; -Commands.prototype._replaceInsert = function(cmd, reqId, spaceId, tuple){ +Commands.prototype._replaceInsert = function(cmd, reqId, spaceId, tuple, opts = {}){ var _this = this; + var _arguments = arguments; return new Promise(function (resolve, reject) { if (Array.isArray(tuple)){ if (typeof(spaceId)=='string') @@ -510,29 +725,35 @@ Commands.prototype._replaceInsert = function(cmd, reqId, spaceId, tuple){ .catch(reject); } var bufTuple = msgpackEncode(tuple); - var len = 15+bufTuple.length; + var len = 21+bufTuple.length; var buffer = createBuffer(len+5); buffer[0] = 0xce; buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; + buffer[5] = 0x83; buffer[6] = tarantoolConstants.KeysCode.code; buffer[7] = cmd; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); - buffer[16] = 0xcd; - buffer.writeUInt16BE(spaceId, 17); - buffer[19] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 20); - - _this.sendCommand([ + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(_this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.tuple; + bufTuple.copy(buffer, 26); + + _this.sendCommand( cmd, - reqId, - {resolve: resolve, reject: reject} - ], buffer); + reqId, + buffer, + [resolve, reject], + _arguments, + _this._pipelined === true + ); } else reject(new TarantoolError('need array')); @@ -566,11 +787,10 @@ Commands.prototype._auth = function(username, password){ buffer[28+user.length] = 0xb4; scrambled.copy(buffer, 29+user.length); - _this.commandsQueue.push([ - tarantoolConstants.RequestCode.rqAuth, - reqId, - {resolve: resolve, reject: reject}, - ]); + _this.sentCommands.set(reqId, [ + tarantoolConstants.RequestCode.rqAuth, + [resolve, reject] + ]) _this.socket.write(buffer); }); }; diff --git a/lib/connection.js b/lib/connection.js index dd3b02b..68aee47 100755 --- a/lib/connection.js +++ b/lib/connection.js @@ -10,11 +10,13 @@ var { findPipelineErrors } = require('./utils'); var { packAs } = require('./msgpack-extensions'); -var Denque = require('denque'); var tarantoolConstants = require('./const'); var Commands = require('./commands'); var Pipeline = require('./pipeline'); +var Transaction = require('./transaction'); +var Parser = require('./parser'); var Connector = require('./connector'); +var AutoPipeline = require('./autopipelining'); var eventHandler = require('./event-handler'); var SliderBuffer = require('./sliderBuffer') @@ -31,6 +33,7 @@ var revertStates = { 256: 'connect', 512: 'changing_host' }; + TarantoolConnection.defaultOptions = { host: 'localhost', port: 3301, @@ -41,7 +44,10 @@ TarantoolConnection.defaultOptions = { beforeReserve: 2, timeout: 0, noDelay: true, + requestTimeout: 0, keepAlive: true, + autoPipeliningPeriod: 0, // disable auto pipelining + tuplesToObjects: false, nonWritableHostPolicy: null, /* What to do when Tarantool server rejects write operation, e.g. because of box.cfg.read_only set to 'true' or during fetching snapshot. Possible values are: @@ -63,6 +69,7 @@ function TarantoolConnection (){ } EventEmitter.call(this); this.reserve = []; + this.parseOptions = parseOptions; this.parseOptions(arguments[0], arguments[1], arguments[2]); this.connector = new Connector(this.options); this.schemaId = null; @@ -79,16 +86,35 @@ function TarantoolConnection (){ CONNECT: 256, CHANGING_HOST: 512 }; + this.schemas = {}; this.dataState = this.states.PREHELLO; - this.commandsQueue = new Denque(); - this.offlineQueue = new Denque(); + this.commandsQueue = new Array(); + this.offlineQueue = new Array(); this.namespace = {}; - this.bufferSlide = new SliderBuffer() + this.bufferSlide = new SliderBuffer(); this.awaitingResponseLength = -1; this.retryAttempts = 0; - this._id = 0; - this.findPipelineError = findPipelineError - this.findPipelineErrors = findPipelineErrors + this._id = [ 1 ]; + this.sentCommands = new Map(); + this.autoPipelineQueue = new Array(); + this.autoPipeliningId = 0; + this.findPipelineError = findPipelineError; + this.findPipelineErrors = findPipelineErrors; + this.resetOfflineQueue = resetOfflineQueue; + this.IteratorsType = tarantoolConstants.IteratorsType; + this.useNextReserve = useNextReserve; + this.sendCommand = sendCommand; + this.setState = setState; + this.connect = connect; + this.flushQueue = flushQueue; + this.silentEmit = silentEmit; + this.destroy = destroy; + this.disconnect = disconnect; + Object.assign(this, Commands.prototype); + Object.assign(this, AutoPipeline.prototype); + Object.assign(this, Parser); + Object.assign(this, Transaction.prototype); + Object.assign(this, Pipeline.prototype); if (this.options.lazyConnect) { this.setState(this.states.INITED); } else { @@ -97,19 +123,16 @@ function TarantoolConnection (){ } util.inherits(TarantoolConnection, EventEmitter); -_.assign(TarantoolConnection.prototype, Commands.prototype); -_.assign(TarantoolConnection.prototype, Pipeline.prototype); -_.assign(TarantoolConnection.prototype, require('./parser')); for (var packerName of Object.keys(packAs)) { TarantoolConnection.prototype['pack' + packerName] = packAs[packerName] } -TarantoolConnection.prototype.resetOfflineQueue = function () { - this.offlineQueue = new Denque(); +function resetOfflineQueue () { + this.offlineQueue = new Array(); }; -TarantoolConnection.prototype.parseOptions = function(){ +function parseOptions (){ this.options = {}; var i; for (i = 0; i < arguments.length; ++i) { @@ -153,7 +176,7 @@ TarantoolConnection.prototype.parseOptions = function(){ this.options.beforeReserve = this.options.beforeReserve < 0 ? 0 : this.options.beforeReserve; }; -TarantoolConnection.prototype.useNextReserve = function(){ +function useNextReserve (){ this.retryAttempts = 0; if(this.reserveIterator == this.reserve.length) this.reserveIterator = 0; delete this.options.port @@ -165,40 +188,84 @@ TarantoolConnection.prototype.useNextReserve = function(){ var reserveOptions = this.reserve[this.reserveIterator++] _.assign(this.options, reserveOptions); - if (!reserveOptions) throw new TarantoolError('Attempted to use next reserve host, but iseems to be that there are none of them. Specify it via connection configuration.') + if (!reserveOptions) throw new TarantoolError('Attempted to use next reserve host, but iseems to be that there are none of them. Specify via the connection configuration.'); - return reserveOptions + return reserveOptions; }; -TarantoolConnection.prototype.sendCommand = function(command, buffer, isPipelined){ +function compareBooleans (a, b) { + switch (a) { + case true: + case false: + return a; + default: + return b + } +} + +function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, opts = {}){ switch (this.state){ case this.states.INITED: this.connect().catch(_.noop); case this.states.CONNECT: if(!this.socket || !this.socket.writable){ - debug('queue -> %s(%s)', command[0], command[1]); - this.offlineQueue.push([command, buffer]); + debug('queue -> %s(%s)', requestCode, reqId); + this.offlineQueue.push([ + requestCode, + reqId, + buffer, + callbacks, + commandArguments, + opts + ]); } else { - if (this.options.nonWritableHostPolicy == 'changeAndRetry') { - command.push(buffer) - } - this.commandsQueue.push(command); - if (!isPipelined) this.socket.write(buffer); // in pipelined mode data is written via its own function + var tuplesToObjects = compareBooleans(opts.tuplesToObjects, this.options.tuplesToObjects); + var setValue = [ + requestCode, + callbacks, + [], + [], + tuplesToObjects + ]; + + if ((this.options.nonWritableHostPolicy === 'changeAndRetry') || tuplesToObjects) setValue[2] = Object.values(commandArguments); + + var requestTimeout = this.options.requestTimeout || opts.requestTimeout; + if (requestTimeout) setValue[3] = setTimeout(function () { + callbacks[1]( + new TarantoolError('Request timed out (' + requestTimeout + ' ms)') + ) + }, requestTimeout); + + this.sentCommands.set(reqId, setValue); + // in pipelined mode data is written via its own function + if ((this.options.autoPipeliningPeriod > 0) && (!opts._pipelined) && (opts.autoPipeline != false)) { + this._addToAutoPipeliningQueue(buffer); + } else if (!opts._pipelined) { + this.socket.write(buffer); + }; } break; case this.states.END: - command[2].reject(new TarantoolError('Connection is closed.')); + callbacks[1](new TarantoolError('Connection is closed.')); break; default: - debug('queue -> %s(%s)', command[0], command[1]); + debug('queue -> %s(%s)', requestCode, reqId); if (!this.options.enableOfflineQueue) { - return command[2].reject(new TarantoolError('Connection not established yet!')); - } - this.offlineQueue.push([command, buffer]); + return callbacks[1](new TarantoolError('Connection not established yet!')); + }; + this.offlineQueue.push([ + requestCode, + reqId, + buffer, + callbacks, + commandArguments, + opts + ]); } }; -TarantoolConnection.prototype.setState = function (state, arg) { +function setState (state, arg) { var address; if (this.socket && this.socket.remoteAddress && this.socket.remotePort) { address = this.socket.remoteAddress + ':' + this.socket.remotePort; @@ -214,7 +281,7 @@ TarantoolConnection.prototype.setState = function (state, arg) { process.nextTick(this.emit.bind(this, revertStates[state], arg)); }; -TarantoolConnection.prototype.connect = function(){ +function connect (){ return new Promise(function (resolve, reject) { if (this.state === this.states.CONNECTING || this.state === this.states.CONNECT || this.state === this.states.CONNECTED || this.state === this.states.AUTH) { reject(new TarantoolError('Tarantool is already connecting/connected')); @@ -265,16 +332,16 @@ TarantoolConnection.prototype.connect = function(){ }.bind(this)); }; -TarantoolConnection.prototype.flushQueue = function (error) { +function flushQueue (error) { while (this.offlineQueue.length > 0) { - this.offlineQueue.shift()[0][2].reject(error); + this.offlineQueue.shift()[3][1](error); } while (this.commandsQueue.length > 0) { - this.commandsQueue.shift()[2].reject(error); + this.commandsQueue.shift()[3][1](error); } }; -TarantoolConnection.prototype.silentEmit = function (eventName) { +function silentEmit (eventName) { var error; if (eventName === 'error') { error = arguments[1]; @@ -304,10 +371,12 @@ TarantoolConnection.prototype.silentEmit = function (eventName) { } return false; }; -TarantoolConnection.prototype.destroy = function () { + +function destroy () { this.disconnect(); }; -TarantoolConnection.prototype.disconnect = function(reconnect){ + +function disconnect (reconnect){ if (!reconnect) { this.manuallyClosing = true; } @@ -322,6 +391,6 @@ TarantoolConnection.prototype.disconnect = function(reconnect){ } }; -TarantoolConnection.prototype.IteratorsType = tarantoolConstants.IteratorsType; +// TarantoolConnection.prototype.IteratorsType = tarantoolConstants.IteratorsType; module.exports = TarantoolConnection; diff --git a/lib/const.js b/lib/const.js index d7754d0..3100261 100755 --- a/lib/const.js +++ b/lib/const.js @@ -11,10 +11,15 @@ var RequestCode = { rqAuth: 0x07, rqEval: 0x08, rqUpsert: 0x09, - rqCallNew: 0x0a, + rqRollback: 0x10, + rqCallNew: 0x0a, // ? rqExecute: 0x0b, + rqPrepare: 0x0d, + rqBegin: 0x0e, + rqCommit: 0x0f, rqDestroy: 0x100, //fake for destroy socket cmd - rqPing: 0x40 + rqPing: 0x40, + rqId: 0x49 }; var KeysCode = { @@ -34,11 +39,36 @@ var KeysCode = { def_tuple: 0x28, data: 0x30, iproto_error_24: 0x31, - meta: 0x32, + metadata: 0x32, + bind_metadata: 0x33, sql_text: 0x40, sql_bind: 0x41, sql_info: 0x42, - iproto_error: 0x52 + stmt_id: 0x43, + iproto_error: 0x52, + iproto_version: 0x54, + iproto_features: 0x55, + iproto_timeout: 0x56, + iproto_txn_isolation: 0x59, + iproto_stream_id: 0x0a, + iproto_auth_type: 0x5b +}; + +var KeysCodeBuffer = {}; +for (var key of Object.keys(KeysCode)) { + KeysCodeBuffer[key] = Buffer.from([KeysCode[key]]) +} + +var PredefinedBuffers = { + selectHeaders: Buffer.from([ + KeysCode.code, + RequestCode.rqSelect, + KeysCode.sync + ]), + selectBody: Buffer.from([ + 0x86, + KeysCode.space_id, + ]) }; // https://github.com/fl00r/go-tarantool-1.6/issues/2 @@ -55,6 +85,11 @@ var IteratorsType = { bitsAllNotSet: 9 }; +var IteratorsTypeBuffer = {}; +for (var key of Object.keys(IteratorsType)) { + IteratorsTypeBuffer[key] = Buffer.from([IteratorsType[key]]) +} + var OkCode = 0; var NetErrCode = 0xfffffff1; // fake code to wrap network problems into response var TimeoutErrCode = 0xfffffff2; // fake code to wrap timeout error into repsonse @@ -99,7 +134,10 @@ var ExportPackage = { Space: Space, IndexSpace: IndexSpace, BufferedIterators: BufferedIterators, - BufferedKeys: BufferedKeys + BufferedKeys: BufferedKeys, + KeysCodeBuffer: KeysCodeBuffer, + PredefinedBuffers: PredefinedBuffers, + IteratorsTypeBuffer: IteratorsTypeBuffer }; module.exports = ExportPackage; \ No newline at end of file diff --git a/lib/event-handler.js b/lib/event-handler.js index 3c99ba1..a124e40 100755 --- a/lib/event-handler.js +++ b/lib/event-handler.js @@ -1,9 +1,10 @@ +// var msgpack = require('msgpack-lite'); var debug = require('debug')('tarantool-driver:handler'); -var { - TarantoolError, - bufferSubarrayPoly -} = require('./utils'); -var _ = require('lodash'); +var utils = require('./utils'); +var tarantoolConstants = require('./const'); + +// var Decoder = msgpack.Decoder; +// var decoder = new Decoder(); exports.connectHandler = function (self) { return function () { @@ -13,48 +14,46 @@ exports.connectHandler = function (self) { self.dataState = self.states.PREHELLO; break; case self.states.CONNECTED: - var connectionOptions = _.pick(self.options, ['port', 'host', 'path']) if(self.options.password){ self.setState(self.states.AUTH); self._auth(self.options.username, self.options.password) .then(function(){ - self.setState(self.states.CONNECT, connectionOptions); + self.setState(self.states.CONNECT, {host: self.options.host, port: self.options.port}); debug('authenticated [%s]', self.options.username); - sendOfflineQueue(self); + sendOfflineQueue.call(self); }, function(err){ self.flushQueue(err); self.silentEmit('error', err); self.disconnect(true); }); } else { - self.setState(self.states.CONNECT, connectionOptions); - sendOfflineQueue(self); + self.setState(self.states.CONNECT, {host: self.options.host, port: self.options.port}); + sendOfflineQueue.call(self); } break; } }; }; -function sendOfflineQueue(self){ - if (self.offlineQueue.length) { - debug('send %d commands in offline queue', self.offlineQueue.length); - var offlineQueue = self.offlineQueue; - self.resetOfflineQueue(); +function sendOfflineQueue(){ + if (this.offlineQueue.length) { + debug('send %d commands in offline queue', this.offlineQueue.length); + var offlineQueue = this.offlineQueue; + this.resetOfflineQueue(); while (offlineQueue.length > 0) { var command = offlineQueue.shift(); - self.sendCommand( - command[0], - command[1] - ); + this.sendCommand.apply(this, command); } } } exports.dataHandler = function(self){ + var buffer = null; + return function(data){ switch(self.dataState){ case self.states.PREHELLO: - self.salt = data[bufferSubarrayPoly](64, 108).toString('utf8'); + self.salt = data.slice(64, 108).toString('utf8'); self.dataState = self.states.CONNECTED; self.setState(self.states.CONNECTED); exports.connectHandler(self)(); @@ -188,7 +187,7 @@ exports.errorHandler = function(self){ exports.closeHandler = function (self) { function close () { self.setState(self.states.END); - self.flushQueue(new TarantoolError('Connection is closed.')); + self.flushQueue(new utils.TarantoolError('Connection is closed.')); } return function(){ @@ -209,7 +208,7 @@ exports.closeHandler = function (self) { return close(); } self.setState(self.states.RECONNECTING, retryDelay); - if ((self.options.reserveHosts && self.options.reserveHosts.length || 0) > 0) { + if (self.options.reserveHosts) { if (self.retryAttempts-1 == self.options.beforeReserve){ self.useNextReserve(); self.connect().catch(function(){}); diff --git a/lib/msgpack-extensions.js b/lib/msgpack-extensions.js index 83cb63e..5db793e 100644 --- a/lib/msgpack-extensions.js +++ b/lib/msgpack-extensions.js @@ -1,4 +1,4 @@ -var { createCodec: msgpackCreateCodec} = require('msgpack-lite'); +var { createCodec: msgpackCreateCodec } = require('msgpack-lite'); var { TarantoolError, createBuffer, @@ -13,7 +13,7 @@ var { Int64BE } = require("int64-buffer"); -var packAs = {} +var packAs = {}; var codec = msgpackCreateCodec(); // Pack big integers correctly (fix for https://github.com/tarantool/node-tarantool-driver/issues/48) @@ -62,15 +62,15 @@ packAs.Decimal = function TarantoolDecimalExt (value) { function isOdd (number) { return number % 2 !== 0; -} +}; +var decimalBuffer = createBuffer(1); // reuse buffer codec.addExtPacker(0x01, packAs.Decimal, (data) => { var strNum = data.value.toString() var rawNum = strNum.replace('-', '') - var scaleBuffer = createBuffer(1) var rawNumSplitted1 = rawNum.split('.')[1] - scaleBuffer.writeInt8(rawNumSplitted1 && rawNum.split('.')[1].length || 0) - var bufHexed = scaleBuffer.toString('hex') + decimalBuffer.writeInt8(rawNumSplitted1 && rawNum.split('.')[1].length || 0) + var bufHexed = decimalBuffer.toString('hex') + rawNum.replace('.', '') + (strNum.startsWith('-') ? 'b' : 'a') @@ -97,20 +97,20 @@ codec.addExtUnpacker(0x01, (buffer) => { }); // Datetime extension +var datetimeBuffer = createBuffer(16); // reuse buffer codec.addExtPacker(0x04, Date, (date) => { var seconds = date.getTime() / 1000 | 0 var nanoseconds = date.getMilliseconds() * 1000 - var buffer = createBuffer(16) - buffer.writeBigUInt64LE(BigInt(seconds)) - buffer.writeUInt32LE(nanoseconds, 8) - buffer.writeUInt32LE(0, 12) + datetimeBuffer.writeBigUInt64LE(BigInt(seconds)) + datetimeBuffer.writeUInt32LE(nanoseconds, 8) + datetimeBuffer.writeUInt32LE(0, 12) /* Node.Js 'Date' doesn't provide nanoseconds, so just using milliseconds. tzoffset is set to UTC, and tzindex is omitted. */ - return buffer; + return datetimeBuffer; }); codec.addExtUnpacker(0x04, (buffer) => { diff --git a/lib/parser.js b/lib/parser.js index 0e1fb44..68861b2 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -26,20 +26,15 @@ exports._processResponse = function(buffer, offset){ { this.schemaId = schemaId; } - var task; - for(var i = 0; i Date: Thu, 16 Jan 2025 17:06:17 +0200 Subject: [PATCH 02/12] Some big improvements - Now supports [prepared](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/#box-sql-box-prepare) SQL statements. - Huge rewrite of the codebase, which improved performance by N%: - Now using 'msgpackr' instead of 'msgpack-lite' - Buffer reuse - Decreased memory consumption - New AutoPipelining mode: send a batch of commands periodically with no need to rewrite your existing code. Benchmark showed x3 performance for the Select requests: - 330k/sec without AutoPipelining - 1100k/sec with AutoPipelining period set to ~10ms - [IPROTO_ID](https://www.tarantool.io/en/doc/latest/reference/internals/iproto/requests/#iproto-id) can be invoked as 'conn.id()' function. - [Streams](https://www.tarantool.io/en/doc/latest/platform/atomic/txn_mode_mvcc/#streams-and-interactive-transactions) support. --- README.md | 19 +- benchmark/read.js | 6 +- lib/commands.js | 1125 +++++++++++++++++++------------------ lib/connection.js | 101 ++-- lib/const.js | 4 +- lib/event-handler.js | 208 +++---- lib/msgpack-extensions.js | 112 ++-- lib/parser.js | 146 +++-- lib/pipeline.js | 62 +- lib/transaction.js | 16 +- lib/utils.js | 81 +-- package.json | 3 +- 12 files changed, 943 insertions(+), 940 deletions(-) diff --git a/README.md b/README.md index 34e8880..734faac 100755 --- a/README.md +++ b/README.md @@ -357,6 +357,19 @@ It's ok you can do whatever you need. I add log options for some technical infor ## Changelog +### 3.2.0 + +- Now supports [prepared](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/#box-sql-box-prepare) SQL statements. +- Huge rewrite of the codebase, which improved performance by N%: + - Now using 'msgpackr' instead of 'msgpack-lite' + - Buffer reuse + - Decreased memory consumption +- New AutoPipelining mode: send a batch of commands periodically with no need to rewrite your existing code. Benchmark showed x3 performance for the Select requests: + - 330k/sec without AutoPipelining + - 1100k/sec with AutoPipelining period set to ~10ms +- [IPROTO_ID](https://www.tarantool.io/en/doc/latest/reference/internals/iproto/requests/#iproto-id) can be invoked as 'conn.id()' function. +- [Streams](https://www.tarantool.io/en/doc/latest/platform/atomic/txn_mode_mvcc/#streams-and-interactive-transactions) support. + ### 3.1.0 - Added 3 new msgpack extensions: UUID, Datetime, Decimal. @@ -420,7 +433,5 @@ Key is now can be just a number. ## ToDo -1. Streams -2. Events and subscriptions -3. Graceful shutdown protocol -4. Prepared SQL statements \ No newline at end of file +1. Events and subscriptions +2. Graceful shutdown protocol \ No newline at end of file diff --git a/benchmark/read.js b/benchmark/read.js index 107d153..d88d56b 100755 --- a/benchmark/read.js +++ b/benchmark/read.js @@ -12,7 +12,7 @@ var connectionArg = process.argv[process.argv.length - 1] var conn = new Driver(connectionArg, { lazyConnect: true, - tuplesToObjects: true + tuplesToObjects: false }); var connAutoPipelined = new Driver(connectionArg, { @@ -28,10 +28,10 @@ Promise.all([ .then(function () { return Promise.all([ conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, noop), - connAutoPipelined.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, noop), + // connAutoPipelined.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, noop), conn.prepare('SELECT * FROM "counter" WHERE "primary" = ? LIMIT 1 OFFSET 0') .then(function (result) { - preparedSelectStmtId = result.id + preparedSelectStmtId = result; }) ]) }) diff --git a/lib/commands.js b/lib/commands.js index 0b96e37..91e34aa 100755 --- a/lib/commands.js +++ b/lib/commands.js @@ -3,73 +3,37 @@ var { createHash } = require('crypto'); var tarantoolConstants = require('./const'); var { bufferFrom, - createBuffer, TarantoolError, - bufferSubarrayPoly + bufferSubarrayPoly, + withResolvers } = require('./utils'); -var { encode: msgpackEncode, Decoder: msgpackDecoder, decode: msgpackDecode } = require('msgpack-lite'); -var { codec } = require('./msgpack-extensions'); -var { getCachedMsgpackBuffer } = require('./buffer-processor'); -var msgpackr = require('msgpackr'); -var packr = new msgpackr.Packr(); +var { Packr } = require('msgpackr'); +var packr = new Packr(); + +var bufferCache = new Map(); +function getCachedMsgpackBuffer (value) { + var search = bufferCache.get(value); + if (search) { + return search; + } else { + var encoded = packr.encode(value); + bufferCache.set(value, encoded); + return encoded; + }; +}; function Commands() {} -// Commands.prototype.sendCommand = function () {}; var maxSmi = 1<<30 -Commands.prototype._getRequestId = function(){ +Commands.prototype._getRequestId = function _getRequestId (){ var _id = this._id if (_id[0] > maxSmi) _id[0] = 1; return _id[0]++; }; -function _getScaleByNumber (num) { - if (num <= 255) { - return 8; - } else if (num <= 65535) { - return 16; - } else if (num <= 4294967295) { - return 32; - } else if (num <= Number.MAX_SAFE_INTEGER) { - return 64; - } else { - throw new TarantoolError('Exeeded max supported number for scale'); - } -}; - -// reuse buffers and pre-write the first byte -var buffer2bytes = createBuffer(2); -buffer2bytes[0] = 0xcc; -var buffer3bytes = createBuffer(3); -buffer3bytes[0] = 0xcd; -var buffer5bytes = createBuffer(5); -buffer5bytes[0] = 0xce; -var buffer9bytes = createBuffer(9); -buffer9bytes[0] = 0xcf; -function _encodeMsgpackNumber (num, scale = 0) { - if (scale === 0) scale = _getScaleByNumber(num); - - switch (scale) { - case 8: - buffer2bytes.writeUInt8(num, 1); - return buffer2bytes; - case 16: - buffer3bytes.writeUInt16BE(num, 1); - return buffer3bytes; - case 32: - buffer5bytes.writeUInt32BE(num, 1); - return buffer5bytes; - case 64: - buffer9bytes.writeBigUInt64BE(num, 1); - return buffer9bytes; - default: - throw new TarantoolError('Unsupported scale provided: ' + scale); - } -}; - -Commands.prototype._getSpaceId = function(name){ +Commands.prototype._getSpaceId = function _getSpaceId (name){ var _this = this; return this.select(tarantoolConstants.Space.space, tarantoolConstants.IndexSpace.name, 1, 0, 'eq', [name], { @@ -99,7 +63,8 @@ Commands.prototype._getSpaceId = function(name){ } }); }; -Commands.prototype._getIndexId = function(spaceId, indexName){ + +Commands.prototype._getIndexId = function _getIndexId (spaceId, indexName){ var _this = this; return this.select(tarantoolConstants.Space.index, tarantoolConstants.IndexSpace.indexName, 1, 0, 'eq', [spaceId, indexName], { @@ -122,13 +87,69 @@ Commands.prototype._getIndexId = function(spaceId, indexName){ }; Commands.prototype.select = function select (spaceId, indexId, limit, offset, iterator, key, opts = {}) { + if (!(key instanceof Array)) key = [key]; + var _this = this; - return new Promise(function(resolve, reject){ - _this.selectCb(spaceId, indexId, limit, offset, iterator, key, resolve, reject, opts); - }); + + if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) + spaceId = _this.namespace[spaceId].id; + if (typeof(indexId)=='string' && _this.namespace[spaceId] && _this.namespace[spaceId].indexes[indexId]) + indexId = _this.namespace[spaceId].indexes[indexId]; + if (typeof(spaceId)=='string' || typeof(indexId)=='string') + { + return _this._getMetadata(spaceId, indexId) + .then(function(info){ + return _this.select(info[0], info[1], limit, offset, iterator, key, opts); + }) + } + + var reqId = this._getRequestId(); + if (iterator == 'all') + key = []; + var bufKey = packr.encode(key); + + var bufferLength = 42+bufKey.length; + var buffer = this.createBuffer(bufferLength); + + buffer[0] = 0xce; + buffer.writeUInt32BE(37+bufKey.length, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqSelect; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x86; + buffer[21] = tarantoolConstants.KeysCode.space_id; + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = tarantoolConstants.KeysCode.limit; + buffer[28] = 0xce; + buffer.writeUInt32BE(limit, 29); + buffer[33] = tarantoolConstants.KeysCode.offset; + buffer[34] = 0xce; + buffer.writeUInt32BE(offset || 0, 35); + buffer[39] = tarantoolConstants.KeysCode.iterator; + buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 40); + buffer[41] = tarantoolConstants.KeysCode.key; + bufKey.copy(buffer, 42); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqSelect, + reqId, + buffer, + null, + arguments, + opts + ); } -Commands.prototype._getMetadata = function(spaceName, indexName){ +Commands.prototype._getMetadata = function _getMetadata (spaceName, indexName){ var _this = this; var spName = this.namespace[spaceName] // reduce overhead of lookup if (spName) @@ -158,132 +179,139 @@ Commands.prototype._getMetadata = function(spaceName, indexName){ return Promise.all(promises); }; -Commands.prototype.ping = function(){ - var _this = this; - return new Promise(function (resolve, reject) { - var reqId = _this._getRequestId(); - var len = 9; - var buffer = createBuffer(len+5); +Commands.prototype.ping = function ping (opts = {}){ + var reqId = this._getRequestId(); + var len = 9; + var buffer = this.createBuffer(len+5); - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqPing; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x82; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqPing; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); - _this.sendCommand( - tarantoolConstants.RequestCode.rqPing, - reqId, - buffer, - [resolve, reject] - ); - }); + return this.sendCommand( + tarantoolConstants.RequestCode.rqPing, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype.begin = function(transactionTimeout, isolationLevel, opts = {}) { - var _this = this; - var _arguments = arguments; - return new Promise(function (resolve, reject) { - if (!_this.streamId) { - reject( - new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') - ); - } - var reqId = _this._getRequestId(); +Commands.prototype.begin = function begin (transTimeoutSec = 60.01 /* prevent JS from converting 60.0 to 60 */, isolationLevel = 0, opts = {}) { + if (!this.streamId) { + reject( + new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') + ); + } - var headersMap = new Map(); - headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqBegin) - headersMap.set(tarantoolConstants.KeysCode.sync, reqId) - headersMap.set(tarantoolConstants.KeysCode.iproto_stream_id, _this.streamId) - var headersBuffer = packr.encode(headersMap) + var reqId = this._getRequestId(); + // check if not decimal + if (Number.isInteger(transTimeoutSec)) transTimeoutSec += 0.001; + var transTimeoutBuf = packr.encode(transTimeoutSec) - var bodyMap = new Map(); - if (transactionTimeout) { - bodyMap.set(tarantoolConstants.KeysCode.iproto_timeout, transactionTimeout) - } - if (isolationLevel) { - bodyMap.set(tarantoolConstants.KeysCode.iproto_txn_isolation, isolationLevel) - } - var bodyBuffer = packr.encode(bodyMap) - - var dataLengthBuffer = _encodeMsgpackNumber(headersBuffer.length + bodyBuffer.length); - var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer, bodyBuffer]) - - _this.sendCommand( - tarantoolConstants.RequestCode.rqBegin, - reqId, - concatenatedBuffers, - [resolve, reject], - _arguments, - opts._pipelined === true - ); - }); -}; + var len = 19+transTimeoutBuf.length; + var buffer = this.createBuffer(5+len); -Commands.prototype.commit = function() { - var _this = this; - var _arguments = arguments; - return new Promise(function (resolve, reject) { - if (!_this.streamId) { - reject( - new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') - ); - } - var reqId = _this._getRequestId(); + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqBegin; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x82; + buffer[21] = tarantoolConstants.KeysCode.iproto_txn_isolation; + buffer.writeUInt8(isolationLevel, 22); + buffer[23] = tarantoolConstants.KeysCode.iproto_timeout; + transTimeoutBuf.copy(buffer, 24) + + return this.sendCommand( + tarantoolConstants.RequestCode.rqBegin, + reqId, + buffer, + null, + arguments, + opts + ); +}; - var headersMap = new Map(); - headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqCommit) - headersMap.set(tarantoolConstants.KeysCode.sync, reqId) - headersMap.set(tarantoolConstants.KeysCode.iproto_stream_id, _this.streamId) - var headersBuffer = packr.encode(headersMap) - - var dataLengthBuffer = _encodeMsgpackNumber(headersBuffer.length); - var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer]) - - _this.sendCommand( - tarantoolConstants.RequestCode.rqCommit, - reqId, - concatenatedBuffers, - [resolve, reject], - _arguments +Commands.prototype.commit = function commit (opts = {}) { + if (!this.streamId) { + reject( + new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') ); - }); -}; + } + var reqId = this._getRequestId(); -Commands.prototype.rollback = function() { - var _this = this; - var _arguments = arguments; - return new Promise(function (resolve, reject) { - if (!_this.streamId) { - reject( - new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') - ); - } - var reqId = _this._getRequestId(); + var len = 14; + var buffer = this.createBuffer(5+len); - var headersMap = new Map(); - headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqRollback) - headersMap.set(tarantoolConstants.KeysCode.sync, reqId) - headersMap.set(tarantoolConstants.KeysCode.iproto_stream_id, _this.streamId) - var headersBuffer = packr.encode(headersMap) - - var dataLengthBuffer = _encodeMsgpackNumber(headersBuffer.length); - var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer]) - - _this.sendCommand( - tarantoolConstants.RequestCode.rqRollback, - reqId, - concatenatedBuffers, - [resolve, reject], - _arguments + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqCommit; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqCommit, + reqId, + buffer, + null, + arguments, + opts + ); +}; + +Commands.prototype.rollback = function rollback (opts = {}) { + if (!this.streamId) { + reject( + new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') ); - }); + } + var reqId = this._getRequestId(); + + var len = 14; + var buffer = this.createBuffer(5+len); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqRollback; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId, 16); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqRollback, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator, key, success, error, opts = {}){ +Commands.prototype.selectCb = function selectCb (spaceId, indexId, limit, offset, iterator, key, success, error, opts = {}){ if (!(key instanceof Array)) key = [key]; @@ -305,10 +333,10 @@ Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator var reqId = this._getRequestId(); if (iterator == 'all') key = []; - var bufKey = msgpackr.pack(key); + var bufKey = packr.encode(key); var bufferLength = 42+bufKey.length; - var buffer = createBuffer(bufferLength); + var buffer = this.createBuffer(bufferLength); buffer[0] = 0xce; buffer.writeUInt32BE(37+bufKey.length, 1); @@ -348,427 +376,417 @@ Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator ); }; -Commands.prototype.delete = function(spaceId, indexId, key){ +Commands.prototype.delete = function _delete (spaceId, indexId, key, opts = {}){ var _this = this; - var _arguments = arguments; - if (Number.isInteger(key)) - key = [key]; - return new Promise(function (resolve, reject) { - if (Array.isArray(key)) - { - if (typeof(spaceId)=='string' || typeof(indexId)=='string') - { - return _this._getMetadata(spaceId, indexId) - .then(function(info){ - return _this.delete(info[0], info[1], key); - }) - .then(resolve) - .catch(reject); - } - var reqId = _this._getRequestId(); - var bufKey = msgpackEncode(key, {codec}); - - var len = 23+bufKey.length; - var buffer = createBuffer(5+len); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqDelete; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(_this.streamId || 0, 16); - buffer[20] = 0x83; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 26); - buffer[27] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 28); - - _this.sendCommand( - tarantoolConstants.RequestCode.rqDelete, - reqId, - buffer, - [resolve, reject], - _arguments, - _this._pipelined === true - ); - } - else - reject(new TarantoolError('need array')); - }); + if (Number.isInteger(key)) key = [key]; + if (!Array.isArray(key)) return Promise.reject(new TarantoolError('need array')); + if (typeof(spaceId)=='string' || typeof(indexId)=='string') { + return this._getMetadata(spaceId, indexId) + .then(function(info){ + return _this.delete(info[0], info[1], key, opts); + }) + } + var reqId = this._getRequestId(); + var bufKey = packr.encode(key); + + var len = 23+bufKey.length; + var buffer = this.createBuffer(5+len); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqDelete; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x83; + buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = tarantoolConstants.KeysCode.key; + bufKey.copy(buffer, 28); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqDelete, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype.update = function(spaceId, indexId, key, ops){ +Commands.prototype.update = function update (spaceId, indexId, key, ops, opts = {}){ + if (Number.isInteger(key)) key = [key]; + if (!(Array.isArray(ops) && Array.isArray(key))) return Promise.reject(new TarantoolError('need array')); + var _this = this; - var _arguments = arguments; - if (Number.isInteger(key)) - key = [key]; - return new Promise(function (resolve, reject) { - if (Array.isArray(ops) && Array.isArray(key)){ - if (typeof(spaceId)=='string' || typeof(indexId)=='string') { - return _this._getMetadata(spaceId, indexId) - .then(function(info){ - return _this.update(info[0], info[1], key, ops); - }) - .then(resolve) - .catch(reject); - } - var reqId = _this._getRequestId(); - var bufKey = msgpackEncode(key, {codec}); - var bufOps = msgpackEncode(ops, {codec}); - - var len = 24+bufKey.length+bufOps.length; - var buffer = createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqUpdate; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(_this.streamId || 0, 16); - buffer[20] = 0x84; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 26); - buffer[27] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 28); - buffer[28+bufKey.length] = tarantoolConstants.KeysCode.tuple; - bufOps.copy(buffer, 29+bufKey.length); - - _this.sendCommand( - tarantoolConstants.RequestCode.rqUpdate, - reqId, - buffer, - [resolve, reject], - _arguments, - _this._pipelined === true - ); - } - else - reject(new TarantoolError('need array')); - }); + + if (typeof(spaceId)=='string' || typeof(indexId)=='string') { + return this._getMetadata(spaceId, indexId) + .then(function(info){ + return _this.update(info[0], info[1], key, ops, opts); + }) + } + var reqId = this._getRequestId(); + var bufKey = packr.encode(key); + var bufOps = packr.encode(ops); + + var len = 24+bufKey.length+bufOps.length; + var buffer = this.createBuffer(len+5); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqUpdate; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x84; + buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = tarantoolConstants.KeysCode.key; + bufKey.copy(buffer, 28); + buffer[28+bufKey.length] = tarantoolConstants.KeysCode.tuple; + bufOps.copy(buffer, 29+bufKey.length); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqUpdate, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype.upsert = function(spaceId, ops, tuple){ +Commands.prototype.upsert = function upsert (spaceId, ops, tuple, opts = {}){ var _this = this; - var _arguments = arguments; - return new Promise(function (resolve, reject) { - if (Array.isArray(ops)){ - if (typeof(spaceId)=='string') - { - return _this._getMetadata(spaceId, 0) - .then(function(info){ - return _this.upsert(info[0], ops, tuple); - }) - .then(resolve) - .catch(reject); - } - var reqId = _this._getRequestId(); - var bufTuple = msgpackEncode(tuple, {codec}); - var bufOps = msgpackEncode(ops, {codec}); - - var len = 22+bufTuple.length+bufOps.length; - var buffer = createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqUpsert; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(_this.streamId || 0, 16); - buffer[20] = 0x83; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 26); - buffer[26+bufTuple.length] = tarantoolConstants.KeysCode.def_tuple; - bufOps.copy(buffer, 27+bufTuple.length); - - _this.sendCommand( - tarantoolConstants.RequestCode.rqUpsert, - reqId, - buffer, - [resolve, reject], - _arguments, - _this._pipelined === true - ); - } - else - reject(new TarantoolError('need ops array')); - }); + if (!Array.isArray(ops)) return Promise.reject(new TarantoolError('need ops array')); + if (typeof(spaceId)=='string') { + return this._getMetadata(spaceId, 0) + .then(function(info){ + return _this.upsert(info[0], ops, tuple, opts); + }) + } + var reqId = this._getRequestId(); + var bufTuple = packr.encode(tuple); + var bufOps = packr.encode(ops); + + var len = 22+bufTuple.length+bufOps.length; + var buffer = this.createBuffer(len+5); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqUpsert; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x83; + buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.tuple; + bufTuple.copy(buffer, 26); + buffer[26+bufTuple.length] = tarantoolConstants.KeysCode.def_tuple; + bufOps.copy(buffer, 27+bufTuple.length); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqUpsert, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype.eval = function(expression){ - var _this = this; - var _arguments = arguments; +Commands.prototype.eval = function eval (expression, opts = {}){ var tuple = Array.prototype.slice.call(arguments, 1); - return new Promise(function (resolve, reject) { - var reqId = _this._getRequestId(); - var bufExp = msgpackEncode(expression); - var bufTuple = msgpackEncode(tuple ? tuple : [], {codec}); - var len = 18+bufExp.length + bufTuple.length; - var buffer = createBuffer(len+5); + var reqId = this._getRequestId(); + var bufExp = getCachedMsgpackBuffer(expression); + var bufTuple = packr.encode(tuple ? tuple : []); + var len = 18+bufExp.length + bufTuple.length; + var buffer = this.createBuffer(len+5); - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqEval; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(_this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.expression, 21); - bufExp.copy(buffer, 22); - buffer[22+bufExp.length] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 23+bufExp.length); - - _this.sendCommand( - tarantoolConstants.RequestCode.rqEval, - reqId, - buffer, - [resolve, reject], - _arguments, - _this._pipelined === true - ); - }); + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqEval; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(tarantoolConstants.KeysCode.expression, 21); + bufExp.copy(buffer, 22); + buffer[22+bufExp.length] = tarantoolConstants.KeysCode.tuple; + bufTuple.copy(buffer, 23+bufExp.length); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqEval, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype.call = function(functionName){ - var _this = this; - var _arguments = arguments; +Commands.prototype.call = function call (functionName, opts = {}){ var tuple = arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : []; - return new Promise(function (resolve, reject) { - var reqId = _this._getRequestId(); - var bufName = msgpackEncode(functionName); - var bufTuple = msgpackEncode(tuple ? tuple : [], {codec}); - var len = 18+bufName.length + bufTuple.length; - var buffer = createBuffer(len+5); + var reqId = this._getRequestId(); + var bufName = getCachedMsgpackBuffer(functionName); + var bufTuple = packr.encode(tuple ? tuple : []); + var len = 18+bufName.length + bufTuple.length; + var buffer = this.createBuffer(len+5); - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqCall; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(_this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.function_name, 21); - bufName.copy(buffer, 22); - buffer[22+bufName.length] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 23+bufName.length); - - _this.sendCommand( - tarantoolConstants.RequestCode.rqCall, - reqId, - buffer, - [resolve, reject], - _arguments, - _this._pipelined === true - ); - }); + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqCall; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(tarantoolConstants.KeysCode.function_name, 21); + bufName.copy(buffer, 22); + buffer[22+bufName.length] = tarantoolConstants.KeysCode.tuple; + bufTuple.copy(buffer, 23+bufName.length); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqCall, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype.sql = function(query, bindParams = []){ - var _this = this; - var _arguments = arguments; - return new Promise(function (resolve, reject) { - var reqId = _this._getRequestId(); - // var bufParams = msgpackEncode(bindParams, {codec}); - var bufParams = msgpackr.pack(bindParams); - var isPreparedStatement = (typeof query === 'number') // in case of the statement ID being passed to 'query' param - var bufQuery = isPreparedStatement ? getCachedMsgpackBuffer(query) : msgpackr.pack(query); // cache only prepared queries, considering them frequently used +Commands.prototype.sql = function sql (query, bindParams = [], opts = {}){ + var reqId = this._getRequestId(); + var bufParams = packr.encode(bindParams); + var isPreparedStatement = (typeof query === 'number') // in case of the statement ID being passed to 'query' param + var bufQuery = isPreparedStatement ? getCachedMsgpackBuffer(query) : packr.encode(query); // cache only prepared queries, considering them to be frequently used - var len = 18+bufQuery.length + bufParams.length; - var buffer = createBuffer(len+5); + var len = 18+bufQuery.length + bufParams.length; + var buffer = this.createBuffer(len+5); - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqExecute; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(_this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(isPreparedStatement ? tarantoolConstants.KeysCode.stmt_id : tarantoolConstants.KeysCode.sql_text, 21); - bufQuery.copy(buffer, 22); - buffer[22+bufQuery.length] = tarantoolConstants.KeysCode.sql_bind; - bufParams.copy(buffer, 23+bufQuery.length); - - _this.sendCommand( - tarantoolConstants.RequestCode.rqExecute, - reqId, - buffer, - [resolve, reject], - _arguments, - _this._pipelined === true - ); - }); + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqExecute; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(isPreparedStatement ? tarantoolConstants.KeysCode.stmt_id : tarantoolConstants.KeysCode.sql_text, 21); + bufQuery.copy(buffer, 22); + buffer[22+bufQuery.length] = tarantoolConstants.KeysCode.sql_bind; + bufParams.copy(buffer, 23+bufQuery.length); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqExecute, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype.prepare = function(query, opts = {}){ +Commands.prototype.sql = function sql (query, bindParams = [], opts = {}){ var _this = this; var _arguments = arguments; - return new Promise(function (resolve, reject) { - var reqId = _this._getRequestId(); - var bufQuery = msgpackEncode(query); + var {promise, resolve, reject} = withResolvers(); - var len = 13+bufQuery.length; - var buffer = createBuffer(len+5); + var reqId = _this._getRequestId(); + // var bufParams = packr.encode(bindParams); + var bufParams = packr.encode(bindParams); + var isPreparedStatement = (typeof query === 'number') // in case of the statement ID being passed to 'query' param + var bufQuery = isPreparedStatement ? getCachedMsgpackBuffer(query) : packr.encode(query); // cache only prepared queries, considering them frequently used - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqPrepare; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.sql_text, 15); - bufQuery.copy(buffer, 16); - - _this.sendCommand( - tarantoolConstants.RequestCode.rqPrepare, - reqId, - buffer, - [resolve, reject], - _arguments, - opts - ); - }); + var len = 18+bufQuery.length + bufParams.length; + var buffer = this.createBuffer(len+5); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqExecute; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(_this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(isPreparedStatement ? tarantoolConstants.KeysCode.stmt_id : tarantoolConstants.KeysCode.sql_text, 21); + bufQuery.copy(buffer, 22); + buffer[22+bufQuery.length] = tarantoolConstants.KeysCode.sql_bind; + bufParams.copy(buffer, 23+bufQuery.length); + + _this.sendCommand( + tarantoolConstants.RequestCode.rqExecute, + reqId, + buffer, + [resolve, reject], + _arguments, + opts + ); + + return promise; }; -Commands.prototype.id = function(version = 3, features = [1], auth_type = 'chap-sha1'){ - var _this = this; - var _arguments = arguments; - return new Promise(function (resolve, reject) { - var reqId = _this._getRequestId(); +Commands.prototype.prepare = function prepare (query, opts = {}){ + var reqId = this._getRequestId(); + var bufQuery = packr.encode(query); - var headersMap = new Map(); - headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqId) - headersMap.set(tarantoolConstants.KeysCode.sync, reqId) - var headersBuffer = packr.encode(headersMap) - - var bodyMap = new Map(); - bodyMap.set(tarantoolConstants.KeysCode.iproto_version, version) - bodyMap.set(tarantoolConstants.KeysCode.iproto_features, features) - bodyMap.set(tarantoolConstants.KeysCode.iproto_auth_type, auth_type) - var bodyBuffer = packr.encode(bodyMap) - - var dataLengthBuffer = _encodeMsgpackNumber(headersBuffer.length + bodyBuffer.length); - var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer, bodyBuffer]) - - _this.sendCommand( - tarantoolConstants.RequestCode.rqId, - reqId, - concatenatedBuffers, - [resolve, reject], - _arguments - ); - }); + var len = 13+bufQuery.length; + var buffer = this.createBuffer(len+5); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x82; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqPrepare; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = 0x82; + buffer.writeUInt8(tarantoolConstants.KeysCode.sql_text, 15); + bufQuery.copy(buffer, 16); + + return this.sendCommand( + tarantoolConstants.RequestCode.rqPrepare, + reqId, + buffer, + null, + arguments, + opts + ); +}; + +Commands.prototype.id = function id (version = 3, features = [1], auth_type = 'chap-sha1', opts){ + var reqId = this._getRequestId(); + + var headersMap = new Map(); + headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqId) + headersMap.set(tarantoolConstants.KeysCode.sync, reqId) + var headersBuffer = packr.encode(headersMap) + + var bodyMap = new Map(); + bodyMap.set(tarantoolConstants.KeysCode.iproto_version, version) + bodyMap.set(tarantoolConstants.KeysCode.iproto_features, features) + bodyMap.set(tarantoolConstants.KeysCode.iproto_auth_type, auth_type) + var bodyBuffer = packr.encode(bodyMap) + + var dataLengthBuffer = packr.encode(headersBuffer.length + bodyBuffer.length); + var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer, bodyBuffer]) + + return this.sendCommand( + tarantoolConstants.RequestCode.rqId, + reqId, + concatenatedBuffers, + null, + arguments, + opts + ); }; -Commands.prototype.insert = function(spaceId, tuple){ +Commands.prototype.insert = function insert (spaceId, tuple, opts = {}){ var reqId = this._getRequestId(); - return this._replaceInsert(tarantoolConstants.RequestCode.rqInsert, reqId, spaceId, tuple); + return this._replaceInsert(tarantoolConstants.RequestCode.rqInsert, reqId, spaceId, tuple, opts); }; -Commands.prototype.replace = function(spaceId, tuple){ +Commands.prototype.replace = function replace (spaceId, tuple, opts = {}){ var reqId = this._getRequestId(); - return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, reqId, spaceId, tuple); + return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, reqId, spaceId, tuple, opts); }; -Commands.prototype._replaceInsert = function(cmd, reqId, spaceId, tuple, opts = {}){ +Commands.prototype._replaceInsert = function _replaceInsert (cmd, reqId, spaceId, tuple, opts = {}){ + if (!Array.isArray(tuple)) return Promise.reject(new TarantoolError('need array')); + var _this = this; - var _arguments = arguments; - return new Promise(function (resolve, reject) { - if (Array.isArray(tuple)){ - if (typeof(spaceId)=='string') - { - return _this._getMetadata(spaceId, 0) - .then(function(info){ - return _this._replaceInsert(cmd, reqId, info[0], tuple); - }) - .then(resolve) - .catch(reject); - } - var bufTuple = msgpackEncode(tuple); - var len = 21+bufTuple.length; - var buffer = createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = cmd; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(_this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 26); - - _this.sendCommand( - cmd, - reqId, - buffer, - [resolve, reject], - _arguments, - _this._pipelined === true - ); - } - else - reject(new TarantoolError('need array')); - }); + + if (typeof(spaceId)=='string') + { + return this._getMetadata(spaceId, 0) + .then(function(info){ + return _this._replaceInsert(cmd, reqId, info[0], tuple, opts); + }) + } + var bufTuple = packr.encode(tuple); + var len = 21+bufTuple.length; + var buffer = this.createBuffer(len+5); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = cmd; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = tarantoolConstants.KeysCode.tuple; + bufTuple.copy(buffer, 26); + + return this.sendCommand( + cmd, + reqId, + buffer, + null, + arguments, + opts + ); }; -Commands.prototype._auth = function(username, password){ +Commands.prototype._auth = function _auth (username, password){ var _this = this; return new Promise(function (resolve, reject) { var reqId = _this._getRequestId(); - var user = msgpackEncode(username); + var user = packr.encode(username); var scrambled = scramble(password, _this.salt); var len = 44+user.length; - var buffer = createBuffer(len+5); + var buffer = _this.createBuffer(len+5); buffer[0] = 0xce; buffer.writeUInt32BE(len, 1); @@ -820,7 +838,14 @@ function scramble(password, salt){ var encSalt = bufferFrom(salt, 'base64'); var step1 = shatransform(password); var step2 = shatransform(step1); - var step3 = shatransform(Buffer.concat([encSalt[bufferSubarrayPoly](0, 20), step2])); + var step3 = shatransform( + Buffer.concat( + [ + bufferSubarrayPoly.apply(encSalt, [0, 20]), + step2 + ] + ) + ); return xor(step1, step3); } diff --git a/lib/connection.js b/lib/connection.js index 68aee47..0cdcfe0 100755 --- a/lib/connection.js +++ b/lib/connection.js @@ -2,14 +2,19 @@ var EventEmitter = require('events').EventEmitter; var util = require('util'); var debug = require('debug')('tarantool-driver:main'); -var _ = require('lodash'); +var { + pick, + defaults, + assign, + noop +} = require('lodash'); var { parseURL, TarantoolError, - findPipelineError, - findPipelineErrors + withResolvers, + createBuffer } = require('./utils'); -var { packAs } = require('./msgpack-extensions'); +var msgpackExt = require('./msgpack-extensions'); var tarantoolConstants = require('./const'); var Commands = require('./commands'); var Pipeline = require('./pipeline'); @@ -18,7 +23,6 @@ var Parser = require('./parser'); var Connector = require('./connector'); var AutoPipeline = require('./autopipelining'); var eventHandler = require('./event-handler'); -var SliderBuffer = require('./sliderBuffer') var revertStates = { 0: 'connecting', @@ -34,7 +38,7 @@ var revertStates = { 512: 'changing_host' }; -TarantoolConnection.defaultOptions = { +var defaultOptions = { host: 'localhost', port: 3301, path: null, @@ -72,7 +76,7 @@ function TarantoolConnection (){ this.parseOptions = parseOptions; this.parseOptions(arguments[0], arguments[1], arguments[2]); this.connector = new Connector(this.options); - this.schemaId = null; + this.schemaId = 0; this.states = { CONNECTING: 0, CONNECTED: 1, @@ -91,15 +95,11 @@ function TarantoolConnection (){ this.commandsQueue = new Array(); this.offlineQueue = new Array(); this.namespace = {}; - this.bufferSlide = new SliderBuffer(); - this.awaitingResponseLength = -1; this.retryAttempts = 0; this._id = [ 1 ]; this.sentCommands = new Map(); this.autoPipelineQueue = new Array(); this.autoPipeliningId = 0; - this.findPipelineError = findPipelineError; - this.findPipelineErrors = findPipelineErrors; this.resetOfflineQueue = resetOfflineQueue; this.IteratorsType = tarantoolConstants.IteratorsType; this.useNextReserve = useNextReserve; @@ -108,26 +108,25 @@ function TarantoolConnection (){ this.connect = connect; this.flushQueue = flushQueue; this.silentEmit = silentEmit; - this.destroy = destroy; - this.disconnect = disconnect; + this.destroy = this.disconnect = disconnect; + this.errorHandler = eventHandler.errorHandler + this.createBuffer = createBuffer Object.assign(this, Commands.prototype); Object.assign(this, AutoPipeline.prototype); Object.assign(this, Parser); Object.assign(this, Transaction.prototype); - Object.assign(this, Pipeline.prototype); + Object.assign(this, Pipeline); + Object.assign(this, msgpackExt); + if (this.options.lazyConnect) { this.setState(this.states.INITED); } else { - this.connect().catch(_.noop); + this.connect().catch(noop); } } util.inherits(TarantoolConnection, EventEmitter); -for (var packerName of Object.keys(packAs)) { - TarantoolConnection.prototype['pack' + packerName] = packAs[packerName] -} - function resetOfflineQueue () { this.offlineQueue = new Array(); }; @@ -140,24 +139,29 @@ function parseOptions (){ if (arg === null || typeof arg === 'undefined') { continue; } - if (typeof arg === 'object') { - _.defaults(this.options, arg); - } else if (typeof arg === 'string') { - if(!isNaN(arg) && (parseFloat(arg) | 0) === parseFloat(arg)){ + + switch (typeof arg) { + case 'object': + defaults(this.options, arg); + break; + case 'string': + if(!isNaN(arg) && (parseFloat(arg) | 0) === parseFloat(arg)){ + this.options.port = arg; + continue; + } + defaults(this.options, parseURL(arg)); + break; + case 'number': this.options.port = arg; - continue; - } - _.defaults(this.options, parseURL(arg)); - } else if (typeof arg === 'number') { - this.options.port = arg; - } else { - throw new TarantoolError('Invalid argument ' + arg); + break; + default: + throw new TarantoolError('Invalid argument ' + arg); } } - _.defaults(this.options, TarantoolConnection.defaultOptions); + defaults(this.options, defaultOptions); var reserveHostsLength = this.options.reserveHosts && this.options.reserveHosts.length || 0 if ((this.options.nonWritableHostPolicy != null) && (reserveHostsLength == 0)) { - throw new TarantoolError('\'nonWritableHostPolicy\' option is specified, but there are no reserve hosts. Specify it in connection options via \'reserveHosts\'') + throw new TarantoolError('\'nonWritableHostPolicy\' option is specified, but there are no reserve hosts. Specify them in connection options via \'reserveHosts\'') } if (typeof this.options.port === 'string') { this.options.port = parseInt(this.options.port, 10); @@ -166,9 +170,9 @@ function parseOptions (){ delete this.options.port delete this.options.host } - if (reserveHostsLength > 0){ + if (reserveHostsLength > 0) { this.reserveIterator = 1; - this.reserve.push(_.pick(this.options, ['port', 'host', 'username', 'password', 'path'])); + this.reserve.push(pick(this.options, ['port', 'host', 'username', 'password', 'path'])); for(i = 0; i %s(%s)', requestCode, reqId); @@ -220,6 +232,8 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o ]); } else { var tuplesToObjects = compareBooleans(opts.tuplesToObjects, this.options.tuplesToObjects); + + // create an array which will be stored till the response is received var setValue = [ requestCode, callbacks, @@ -239,7 +253,8 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o this.sentCommands.set(reqId, setValue); // in pipelined mode data is written via its own function - if ((this.options.autoPipeliningPeriod > 0) && (!opts._pipelined) && (opts.autoPipeline != false)) { + var shouldAutoPipeline = (this.options.autoPipeliningPeriod > 0) && (opts.autoPipeline !== false) && (opts._pipelined !== true) + if (shouldAutoPipeline) { this._addToAutoPipeliningQueue(buffer); } else if (!opts._pipelined) { this.socket.write(buffer); @@ -263,6 +278,8 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o opts ]); } + + return promise; }; function setState (state, arg) { @@ -299,7 +316,7 @@ function connect (){ } _this.socket = socket; socket.once('connect', eventHandler.connectHandler(_this)); - socket.once('error', eventHandler.errorHandler(_this)); + socket.once('error', _this.errorHandler.bind(_this)) socket.once('close', eventHandler.closeHandler(_this)); socket.on('data', eventHandler.dataHandler(_this)); @@ -312,7 +329,7 @@ function connect (){ error.errorno = 'ETIMEDOUT'; error.code = 'ETIMEDOUT'; error.syscall = 'connect'; - eventHandler.errorHandler(_this)(error); + _this.errorHandler(error); }); socket.once('connect', function () { socket.setTimeout(0); @@ -372,10 +389,6 @@ function silentEmit (eventName) { return false; }; -function destroy () { - this.disconnect(); -}; - function disconnect (reconnect){ if (!reconnect) { this.manuallyClosing = true; @@ -391,6 +404,4 @@ function disconnect (reconnect){ } }; -// TarantoolConnection.prototype.IteratorsType = tarantoolConstants.IteratorsType; - module.exports = TarantoolConnection; diff --git a/lib/const.js b/lib/const.js index 3100261..901b0f4 100755 --- a/lib/const.js +++ b/lib/const.js @@ -12,7 +12,7 @@ var RequestCode = { rqEval: 0x08, rqUpsert: 0x09, rqRollback: 0x10, - rqCallNew: 0x0a, // ? + rqCallNew: 0x0a, rqExecute: 0x0b, rqPrepare: 0x0d, rqBegin: 0x0e, @@ -94,8 +94,6 @@ var OkCode = 0; var NetErrCode = 0xfffffff1; // fake code to wrap network problems into response var TimeoutErrCode = 0xfffffff2; // fake code to wrap timeout error into repsonse -var PacketLengthBytes = 5; - var Space = { schema: 272, space: 281, diff --git a/lib/event-handler.js b/lib/event-handler.js index a124e40..3591e9b 100755 --- a/lib/event-handler.js +++ b/lib/event-handler.js @@ -1,10 +1,6 @@ -// var msgpack = require('msgpack-lite'); var debug = require('debug')('tarantool-driver:handler'); -var utils = require('./utils'); -var tarantoolConstants = require('./const'); - -// var Decoder = msgpack.Decoder; -// var decoder = new Decoder(); +var {TarantoolError} = require('./utils'); +var {UnpackrStream} = require('msgpackr'); exports.connectHandler = function (self) { return function () { @@ -23,7 +19,7 @@ exports.connectHandler = function (self) { sendOfflineQueue.call(self); }, function(err){ self.flushQueue(err); - self.silentEmit('error', err); + self.errorHandler(err); self.disconnect(true); }); } else { @@ -47,147 +43,87 @@ function sendOfflineQueue(){ } } -exports.dataHandler = function(self){ - var buffer = null; +function createUnpackrStream () { + var self = this; + var decodedHeaders = {}; + var decodingStep = 0; - return function(data){ - switch(self.dataState){ - case self.states.PREHELLO: - self.salt = data.slice(64, 108).toString('utf8'); - self.dataState = self.states.CONNECTED; - self.setState(self.states.CONNECTED); - exports.connectHandler(self)(); - break; - case self.states.CONNECTED: - if (data.length >= 5) - { - var len = data.readUInt32BE(1); - var offset = 5; - while(len > 0 && len+offset <= data.length) - { - self._processResponse(data, offset, len); - offset+=len; - if (data.length - offset) - { - if (data.length-offset >= 5) - { - len = data.readUInt32BE(offset+1); - offset+=5; - } - else - { - len = -1; - } - } - else - { - return; - } - } - if (len) - self.awaitingResponseLength = len; - if (self.awaitingResponseLength>0) - self.dataState = self.states.AWAITING; - if (self.awaitingResponseLength<0) - self.dataState = self.states.AWAITING_LENGTH; - self.bufferSlide.add(data, offset, data.length - offset); - } - else - { - self.dataState = self.states.AWAITING_LENGTH; - self.bufferSlide.add(data); - return; - } - break; - case self.states.AWAITING: - self.bufferSlide.add(data); - while(self.awaitingResponseLength > 0 && self.awaitingResponseLength <= self.bufferSlide.bufferLength) - { - self._processResponse(self.bufferSlide.buffer, self.bufferSlide.bufferOffset); - self.bufferSlide.bufferOffset += self.awaitingResponseLength; - self.bufferSlide.bufferLength -= self.awaitingResponseLength; - if (self.bufferSlide.bufferLength) - { - if (self.bufferSlide.bufferLength>=5) - { - self.awaitingResponseLength = self.bufferSlide.buffer.readUInt32BE(self.bufferSlide.bufferOffset+1); - self.bufferSlide.bufferLength-=5; - self.bufferSlide.bufferOffset+=5; - } - else - { - self.awaitingResponseLength = -1; - } - } - else - { - self.awaitingResponseLength = -1; - self.dataState = self.states.CONNECTED; - self.state = self.states.CONNECT; - return; - } - } - if (self.awaitingResponseLength>0) - self.dataState = self.states.AWAITING; - self.state = self.states.AWAITING; - if (self.awaitingResponseLength<0) - self.dataState = self.states.AWAITING_LENGTH; - self.state = self.states.AWAITING_LENGTH; + var unpackrStream = new UnpackrStream({ + mapsAsObjects: true, + int64AsType: "auto", + }); + + unpackrStream.on("error", function (error) { + self.errorHandler( + new TarantoolError("Msgpack unpacker errored", { + cause: error, + }) + ); + }); + + unpackrStream.on("data", function (data) { + var type = typeof data; + switch (type) { + case "number": + decodingStep = 0; + break; + case "object": + switch (decodingStep) { + case 0: + decodedHeaders = data; + decodingStep = 1; break; - case self.states.AWAITING_LENGTH: - self.bufferSlide.add(data); - if (self.bufferSlide.bufferLength >= 5) - { - self.awaitingResponseLength = self.bufferSlide.buffer.readUInt32BE(self.bufferSlide.bufferOffset+1); - self.bufferSlide.bufferLength-=5; - self.bufferSlide.bufferOffset+=5; - while(self.awaitingResponseLength >0 && self.awaitingResponseLength <= self.bufferSlide.bufferLength) - { - self._processResponse(self.bufferSlide.buffer, self.bufferSlide.bufferOffset, self.awaitingResponseLength); - self.bufferSlide.bufferOffset += self.awaitingResponseLength; - self.bufferSlide.bufferLength -= self.awaitingResponseLength; - if (self.bufferSlide.bufferLength) - { - if (self.bufferSlide.bufferLength>=5) - { - self.awaitingResponseLength = self.bufferSlide.buffer.readUInt32BE(self.bufferSlide.bufferOffset+1); - self.bufferSlide.bufferLength-=5; - self.bufferSlide.bufferOffset+=5; - } - else - { - self.awaitingResponseLength = -1; - } - } - else - { - self.awaitingResponseLength = -1; - self.dataState = self.states.CONNECTED; - self.state = self.states.CONNECT; - return; - } - } - if (self.awaitingResponseLength>0) - self.dataState = self.states.AWAITING; - if (self.awaitingResponseLength<0) - self.dataState = self.states.AWAITING_LENGTH; - } + case 1: + self.processResponse(decodedHeaders, data); + decodingStep = 2; break; + default: + self.errorHandler( + new TarantoolError( + "Unknown step detected while decoding response data, maybe network loss occured with some bytes?" + ) + ); + } + break; + default: + self.errorHandler( + new TarantoolError( + "Type of decoded value does not satisfy requirements: " + type + ) + ); + } + }); + + return unpackrStream; +} + +exports.dataHandler = function (self) { + var unpackrStream = createUnpackrStream.call(self); + return function (data) { + switch (self.dataState) { + case self.states.PREHELLO: + self.salt = data.toString("utf8").split('\n')[1]; + self.dataState = self.states.CONNECTED; + self.setState(self.states.CONNECTED); + exports.connectHandler(self)(); + break; + case self.states.CONNECTED: + unpackrStream.write(data) + break; } }; }; -exports.errorHandler = function(self){ - return function(error){ - debug('error: %s', error); - self.silentEmit('error', error); - }; +function errorHandler (error){ + debug('error: %s', error); + this.silentEmit('error', error); }; +exports.errorHandler = errorHandler; exports.closeHandler = function (self) { function close () { self.setState(self.states.END); - self.flushQueue(new utils.TarantoolError('Connection is closed.')); + self.flushQueue(new TarantoolError('Connection is closed.')); } return function(){ diff --git a/lib/msgpack-extensions.js b/lib/msgpack-extensions.js index 5db793e..9cf84ab 100644 --- a/lib/msgpack-extensions.js +++ b/lib/msgpack-extensions.js @@ -1,4 +1,4 @@ -var { createCodec: msgpackCreateCodec } = require('msgpack-lite'); +var { addExtension } = require('msgpackr'); var { TarantoolError, createBuffer, @@ -14,7 +14,6 @@ var { } = require("int64-buffer"); var packAs = {}; -var codec = msgpackCreateCodec(); // Pack big integers correctly (fix for https://github.com/tarantool/node-tarantool-driver/issues/48) packAs.Integer = function (value) { @@ -35,12 +34,15 @@ packAs.Uuid = function TarantoolUuidExt (value) { this.value = value } -codec.addExtPacker(0x02, packAs.Uuid, (data) => { - return uuidParse(data.value); -}); - -codec.addExtUnpacker(0x02, (buffer) => { - return uuidStringify(buffer) +addExtension({ + Class: packAs.Uuid, + type: 0x02, + pack(instance) { + return uuidParse(instance.value); + }, + unpack(buffer) { + return uuidStringify(buffer) + } }); // Decimal extension @@ -65,64 +67,72 @@ function isOdd (number) { }; var decimalBuffer = createBuffer(1); // reuse buffer -codec.addExtPacker(0x01, packAs.Decimal, (data) => { - var strNum = data.value.toString() - var rawNum = strNum.replace('-', '') - var rawNumSplitted1 = rawNum.split('.')[1] - decimalBuffer.writeInt8(rawNumSplitted1 && rawNum.split('.')[1].length || 0) - var bufHexed = decimalBuffer.toString('hex') - + rawNum.replace('.', '') - + (strNum.startsWith('-') ? 'b' : 'a') - - if (isOdd(bufHexed.length)) { - bufHexed = bufHexed.slice(0, 2) + '0' + bufHexed.slice(2) - } - - return bufferFrom(bufHexed, 'hex') -}); +addExtension({ + Class: packAs.Decimal, + type: 0x01, + pack(instance) { + var strNum = instance.value.toString() + var rawNum = strNum.replace('-', '') + var rawNumSplitted1 = rawNum.split('.')[1] + decimalBuffer.writeInt8(rawNumSplitted1 && rawNum.split('.')[1].length || 0) + var bufHexed = decimalBuffer.toString('hex') + + rawNum.replace('.', '') + + (strNum.startsWith('-') ? 'b' : 'a') + + if (isOdd(bufHexed.length)) { + bufHexed = bufHexed.slice(0, 2) + '0' + bufHexed.slice(2) + } -codec.addExtUnpacker(0x01, (buffer) => { - var scale = buffer.readIntBE(0, 1) + return bufferFrom(bufHexed, 'hex') + }, + unpack(buffer) { + var scale = buffer.readIntBE(0, 1) var hex = buffer.toString('hex') var sign = ['b', 'd'].includes(hex.slice(-1)) ? '-' : '+' var slicedValue = hex.slice(2).slice(0, -1) + // readDoubleBE if (scale > 0) { var nScale = scale * -1 slicedValue = slicedValue.slice(0, nScale) + '.' + slicedValue.slice(nScale) } return parseFloat(sign + slicedValue) + } }); // Datetime extension var datetimeBuffer = createBuffer(16); // reuse buffer -codec.addExtPacker(0x04, Date, (date) => { - var seconds = date.getTime() / 1000 | 0 - var nanoseconds = date.getMilliseconds() * 1000 - - datetimeBuffer.writeBigUInt64LE(BigInt(seconds)) - datetimeBuffer.writeUInt32LE(nanoseconds, 8) - datetimeBuffer.writeUInt32LE(0, 12) - /* - Node.Js 'Date' doesn't provide nanoseconds, so just using milliseconds. - tzoffset is set to UTC, and tzindex is omitted. - */ - - return datetimeBuffer; -}); - -codec.addExtUnpacker(0x04, (buffer) => { - var time = new Date(parseInt(buffer.readBigUInt64LE(0)) * 1000) - - if (buffer.length > 8) { - var milliseconds = (buffer.readUInt32LE(8) / 1000 | 0) - time.setMilliseconds(milliseconds) - } +addExtension({ + Class: Date, + type: 0x04, + pack(instance) { + var seconds = instance.getTime() / 1000 | 0 + var nanoseconds = instance.getMilliseconds() * 1000 + + datetimeBuffer.writeBigUInt64LE(BigInt(seconds)) + datetimeBuffer.writeUInt32LE(nanoseconds, 8) + datetimeBuffer.writeUInt32LE(0, 12) + /* + Node.Js 'Date' doesn't provide nanoseconds, so just using milliseconds. + tzoffset is set to UTC, and tzindex is omitted. + */ + + return datetimeBuffer; + }, + unpack(buffer) { + var time = new Date(parseInt(buffer.readBigUInt64LE(0)) * 1000) + + if (buffer.length > 8) { + var milliseconds = (buffer.readUInt32LE(8) / 1000 | 0) + time.setMilliseconds(milliseconds) + } - return time -}) + return time; + } +}); -exports.packAs = packAs -exports.codec = codec \ No newline at end of file +for (var packerName of Object.keys(packAs)) { + exports['pack' + packerName] = packAs[packerName] +} \ No newline at end of file diff --git a/lib/parser.js b/lib/parser.js index 68861b2..4a32e1d 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,85 +1,85 @@ -var { Decoder: msgpackDecoder } = require('msgpack-lite'); -var tarantoolConstants = require('./const'); +var { KeysCode, RequestCode } = require('./const'); var { TarantoolError } = require('./utils'); -var { codec } = require('./msgpack-extensions'); +var debug = require('debug')('tarantool-driver:parser'); -var decoder = new msgpackDecoder({codec}); +exports.processResponse = function(headers, data){ + var schemaId = headers[KeysCode.schema_version] + var reqId = headers[KeysCode.sync] + var code = headers[KeysCode.code] -exports._processResponse = function(buffer, offset){ - decoder.buffer = buffer; - decoder.offset = offset || 0; - var headers = decoder.fetch(); - var schemaId = headers[tarantoolConstants.KeysCode.schema_version] - var reqId = headers[tarantoolConstants.KeysCode.sync] - var code = headers[tarantoolConstants.KeysCode.code] - var data = decoder.fetch() - - if (this.schemaId) - { - if (this.schemaId != schemaId) - { + if (this.schemaId) { + if (this.schemaId != schemaId) { this.schemaId = schemaId; this.namespace = {}; } - } - else - { + } else { this.schemaId = schemaId; } + var task = this.sentCommands.get(reqId); this.sentCommands.delete(reqId); - var dfd = task[1]; - var timeoutId = task[3]; + var dfd = task && task[1]; + var timeoutId = task && task[3]; if (timeoutId) clearTimeout(timeoutId); if (code === 0) { dfd[0](this._returnBool(task, data)); - } - else { - var tarantoolErrorObject = data[tarantoolConstants.KeysCode.iproto_error] && data[tarantoolConstants.KeysCode.iproto_error][0x00][0] - var errorDecription = (tarantoolErrorObject && tarantoolErrorObject[0x03]) || data[tarantoolConstants.KeysCode.iproto_error_24] + } else { + var tarantoolErrorObject = data[KeysCode.iproto_error] && data[KeysCode.iproto_error][0x00][0] + var errCode = tarantoolErrorObject && tarantoolErrorObject[0x05] + var errDecription = (tarantoolErrorObject && tarantoolErrorObject[0x03]) || data[KeysCode.iproto_error_24] - if ([ - 358 /* code of 'read-only' */, - "Can't modify data on a read-only instance - box.cfg.read_only is true", - 3859 /* code of 'bootstrap not finished' */ - ].includes((tarantoolErrorObject && tarantoolErrorObject[0x02]) || errorDecription)) { - switch (this.options.nonWritableHostPolicy) { - case 'changeAndRetry': - var attemptsCount = task[2].attempt - if (attemptsCount) { - task[2].attempt++ - } else { - task[2].attempt = 1 - } + switch (errCode) { + case 7: /* ER_READONLY */ + case 116: /* ER_LOADING */ + switch (this.options.nonWritableHostPolicy) { + case "changeAndRetry": + var attemptsCount = task[2].attempt; + if (attemptsCount) { + task[2].attempt++; + } else { + task[2].attempt = 1; + } - if (this.options.maxRetriesPerRequest <= attemptsCount) { - return dfd[1](new TarantoolError(errorDecription)); - } + if (this.options.maxRetriesPerRequest <= attemptsCount) { + return dfd[1](new TarantoolError(errDecription)); + } - this.offlineQueue.push([[task[0], task[1], task[2]], task[3]]); - return changeHost.call(this, errorDecription); - case 'changeHost': - changeHost.call(this, errorDecription); - } + this.offlineQueue.push([[task[0], task[1], task[2]], task[3]]); + return changeHost.call(this, errDecription); + case "changeHost": + changeHost.call(this, errDecription); + default: + dfd[1](new TarantoolError(errDecription)); + } + break; + default: + debug('Processed response with an unsuccessful response code: %s', errDecription); + this.errorHandler( + new TarantoolError( + "Processed response with an unsuccessful response code", + { + cause: errDecription, + } + ) + ); + return; } - - dfd[1](new TarantoolError(errorDecription)); } }; function _returnBool(task, data){ var cmd = task[0]; switch (cmd){ - case tarantoolConstants.RequestCode.rqAuth: - case tarantoolConstants.RequestCode.rqPing: + case RequestCode.rqAuth: + case RequestCode.rqPing: return true; - case tarantoolConstants.RequestCode.rqExecute: - if (data[tarantoolConstants.KeysCode.metadata]) { + case RequestCode.rqExecute: + if (data[KeysCode.metadata]) { var res = []; - var meta = data[tarantoolConstants.KeysCode.metadata]; - var rows = data[tarantoolConstants.KeysCode.data]; + var meta = data[KeysCode.metadata]; + var rows = data[KeysCode.data]; for (var i = 0; i < rows.length; i++) { var formattedRow = {}; for (var j = 0; j < meta.length; j++ ) { @@ -89,40 +89,36 @@ function _returnBool(task, data){ } return res; } else { - return 'Affected row count: ' + (data[tarantoolConstants.KeysCode.sql_info][0x0] || 0); + return 'Affected row count: ' + (data[KeysCode.sql_info][0x0] || 0); } - case tarantoolConstants.RequestCode.rqPrepare: - return { - id: data[tarantoolConstants.KeysCode.stmt_id], - metadata: data[tarantoolConstants.KeysCode.metadata], - bind_metadata: data[tarantoolConstants.KeysCode.bind_metadata] - }; - case tarantoolConstants.RequestCode.rqId: + case RequestCode.rqPrepare: + return data[KeysCode.stmt_id]; + case RequestCode.rqId: return { - version: data[tarantoolConstants.KeysCode.iproto_version], - features: data[tarantoolConstants.KeysCode.iproto_features], - auth_type: data[tarantoolConstants.KeysCode.iproto_auth_type] + version: data[KeysCode.iproto_version], + features: data[KeysCode.iproto_features], + auth_type: data[KeysCode.iproto_auth_type] }; - case tarantoolConstants.RequestCode.rqSelect: - case tarantoolConstants.RequestCode.rqInsert: - case tarantoolConstants.RequestCode.rqReplace: - case tarantoolConstants.RequestCode.rqUpdate: - case tarantoolConstants.RequestCode.rqDelete: + case RequestCode.rqSelect: + case RequestCode.rqInsert: + case RequestCode.rqReplace: + case RequestCode.rqUpdate: + case RequestCode.rqDelete: if (task[4]) { // should load the new space shema in case of change if (!this.namespace[task[2][0]]) { return []; } - return convertTupleToObject(data[tarantoolConstants.KeysCode.data], this.namespace[task[2][0]].tupleKeys) + return convertTupleToObject(data[KeysCode.data], this.namespace[task[2][0]].tupleKeys) }; default: - return data[tarantoolConstants.KeysCode.data]; + return data[KeysCode.data]; } } exports._returnBool = _returnBool -function changeHost (errorDecription) { - this.setState(512, errorDecription) // event 'changing_host' +function changeHost (errDecription) { + this.setState(512, errDecription) // event 'changing_host' this.useNextReserve() this.disconnect(true) } diff --git a/lib/pipeline.js b/lib/pipeline.js index 104136f..97fdc17 100644 --- a/lib/pipeline.js +++ b/lib/pipeline.js @@ -1,9 +1,6 @@ var { prototype: commandsPrototype } = require('./commands'); -var {RequestCode} = require('./const'); - -var commandsPrototypeKeys = Object.keys(commandsPrototype); function commandInterceptorFactory (method) { return function commandInterceptor () { @@ -12,17 +9,17 @@ function commandInterceptorFactory (method) { } } +var commandsPrototypeKeys = Object.keys(commandsPrototype); var setOfCommandInterceptors = {}; for (var commandKey of commandsPrototypeKeys) { setOfCommandInterceptors[commandKey] = commandInterceptorFactory(commandKey) } function sendCommandInterceptorFactory (self) { - return function sendCommandInterceptor (command, buffer, isPipelined) { + return function sendCommandInterceptor (requestCode, reqId, buffer, callbacks, commandArguments, opts) { // If the Select request was made to search for a system space/index name, then bypass this - if ((RequestCode.rqSelect == command[0]) && !isPipelined) { - self._parent.sendCommand(command, buffer) - return; + if (opts.autoPipeline === false) { + return self._parent.sendCommand(requestCode, reqId, buffer, callbacks, commandArguments, opts) } self._buffersConcatenedCounter++ @@ -32,8 +29,14 @@ function sendCommandInterceptorFactory (self) { self.pipelinedBuffer = buffer } + opts._pipelined = true; + // firstly add the command to 'sentCommands' Map + var send = self._parent.sendCommand(requestCode, reqId, buffer, callbacks, commandArguments, opts) + // then write the buffer + // in order to avoid the possible race conditions (?) self._trySendConcatenedBuffer() - self._parent.sendCommand(command, buffer, true) + + return send; } } @@ -47,15 +50,12 @@ function _trySendConcatenedBuffer () { function exec () { var _this = this; var promises = []; - for (var interceptedCommand of _this.pipelinedCommands) { + for (var interceptedCommand of this.pipelinedCommands) { var args = interceptedCommand[1] - if (interceptedCommand[0] == 'select') { - args = Object.values(args) // Otherwise 'arguments' doesn't get applied to function below correctly - args[6] = true // Marks 'select' request as pipelined, otherwise '_getMetadata' will break the pipelined queue - } + var method = interceptedCommand[0] promises.push( new Promise(function (resolve) { - _this._originalMethods[interceptedCommand[0]].apply(_this._originalMethods, args) + _this._originalMethods[method].apply(_this._originalMethods, args) .then(function (result) { resolve([null, result]) }) @@ -68,9 +68,35 @@ function exec () { ); } - return Promise.all(promises) + return new Promise(function (resolve) { + Promise.all(promises) + .then(function (result) { + result.findPipelineError = findPipelineError + result.findPipelineErrors = findPipelineErrors + resolve(result) + }) + }) } +function findPipelineError () { + var error = this.find(element => element[0]) + if (error !== undefined) { + return error[0] + } else { + return null; + } +}; + +function findPipelineErrors () { + var aoe = []; + for (var subarray of this) { + var errored_element = subarray[0] + if (errored_element) aoe.push(errored_element) + } + + return aoe; +}; + function flushPipelined () { this.pipelinedCommands = []; } @@ -94,8 +120,6 @@ function Pipeline (self) { Object.assign(this, setOfCommandInterceptors); } -Pipeline.prototype.pipeline = function () { +module.exports.pipeline = function () { return new Pipeline(this); -} - -module.exports = Pipeline; \ No newline at end of file +}; \ No newline at end of file diff --git a/lib/transaction.js b/lib/transaction.js index 8ae89eb..5facc85 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -1,23 +1,11 @@ class Transaction { constructor (self) { + this.streamId = self._getRequestId(); Object.assign( - this, - { - streamId: self._getRequestId() - }, + this, self ) }; - // async begin (transactionTimeout, isolationLevel, opts) { - - // return this._begin(transactionTimeout, isolationLevel, opts); - // }; - // async commit () { - // return this._commit(); - // }; - // async rollback () { - // return this._rollback(); - // }; }; function createTransaction () { diff --git a/lib/utils.js b/lib/utils.js index 571df4d..f23fbf9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,10 +7,27 @@ if (Buffer.allocUnsafe) { } else { createBufferMethod = new Buffer; } +var preallocatedBuf = createBufferMethod(10000); exports.createBuffer = function (size){ - return createBufferMethod(size); + // if running in autopipeling mode, this prevents the older buffers from being modified + if (this.autoPipeliningId) { + return createBufferMethod(size); + } + + // it is faster to reuse the existing buffer than allocating a new one + if (size > preallocatedBuf.length) { + preallocatedBuf = createBufferMethod(size); + return preallocatedBuf + } else { + return preallocatedBuf.subarray(0, size) + } }; +// flush preallocatedBuf every 3 sec +setTimeout(function () { + preallocatedBuf = createBufferMethod(10000); +}, 3000) + // cache the method of Buffer creation using an array of bytes var bufferFromMethod if (Buffer.from) { @@ -23,32 +40,7 @@ exports.bufferFrom = function (data, encoding) { } // 'buf.slice' is deprecated since Node v17.5.0 / v16.15.0, so we should proceed with 'buf.subarray' -var bufferSubarrayPolyName -if (Buffer.prototype.subarray) { - bufferSubarrayPolyName = 'subarray' -} else { - bufferSubarrayPolyName = 'slice' -}; -exports.bufferSubarrayPoly = bufferSubarrayPolyName; - -exports.findPipelineError = function (array) { - var error = array.find(element => element[0]) - if (error !== undefined) { - return error[0] - } else { - return null; - } -}; - -exports.findPipelineErrors = function (array) { - var array_of_errors = []; - for (var subarray of array) { - var errored_element = subarray[0] - if (errored_element) array_of_errors.push(errored_element) - } - - return array_of_errors -}; +exports.bufferSubarrayPoly = Buffer.prototype.subarray ? Buffer.prototype.subarray : Buffer.prototype.slice exports.parseURL = function(str, reserve){ var result = {}; @@ -78,15 +70,28 @@ exports.parseURL = function(str, reserve){ return result; }; -exports.TarantoolError = function(msg){ - Error.call(this); - this.message = msg; - this.name = 'TarantoolError'; - if (Error.captureStackTrace) { - Error.captureStackTrace(this); - } else { - this.stack = new Error().stack; - } +exports.TarantoolError = function (msg, opts) { + var err = new Error(msg, opts); + err.name = 'TarantoolError'; + return err; +} + +function withResolversPoly () { + var resolve = Promise.resolve; + var reject = Promise.reject; + var promise = new Promise(function (_resolve, _reject) { + resolve = _resolve; + reject = _reject; + }); + + return { + promise, + resolve, + reject + }; }; -exports.TarantoolError.prototype = Object.create(Error.prototype); -exports.TarantoolError.prototype.constructor = Error; \ No newline at end of file + +function withResolversOrig () { + return Promise.withResolvers() +} +exports.withResolvers = Promise.withResolvers ? withResolversOrig : withResolversPoly \ No newline at end of file diff --git a/package.json b/package.json index aeac591..337f61bd 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tarantool-driver", - "version": "4.0.0", + "version": "3.2.0", "description": "Tarantool driver for 1.7+", "main": "index.js", "scripts": { @@ -39,7 +39,6 @@ "debug": "^2.6.8", "int64-buffer": "^1.0.1", "lodash": "^4.17.4", - "msgpack-lite": "^0.1.20", "uuid": "^9.0.0", "msgpackr": "^1.11.2" }, From ff15bd5992f5f1b63131ecfb1995f8af8e63dd54 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:31:08 +0200 Subject: [PATCH 03/12] Fix Insert/Replace functions --- lib/commands.js | 32 ++++++++++++------------- lib/const.js | 50 ++++----------------------------------- lib/msgpack-extensions.js | 10 ++++---- lib/utils.js | 31 ++++-------------------- package.json | 2 +- 5 files changed, 29 insertions(+), 96 deletions(-) diff --git a/lib/commands.js b/lib/commands.js index 91e34aa..fd652a1 100755 --- a/lib/commands.js +++ b/lib/commands.js @@ -2,13 +2,14 @@ var { createHash } = require('crypto'); var tarantoolConstants = require('./const'); var { - bufferFrom, TarantoolError, - bufferSubarrayPoly, withResolvers } = require('./utils'); var { Packr } = require('msgpackr'); -var packr = new Packr(); +var packr = new Packr({ + variableMapSize: true, + useRecords: false +}); var bufferCache = new Map(); function getCachedMsgpackBuffer (value) { @@ -631,7 +632,6 @@ Commands.prototype.sql = function sql (query, bindParams = [], opts = {}){ var {promise, resolve, reject} = withResolvers(); var reqId = _this._getRequestId(); - // var bufParams = packr.encode(bindParams); var bufParams = packr.encode(bindParams); var isPreparedStatement = (typeof query === 'number') // in case of the statement ID being passed to 'query' param var bufQuery = isPreparedStatement ? getCachedMsgpackBuffer(query) : packr.encode(query); // cache only prepared queries, considering them frequently used @@ -697,7 +697,7 @@ Commands.prototype.prepare = function prepare (query, opts = {}){ ); }; -Commands.prototype.id = function id (version = 3, features = [1], auth_type = 'chap-sha1', opts){ +Commands.prototype.id = function id (version = 3, features = [1], auth_type = 'chap-sha1', opts = {}){ var reqId = this._getRequestId(); var headersMap = new Map(); @@ -725,16 +725,14 @@ Commands.prototype.id = function id (version = 3, features = [1], auth_type = 'c }; Commands.prototype.insert = function insert (spaceId, tuple, opts = {}){ - var reqId = this._getRequestId(); - return this._replaceInsert(tarantoolConstants.RequestCode.rqInsert, reqId, spaceId, tuple, opts); + return this._replaceInsert(tarantoolConstants.RequestCode.rqInsert, spaceId, tuple, opts); }; Commands.prototype.replace = function replace (spaceId, tuple, opts = {}){ - var reqId = this._getRequestId(); - return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, reqId, spaceId, tuple, opts); + return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, spaceId, tuple, opts); }; -Commands.prototype._replaceInsert = function _replaceInsert (cmd, reqId, spaceId, tuple, opts = {}){ +Commands.prototype._replaceInsert = function _replaceInsert (cmd, spaceId, tuple, opts = {}){ if (!Array.isArray(tuple)) return Promise.reject(new TarantoolError('need array')); var _this = this; @@ -743,9 +741,11 @@ Commands.prototype._replaceInsert = function _replaceInsert (cmd, reqId, spaceId { return this._getMetadata(spaceId, 0) .then(function(info){ - return _this._replaceInsert(cmd, reqId, info[0], tuple, opts); + return _this._replaceInsert(cmd, info[0], tuple, opts); }) } + + var reqId = this._getRequestId(); var bufTuple = packr.encode(tuple); var len = 21+bufTuple.length; var buffer = this.createBuffer(len+5); @@ -818,8 +818,8 @@ function shatransform(t){ } function xor(a, b) { - if (!Buffer.isBuffer(a)) a = bufferFrom(a); - if (!Buffer.isBuffer(b)) b = bufferFrom(b); + if (!Buffer.isBuffer(a)) a = Buffer.from(a); + if (!Buffer.isBuffer(b)) b = Buffer.from(b); var res = []; var i; if (a.length > b.length) { @@ -831,17 +831,17 @@ function xor(a, b) { res.push(a[i] ^ b[i]); } } - return bufferFrom(res); + return Buffer.from(res); } function scramble(password, salt){ - var encSalt = bufferFrom(salt, 'base64'); + var encSalt = Buffer.from(salt, 'base64'); var step1 = shatransform(password); var step2 = shatransform(step1); var step3 = shatransform( Buffer.concat( [ - bufferSubarrayPoly.apply(encSalt, [0, 20]), + encSalt.subarray(0, 20), step2 ] ) diff --git a/lib/const.js b/lib/const.js index 901b0f4..678c361 100755 --- a/lib/const.js +++ b/lib/const.js @@ -1,4 +1,3 @@ -var {bufferFrom} = require('./utils') // i steal it from go var RequestCode = { rqConnect: 0x00, //fake for connect @@ -54,23 +53,6 @@ var KeysCode = { iproto_auth_type: 0x5b }; -var KeysCodeBuffer = {}; -for (var key of Object.keys(KeysCode)) { - KeysCodeBuffer[key] = Buffer.from([KeysCode[key]]) -} - -var PredefinedBuffers = { - selectHeaders: Buffer.from([ - KeysCode.code, - RequestCode.rqSelect, - KeysCode.sync - ]), - selectBody: Buffer.from([ - 0x86, - KeysCode.space_id, - ]) -}; - // https://github.com/fl00r/go-tarantool-1.6/issues/2 var IteratorsType = { eq: 0, @@ -85,11 +67,6 @@ var IteratorsType = { bitsAllNotSet: 9 }; -var IteratorsTypeBuffer = {}; -for (var key of Object.keys(IteratorsType)) { - IteratorsTypeBuffer[key] = Buffer.from([IteratorsType[key]]) -} - var OkCode = 0; var NetErrCode = 0xfffffff1; // fake code to wrap network problems into response var TimeoutErrCode = 0xfffffff2; // fake code to wrap timeout error into repsonse @@ -111,31 +88,12 @@ var IndexSpace = { indexName: 2 }; -var BufferedIterators = {}; -for(t in IteratorsType) -{ - BufferedIterators[t] = bufferFrom([KeysCode.iterator, IteratorsType[t], KeysCode.key]); -} - -var BufferedKeys = {}; -for(k in KeysCode) -{ - BufferedKeys[k] = bufferFrom([KeysCode[k]]); -} - -var ExportPackage = { +module.exports = { RequestCode: RequestCode, KeysCode: KeysCode, IteratorsType: IteratorsType, OkCode: OkCode, - passEnter: bufferFrom('a9636861702d73686131', 'hex') /* from msgpack.encode('chap-sha1') */, + passEnter: Buffer.from('a9636861702d73686131', 'hex') /* from msgpack.encode('chap-sha1') */, Space: Space, - IndexSpace: IndexSpace, - BufferedIterators: BufferedIterators, - BufferedKeys: BufferedKeys, - KeysCodeBuffer: KeysCodeBuffer, - PredefinedBuffers: PredefinedBuffers, - IteratorsTypeBuffer: IteratorsTypeBuffer -}; - -module.exports = ExportPackage; \ No newline at end of file + IndexSpace: IndexSpace +}; \ No newline at end of file diff --git a/lib/msgpack-extensions.js b/lib/msgpack-extensions.js index 9cf84ab..22d2592 100644 --- a/lib/msgpack-extensions.js +++ b/lib/msgpack-extensions.js @@ -1,8 +1,6 @@ var { addExtension } = require('msgpackr'); var { - TarantoolError, - createBuffer, - bufferFrom + TarantoolError } = require('./utils'); var { parse: uuidParse, @@ -66,7 +64,6 @@ function isOdd (number) { return number % 2 !== 0; }; -var decimalBuffer = createBuffer(1); // reuse buffer addExtension({ Class: packAs.Decimal, type: 0x01, @@ -74,6 +71,7 @@ addExtension({ var strNum = instance.value.toString() var rawNum = strNum.replace('-', '') var rawNumSplitted1 = rawNum.split('.')[1] + var decimalBuffer = Buffer.allocUnsafe(1); decimalBuffer.writeInt8(rawNumSplitted1 && rawNum.split('.')[1].length || 0) var bufHexed = decimalBuffer.toString('hex') + rawNum.replace('.', '') @@ -83,7 +81,7 @@ addExtension({ bufHexed = bufHexed.slice(0, 2) + '0' + bufHexed.slice(2) } - return bufferFrom(bufHexed, 'hex') + return Buffer.from(bufHexed, 'hex') }, unpack(buffer) { var scale = buffer.readIntBE(0, 1) @@ -103,7 +101,6 @@ addExtension({ }); // Datetime extension -var datetimeBuffer = createBuffer(16); // reuse buffer addExtension({ Class: Date, type: 0x04, @@ -111,6 +108,7 @@ addExtension({ var seconds = instance.getTime() / 1000 | 0 var nanoseconds = instance.getMilliseconds() * 1000 + var datetimeBuffer = Buffer.allocUnsafe(16); datetimeBuffer.writeBigUInt64LE(BigInt(seconds)) datetimeBuffer.writeUInt32LE(nanoseconds, 8) datetimeBuffer.writeUInt32LE(0, 12) diff --git a/lib/utils.js b/lib/utils.js index f23fbf9..c81fe69 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,22 +1,13 @@ -// cache the method of Buffer allocation -var createBufferMethod -if (Buffer.allocUnsafe) { - createBufferMethod = Buffer.allocUnsafe; -} else if (Buffer.alloc) { - createBufferMethod = Buffer.alloc; -} else { - createBufferMethod = new Buffer; -} -var preallocatedBuf = createBufferMethod(10000); +var preallocatedBuf = Buffer.allocUnsafe(Buffer.poolSize); exports.createBuffer = function (size){ // if running in autopipeling mode, this prevents the older buffers from being modified if (this.autoPipeliningId) { - return createBufferMethod(size); + return Buffer.allocUnsafe(size); } // it is faster to reuse the existing buffer than allocating a new one if (size > preallocatedBuf.length) { - preallocatedBuf = createBufferMethod(size); + preallocatedBuf = Buffer.allocUnsafe(size); return preallocatedBuf } else { return preallocatedBuf.subarray(0, size) @@ -25,23 +16,9 @@ exports.createBuffer = function (size){ // flush preallocatedBuf every 3 sec setTimeout(function () { - preallocatedBuf = createBufferMethod(10000); + preallocatedBuf = Buffer.allocUnsafe(Buffer.poolSize); }, 3000) -// cache the method of Buffer creation using an array of bytes -var bufferFromMethod -if (Buffer.from) { - bufferFromMethod = Buffer.from; -} else { - bufferFromMethod = new Buffer; -} -exports.bufferFrom = function (data, encoding) { - return bufferFromMethod(data, encoding); -} - -// 'buf.slice' is deprecated since Node v17.5.0 / v16.15.0, so we should proceed with 'buf.subarray' -exports.bufferSubarrayPoly = Buffer.prototype.subarray ? Buffer.prototype.subarray : Buffer.prototype.slice - exports.parseURL = function(str, reserve){ var result = {}; if (str.startsWith('/')) { diff --git a/package.json b/package.json index 337f61bd..c07bcae 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tarantool-driver", - "version": "3.2.0", + "version": "3.2.1", "description": "Tarantool driver for 1.7+", "main": "index.js", "scripts": { From 592ec8cf9148e4aa95e319a61b50c10640d78d58 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:21:56 +0200 Subject: [PATCH 04/12] Add files via upload --- lib/parser.js | 7 +------ lib/utils.js | 1 + package.json | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index 4a32e1d..9a2e8f0 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,6 +1,5 @@ var { KeysCode, RequestCode } = require('./const'); var { TarantoolError } = require('./utils'); -var debug = require('debug')('tarantool-driver:parser'); exports.processResponse = function(headers, data){ var schemaId = headers[KeysCode.schema_version] @@ -55,13 +54,9 @@ exports.processResponse = function(headers, data){ } break; default: - debug('Processed response with an unsuccessful response code: %s', errDecription); this.errorHandler( new TarantoolError( - "Processed response with an unsuccessful response code", - { - cause: errDecription, - } + "Processed response with an unsuccessful response code: " + errDecription ) ); return; diff --git a/lib/utils.js b/lib/utils.js index c81fe69..017959c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,6 @@ var preallocatedBuf = Buffer.allocUnsafe(Buffer.poolSize); exports.createBuffer = function (size){ + return Buffer.allocUnsafe(size); // if running in autopipeling mode, this prevents the older buffers from being modified if (this.autoPipeliningId) { return Buffer.allocUnsafe(size); diff --git a/package.json b/package.json index c07bcae..824adf3 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tarantool-driver", - "version": "3.2.1", + "version": "3.2.2", "description": "Tarantool driver for 1.7+", "main": "index.js", "scripts": { From 19a20f7cb2335de520e0844dc273981999e7eea1 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Fri, 21 Mar 2025 20:00:37 +0100 Subject: [PATCH 05/12] Code improvements --- README.md | 3 +- benchmark/read.js | 43 +++++---- benchmark/write.js | 1 - lib/autopipelining.js | 18 ++-- lib/commands.js | 200 +++++++++++++++++++++--------------------- lib/connection.js | 195 ++++++++++++++++++++-------------------- lib/connector.js | 15 +--- lib/event-handler.js | 108 +++++++++++------------ lib/parser.js | 21 ++--- lib/pipeline.js | 100 +++++++++++---------- lib/utils.js | 26 +++--- package.json | 2 +- 12 files changed, 370 insertions(+), 362 deletions(-) diff --git a/README.md b/README.md index 734faac..f30d008 100755 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Connection related custom events: | [options.noDelay] | boolean | true | Disables the use of Nagle's algorithm (recommended). | | [options.lazyConnect] | boolean | false | By default, When a new `Tarantool` instance is created, it will connect to Tarantool server automatically. If you want to keep disconnected util a command is called, you can pass the `lazyConnect` option to the constructor. | | [options.nonWritableHostPolicy] | string | null | What to do when Tarantool server rejects write operation, e.g. because of `box.cfg.read_only` set to `true` or during snapshot fetching.
Possible values are:
- `null`: just rejects Promise with an error
- `changeHost`: disconnect from the current host and connect to the next from `reserveHosts`. Pending Promise will be rejected.
- `changeAndRetry`: same as `changeHost`, but after reconnecting tries to run the command again in order to fullfil the Promise | +| [options.enableAutoPipelining] | boolean | false | In auto pipelining mode, all commands issued during an event loop are enqueued in a pipeline automatically managed by tarantool-driver. At the end of the iteration, the pipeline is executed and thus all commands are sent to the server at the same time. | | [options.maxRetriesPerRequest] | number | 5 | Number of attempts to find the alive host if `nonWritableHostPolicy` is not null. | | [options.enableOfflineQueue] | boolean | true | By default, if there is no active connection to the Tarantool server, commands are added to a queue and are executed once the connection is "ready", meaning the connection to the Tarantool server has been established and auth passed (`connect` event is also executed at this moment). If this option is false, when execute the command when the connection isn't ready, an error will be returned. | | [options.reserveHosts] | array | [] | Array of [strings](https://tarantool.org/en/doc/reference/configuration/index.html?highlight=uri#uri) - reserve hosts. Client will try to connect to hosts from this array after loosing connection with current host and will do it cyclically. See example below.| @@ -366,7 +367,7 @@ It's ok you can do whatever you need. I add log options for some technical infor - Decreased memory consumption - New AutoPipelining mode: send a batch of commands periodically with no need to rewrite your existing code. Benchmark showed x3 performance for the Select requests: - 330k/sec without AutoPipelining - - 1100k/sec with AutoPipelining period set to ~10ms + - 1100k/sec with AutoPipelining feature enabled - [IPROTO_ID](https://www.tarantool.io/en/doc/latest/reference/internals/iproto/requests/#iproto-id) can be invoked as 'conn.id()' function. - [Streams](https://www.tarantool.io/en/doc/latest/platform/atomic/txn_mode_mvcc/#streams-and-interactive-transactions) support. diff --git a/benchmark/read.js b/benchmark/read.js index d88d56b..6f3c0fb 100755 --- a/benchmark/read.js +++ b/benchmark/read.js @@ -1,4 +1,3 @@ -/* global Promise */ 'use strict'; var Benchmark = require('benchmark'); @@ -6,7 +5,7 @@ var suite = new Benchmark.Suite(); var Driver = require('../lib/connection.js'); var noop = require('lodash/noop'); var promises; -var preparedSelectStmtId; +var preparedSelectStmtId = 0; var connectionArg = process.argv[process.argv.length - 1] @@ -15,20 +14,12 @@ var conn = new Driver(connectionArg, { tuplesToObjects: false }); -var connAutoPipelined = new Driver(connectionArg, { - lazyConnect: true, - autoPipeliningPeriod: 10 // just a 1 millisecond window! -}); - -Promise.all([ - conn.connect(), - connAutoPipelined.connect() -]) -// preload schema and create a prepared SQL statement +conn.connect() .then(function () { return Promise.all([ + // preload schemas conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, noop), - // connAutoPipelined.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, noop), + // create a prepared SQL statement conn.prepare('SELECT * FROM "counter" WHERE "primary" = ? LIMIT 1 OFFSET 0') .then(function (result) { preparedSelectStmtId = result; @@ -48,9 +39,29 @@ Promise.all([ }}); // show the performance improvement when using an autopipelining - suite.add('non-deferred select + autopipelining window of 1ms', {defer: false, fn: function(){ - connAutoPipelined.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, console.error); - }}); + suite.add("non-deferred select + autopipelining", { + defer: false, + onStart: () => { + // enable the autopipelining feature + conn.enableAutoPipelining = true; + }, + onComplete: () => { + // disable the autopipelining feature + conn.enableAutoPipelining = false; + }, + fn: function () { + conn.selectCb( + "counter", + 0, + 1, + 0, + "eq", + ["test"], + noop, + console.error + ); + }, + }); suite.add('non-deferred sql select', {defer: false, fn: function(){ conn.sql('SELECT * FROM "counter" WHERE "primary" = ? LIMIT 1 OFFSET 0', ['test']); diff --git a/benchmark/write.js b/benchmark/write.js index fc195fb..87b8d0c 100755 --- a/benchmark/write.js +++ b/benchmark/write.js @@ -1,4 +1,3 @@ -/* global Promise */ 'use strict'; var Benchmark = require('benchmark'); var suite = new Benchmark.Suite(); diff --git a/lib/autopipelining.js b/lib/autopipelining.js index 44476f9..de98d5a 100644 --- a/lib/autopipelining.js +++ b/lib/autopipelining.js @@ -1,16 +1,20 @@ function AutoPipeline() {} -AutoPipeline.prototype._processAutoPipeliningQueue = function (self) { - var concatenatedBuffer = Buffer.concat(self.autoPipelineQueue); - self.autoPipelineQueue = []; - self.autoPipeliningId = 0; - self.socket.write(concatenatedBuffer); +AutoPipeline.prototype._processAutoPipeliningQueue = function () { + var concatenatedBuffer = Buffer.concat(this.autoPipelineQueue); + this.autoPipelineQueue = []; + this.autoPipeliningId = 0; + this.socket.write(concatenatedBuffer); + // check if this feature is still enabled and continue processing if so + if (this.autoPipeliningEnabled) setImmediate(this._processAutoPipeliningQueue.bind(this)); }; AutoPipeline.prototype._addToAutoPipeliningQueue = function (buffer) { this.autoPipelineQueue.push(buffer); - if (!this.autoPipeliningId) { - this.autoPipeliningId = setTimeout(this._processAutoPipeliningQueue, this.options.autoPipeliningPeriod, this); + // check if auto pipelining is already started in order to don't run 'setImmediate' each time + if (!this.autoPipeliningStarted) { + setImmediate(this._processAutoPipeliningQueue.bind(this)) + this.autoPipeliningStarted = true; } }; diff --git a/lib/commands.js b/lib/commands.js index fd652a1..c9fff42 100755 --- a/lib/commands.js +++ b/lib/commands.js @@ -23,18 +23,67 @@ function getCachedMsgpackBuffer (value) { }; }; -function Commands() {} - var maxSmi = 1<<30 - -Commands.prototype._getRequestId = function _getRequestId (){ +exports._getRequestId = function _getRequestId (){ var _id = this._id if (_id[0] > maxSmi) _id[0] = 1; return _id[0]++; }; -Commands.prototype._getSpaceId = function _getSpaceId (name){ +exports._getMetadata = function _getMetadata (spaceName, indexName){ + var _this = this; + var spName = this.namespace[spaceName] // reduce overhead of lookup + if (spName) + { + spaceName = spName.id; + } + if (typeof(spName) != 'undefined' && typeof(spName.indexes[indexName])!='undefined') + { + indexName = spName.indexes[indexName]; + } + if (typeof(spaceName)=='string' && typeof(indexName)=='string') + { + return this._getSpaceId(spaceName) + .then(function(spaceId){ + return Promise.all([spaceId, _this._getIndexId(spaceId, indexName)]); + }); + } + var promises = []; + if (typeof(spaceName) == 'string') + promises.push(this._getSpaceId(spaceName)); + else + promises.push(spaceName); + if (typeof(indexName) == 'string') + promises.push(this._getIndexId(spaceName, indexName)); + else + promises.push(indexName); + return Promise.all(promises); +}; + +exports._getIndexId = function _getIndexId (spaceId, indexName){ + var _this = this; + return this.select(tarantoolConstants.Space.index, tarantoolConstants.IndexSpace.indexName, 1, 0, + 'eq', [spaceId, indexName], { + tuplesToObjects: false, + autoPipeline: false + }) + .then(function(value) { + if (value && value[0] && value[0].length>1) { + var indexId = value[0][1]; + var space = _this.namespace[spaceId]; + if (space) { + _this.namespace[space.name].indexes[indexName] = indexId; + _this.namespace[space.id].indexes[indexName] = indexId; + } + return indexId; + } + else + throw new TarantoolError('Cannot read a space name indexes or index is not defined'); + }); +}; + +exports._getSpaceId = function _getSpaceId (name){ var _this = this; return this.select(tarantoolConstants.Space.space, tarantoolConstants.IndexSpace.name, 1, 0, 'eq', [name], { @@ -65,34 +114,30 @@ Commands.prototype._getSpaceId = function _getSpaceId (name){ }); }; -Commands.prototype._getIndexId = function _getIndexId (spaceId, indexName){ - var _this = this; - return this.select(tarantoolConstants.Space.index, tarantoolConstants.IndexSpace.indexName, 1, 0, - 'eq', [spaceId, indexName], { - tuplesToObjects: false, - autoPipeline: false - }) - .then(function(value) { - if (value && value[0] && value[0].length>1) { - var indexId = value[0][1]; - var space = _this.namespace[spaceId]; - if (space) { - _this.namespace[space.name].indexes[indexName] = indexId; - _this.namespace[space.id].indexes[indexName] = indexId; - } - return indexId; - } - else - throw new TarantoolError('Cannot read a space name indexes or index is not defined'); - }); -}; +exports.writePacketHeaders = function writePacketHeaders (buffer, length) { + const reqId = this._getRequestId(); -Commands.prototype.select = function select (spaceId, indexId, limit, offset, iterator, key, opts = {}) { - if (!(key instanceof Array)) key = [key]; + buffer[0] = 0xce; + buffer.writeUInt32BE(length, 1); + buffer[5] = 0x83; + buffer[6] = tarantoolConstants.KeysCode.code; + buffer[7] = tarantoolConstants.RequestCode.rqSelect; + buffer[8] = tarantoolConstants.KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id + buffer[15] = 0xce; + buffer.writeUInt32BE(this.streamId || 0, 16); + + return reqId; +} + +exports.select = function select (spaceId, indexId, limit, offset = 0, iterator = 'eq', key, opts = {}) { + if (!Array.isArray(key)) key = [key]; var _this = this; - if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) + if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) spaceId = _this.namespace[spaceId].id; if (typeof(indexId)=='string' && _this.namespace[spaceId] && _this.namespace[spaceId].indexes[indexId]) indexId = _this.namespace[spaceId].indexes[indexId]; @@ -104,25 +149,13 @@ Commands.prototype.select = function select (spaceId, indexId, limit, offset, it }) } - var reqId = this._getRequestId(); - if (iterator == 'all') - key = []; - var bufKey = packr.encode(key); + if (iterator == 'all') key = []; - var bufferLength = 42+bufKey.length; - var buffer = this.createBuffer(bufferLength); + var bufKey = packr.encode(key); + const len = 37+bufKey.length; + var buffer = this.createBuffer(len+5); - buffer[0] = 0xce; - buffer.writeUInt32BE(37+bufKey.length, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqSelect; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); + const reqId = this.writePacketHeaders(buffer, len); buffer[20] = 0x86; buffer[21] = tarantoolConstants.KeysCode.space_id; buffer[22] = 0xcd; @@ -134,7 +167,7 @@ Commands.prototype.select = function select (spaceId, indexId, limit, offset, it buffer.writeUInt32BE(limit, 29); buffer[33] = tarantoolConstants.KeysCode.offset; buffer[34] = 0xce; - buffer.writeUInt32BE(offset || 0, 35); + buffer.writeUInt32BE(offset, 35); buffer[39] = tarantoolConstants.KeysCode.iterator; buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 40); buffer[41] = tarantoolConstants.KeysCode.key; @@ -150,37 +183,7 @@ Commands.prototype.select = function select (spaceId, indexId, limit, offset, it ); } -Commands.prototype._getMetadata = function _getMetadata (spaceName, indexName){ - var _this = this; - var spName = this.namespace[spaceName] // reduce overhead of lookup - if (spName) - { - spaceName = spName.id; - } - if (typeof(spName) != 'undefined' && typeof(spName.indexes[indexName])!='undefined') - { - indexName = spName.indexes[indexName]; - } - if (typeof(spaceName)=='string' && typeof(indexName)=='string') - { - return this._getSpaceId(spaceName) - .then(function(spaceId){ - return Promise.all([spaceId, _this._getIndexId(spaceId, indexName)]); - }); - } - var promises = []; - if (typeof(spaceName) == 'string') - promises.push(this._getSpaceId(spaceName)); - else - promises.push(spaceName); - if (typeof(indexName) == 'string') - promises.push(this._getIndexId(spaceName, indexName)); - else - promises.push(indexName); - return Promise.all(promises); -}; - -Commands.prototype.ping = function ping (opts = {}){ +exports.ping = function ping (opts = {}){ var reqId = this._getRequestId(); var len = 9; var buffer = this.createBuffer(len+5); @@ -204,7 +207,7 @@ Commands.prototype.ping = function ping (opts = {}){ ); }; -Commands.prototype.begin = function begin (transTimeoutSec = 60.01 /* prevent JS from converting 60.0 to 60 */, isolationLevel = 0, opts = {}) { +exports.begin = function begin (transTimeoutSec = 60.01 /* prevent JS from converting 60.0 to 60 */, isolationLevel = 0, opts = {}) { if (!this.streamId) { reject( new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') @@ -246,7 +249,7 @@ Commands.prototype.begin = function begin (transTimeoutSec = 60.01 /* prevent JS ); }; -Commands.prototype.commit = function commit (opts = {}) { +exports.commit = function commit (opts = {}) { if (!this.streamId) { reject( new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') @@ -279,7 +282,7 @@ Commands.prototype.commit = function commit (opts = {}) { ); }; -Commands.prototype.rollback = function rollback (opts = {}) { +exports.rollback = function rollback (opts = {}) { if (!this.streamId) { reject( new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') @@ -312,9 +315,8 @@ Commands.prototype.rollback = function rollback (opts = {}) { ); }; -Commands.prototype.selectCb = function selectCb (spaceId, indexId, limit, offset, iterator, key, success, error, opts = {}){ - if (!(key instanceof Array)) - key = [key]; +exports.selectCb = function selectCb (spaceId, indexId, limit, offset, iterator, key, success, error, opts = {}){ + if (!Array.isArray(key)) key = [key]; var _this = this; @@ -377,7 +379,7 @@ Commands.prototype.selectCb = function selectCb (spaceId, indexId, limit, offset ); }; -Commands.prototype.delete = function _delete (spaceId, indexId, key, opts = {}){ +exports.delete = function _delete (spaceId, indexId, key, opts = {}){ var _this = this; if (Number.isInteger(key)) key = [key]; if (!Array.isArray(key)) return Promise.reject(new TarantoolError('need array')); @@ -423,7 +425,7 @@ Commands.prototype.delete = function _delete (spaceId, indexId, key, opts = {}){ ); }; -Commands.prototype.update = function update (spaceId, indexId, key, ops, opts = {}){ +exports.update = function update (spaceId, indexId, key, ops, opts = {}){ if (Number.isInteger(key)) key = [key]; if (!(Array.isArray(ops) && Array.isArray(key))) return Promise.reject(new TarantoolError('need array')); @@ -474,7 +476,7 @@ Commands.prototype.update = function update (spaceId, indexId, key, ops, opts = ); }; -Commands.prototype.upsert = function upsert (spaceId, ops, tuple, opts = {}){ +exports.upsert = function upsert (spaceId, ops, tuple, opts = {}){ var _this = this; if (!Array.isArray(ops)) return Promise.reject(new TarantoolError('need ops array')); if (typeof(spaceId)=='string') { @@ -520,7 +522,7 @@ Commands.prototype.upsert = function upsert (spaceId, ops, tuple, opts = {}){ ); }; -Commands.prototype.eval = function eval (expression, opts = {}){ +exports.eval = function eval (expression, opts = {}){ var tuple = Array.prototype.slice.call(arguments, 1); var reqId = this._getRequestId(); var bufExp = getCachedMsgpackBuffer(expression); @@ -555,7 +557,7 @@ Commands.prototype.eval = function eval (expression, opts = {}){ ); }; -Commands.prototype.call = function call (functionName, opts = {}){ +exports.call = function call (functionName, opts = {}){ var tuple = arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : []; var reqId = this._getRequestId(); var bufName = getCachedMsgpackBuffer(functionName); @@ -590,7 +592,7 @@ Commands.prototype.call = function call (functionName, opts = {}){ ); }; -Commands.prototype.sql = function sql (query, bindParams = [], opts = {}){ +exports.sql = function sql (query, bindParams = [], opts = {}){ var reqId = this._getRequestId(); var bufParams = packr.encode(bindParams); var isPreparedStatement = (typeof query === 'number') // in case of the statement ID being passed to 'query' param @@ -626,7 +628,7 @@ Commands.prototype.sql = function sql (query, bindParams = [], opts = {}){ ); }; -Commands.prototype.sql = function sql (query, bindParams = [], opts = {}){ +exports.sql = function sql (query, bindParams = [], opts = {}){ var _this = this; var _arguments = arguments; var {promise, resolve, reject} = withResolvers(); @@ -668,7 +670,7 @@ Commands.prototype.sql = function sql (query, bindParams = [], opts = {}){ return promise; }; -Commands.prototype.prepare = function prepare (query, opts = {}){ +exports.prepare = function prepare (query, opts = {}){ var reqId = this._getRequestId(); var bufQuery = packr.encode(query); @@ -697,7 +699,7 @@ Commands.prototype.prepare = function prepare (query, opts = {}){ ); }; -Commands.prototype.id = function id (version = 3, features = [1], auth_type = 'chap-sha1', opts = {}){ +exports.id = function id (version = 3, features = [1], auth_type = 'chap-sha1', opts = {}){ var reqId = this._getRequestId(); var headersMap = new Map(); @@ -724,15 +726,15 @@ Commands.prototype.id = function id (version = 3, features = [1], auth_type = 'c ); }; -Commands.prototype.insert = function insert (spaceId, tuple, opts = {}){ +exports.insert = function insert (spaceId, tuple, opts = {}){ return this._replaceInsert(tarantoolConstants.RequestCode.rqInsert, spaceId, tuple, opts); }; -Commands.prototype.replace = function replace (spaceId, tuple, opts = {}){ +exports.replace = function replace (spaceId, tuple, opts = {}){ return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, spaceId, tuple, opts); }; -Commands.prototype._replaceInsert = function _replaceInsert (cmd, spaceId, tuple, opts = {}){ +exports._replaceInsert = function _replaceInsert (cmd, spaceId, tuple, opts = {}){ if (!Array.isArray(tuple)) return Promise.reject(new TarantoolError('need array')); var _this = this; @@ -778,7 +780,7 @@ Commands.prototype._replaceInsert = function _replaceInsert (cmd, spaceId, tuple ); }; -Commands.prototype._auth = function _auth (username, password){ +exports._auth = function _auth (username, password){ var _this = this; return new Promise(function (resolve, reject) { var reqId = _this._getRequestId(); @@ -847,6 +849,4 @@ function scramble(password, salt){ ) ); return xor(step1, step3); -} - -module.exports = Commands; \ No newline at end of file +} \ No newline at end of file diff --git a/lib/connection.js b/lib/connection.js index 0cdcfe0..700cd7b 100755 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,30 +1,28 @@ -/* global Promise */ -var EventEmitter = require('events').EventEmitter; -var util = require('util'); -var debug = require('debug')('tarantool-driver:main'); -var { +const {EventEmitter} = require('events'); +const debug = require('debug')('tarantool-driver:main'); +const { pick, defaults, assign, noop } = require('lodash'); -var { +const { parseURL, TarantoolError, withResolvers, createBuffer } = require('./utils'); -var msgpackExt = require('./msgpack-extensions'); -var tarantoolConstants = require('./const'); -var Commands = require('./commands'); -var Pipeline = require('./pipeline'); -var Transaction = require('./transaction'); -var Parser = require('./parser'); -var Connector = require('./connector'); -var AutoPipeline = require('./autopipelining'); -var eventHandler = require('./event-handler'); +const msgpackExt = require('./msgpack-extensions'); +const tarantoolConstants = require('./const'); +const Commands = require('./commands'); +const Pipeline = require('./pipeline'); +const Transaction = require('./transaction'); +const Parser = require('./parser'); +const Connector = require('./connector'); +const AutoPipeline = require('./autopipelining'); +const eventHandler = require('./event-handler'); -var revertStates = { +const revertStates = { 0: 'connecting', 1: 'connected', 2: 'awaiting', @@ -38,7 +36,21 @@ var revertStates = { 512: 'changing_host' }; -var defaultOptions = { +const states = { + CONNECTING: 0, + CONNECTED: 1, + AWAITING: 2, + INITED: 4, + PREHELLO: 8, + AWAITING_LENGTH: 16, + END: 32, + RECONNECTING: 64, + AUTH: 128, + CONNECT: 256, + CHANGING_HOST: 512 +}; + +const defaultOptions = { host: 'localhost', port: 3301, path: null, @@ -50,7 +62,6 @@ var defaultOptions = { noDelay: true, requestTimeout: 0, keepAlive: true, - autoPipeliningPeriod: 0, // disable auto pipelining tuplesToObjects: false, nonWritableHostPolicy: null, /* What to do when Tarantool server rejects write operation, e.g. because of box.cfg.read_only set to 'true' or during fetching snapshot. @@ -67,72 +78,71 @@ var defaultOptions = { lazyConnect: false }; -function TarantoolConnection (){ - if (!(this instanceof TarantoolConnection)) { - return new TarantoolConnection(arguments[0], arguments[1], arguments[2]); +class TarantoolConnection extends EventEmitter { + constructor () { + super(); + this.reserve = []; + this.connecting = false; + this.socket = null; + this.parseOptions = parseOptions; + this.options = {}; + this.parseOptions(arguments[0], arguments[1], arguments[2]); + this.schemaId = 0; + this.states = states; + this.schemas = {}; + this.dataState = this.states.PREHELLO; + this.commandsQueue = []; + this.offlineQueue = []; + this.namespace = {}; + this.retryAttempts = 0; + this._id = [ 1 ]; + this.sentCommands = new Map(); + this.autoPipelineQueue = []; + this.autoPipeliningEnabled = false; + this.autoPipeliningStarted = false; + this.enableAutoPipelining = this.options.enableAutoPipelining; + this.resetOfflineQueue = resetOfflineQueue; + this.useNextReserve = useNextReserve; + this.sendCommand = sendCommand; + this.setState = setState; + this.connect = connect; + this.flushQueue = flushQueue; + this.silentEmit = silentEmit; + this.destroy = this.disconnect = disconnect; + this.errorHandler = eventHandler.errorHandler; + this.createBuffer = createBuffer; + Object.assign(this, Connector); + Object.assign(this, Commands); + Object.assign(this, AutoPipeline.prototype); + Object.assign(this, Parser); + Object.assign(this, Transaction.prototype); + Object.assign(this, Pipeline); + Object.assign(this, msgpackExt); + + if (this.options.lazyConnect) { + this.setState(this.states.INITED); + } else { + this.connect().catch(noop); + } } - EventEmitter.call(this); - this.reserve = []; - this.parseOptions = parseOptions; - this.parseOptions(arguments[0], arguments[1], arguments[2]); - this.connector = new Connector(this.options); - this.schemaId = 0; - this.states = { - CONNECTING: 0, - CONNECTED: 1, - AWAITING: 2, - INITED: 4, - PREHELLO: 8, - AWAITING_LENGTH: 16, - END: 32, - RECONNECTING: 64, - AUTH: 128, - CONNECT: 256, - CHANGING_HOST: 512 - }; - this.schemas = {}; - this.dataState = this.states.PREHELLO; - this.commandsQueue = new Array(); - this.offlineQueue = new Array(); - this.namespace = {}; - this.retryAttempts = 0; - this._id = [ 1 ]; - this.sentCommands = new Map(); - this.autoPipelineQueue = new Array(); - this.autoPipeliningId = 0; - this.resetOfflineQueue = resetOfflineQueue; - this.IteratorsType = tarantoolConstants.IteratorsType; - this.useNextReserve = useNextReserve; - this.sendCommand = sendCommand; - this.setState = setState; - this.connect = connect; - this.flushQueue = flushQueue; - this.silentEmit = silentEmit; - this.destroy = this.disconnect = disconnect; - this.errorHandler = eventHandler.errorHandler - this.createBuffer = createBuffer - Object.assign(this, Commands.prototype); - Object.assign(this, AutoPipeline.prototype); - Object.assign(this, Parser); - Object.assign(this, Transaction.prototype); - Object.assign(this, Pipeline); - Object.assign(this, msgpackExt); - if (this.options.lazyConnect) { - this.setState(this.states.INITED); - } else { - this.connect().catch(noop); + set enableAutoPipelining (value = false) { + if (typeof value != 'boolean') throw new Error(`Should pass a boolean to 'enableAutoPipelining'`); + + this.autoPipeliningEnabled = value; + this.autoPipeliningStarted = false; } -} -util.inherits(TarantoolConnection, EventEmitter); + get enableAutoPipelining () { + return this.autoPipeliningEnabled; + } +} function resetOfflineQueue () { - this.offlineQueue = new Array(); + this.offlineQueue = []; }; function parseOptions (){ - this.options = {}; var i; for (i = 0; i < arguments.length; ++i) { var arg = arguments[i]; @@ -197,16 +207,6 @@ function useNextReserve (){ return reserveOptions; }; -function compareBooleans (a, b) { - switch (a) { - case true: - case false: - return a; - default: - return b - } -} - function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, opts = {}){ // create promise for async methods var promise; @@ -231,7 +231,7 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o opts ]); } else { - var tuplesToObjects = compareBooleans(opts.tuplesToObjects, this.options.tuplesToObjects); + const tuplesToObjects = opts.tuplesToObjects ?? this.options.tuplesToObjects; // create an array which will be stored till the response is received var setValue = [ @@ -252,11 +252,16 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o }, requestTimeout); this.sentCommands.set(reqId, setValue); - // in pipelined mode data is written via its own function - var shouldAutoPipeline = (this.options.autoPipeliningPeriod > 0) && (opts.autoPipeline !== false) && (opts._pipelined !== true) + const shouldAutoPipeline = + opts._pipelined === true + ? false + : opts.autoPipeline ?? this.autoPipeliningEnabled; + // check if should be autopipelined if (shouldAutoPipeline) { this._addToAutoPipeliningQueue(buffer); + // in manually pipelined mode data is written via its own function } else if (!opts._pipelined) { + debug(`sending request №${reqId} to server`) this.socket.write(buffer); }; } @@ -306,7 +311,7 @@ function connect (){ } this.setState(this.states.CONNECTING); var _this = this; - this.connector.connect(function(err, socket){ + this._connect(function(err, socket){ if(err){ _this.flushQueue(err); _this.silentEmit('error', err); @@ -315,10 +320,10 @@ function connect (){ return; } _this.socket = socket; - socket.once('connect', eventHandler.connectHandler(_this)); - socket.once('error', _this.errorHandler.bind(_this)) - socket.once('close', eventHandler.closeHandler(_this)); - socket.on('data', eventHandler.dataHandler(_this)); + socket.once('connect', eventHandler.connectHandler.bind(_this)); + socket.once('error', eventHandler.errorHandler.bind(_this)) + socket.once('close', eventHandler.closeHandler.bind(_this)); + socket.on('data', eventHandler.dataHandler.call(_this)); if (_this.options.timeout) { socket.setTimeout(_this.options.timeout, function () { @@ -400,8 +405,8 @@ function disconnect (reconnect){ if (this.state === this.states.INITED) { eventHandler.closeHandler(this)(); } else { - this.connector.disconnect(); + this._disconnect(); } }; -module.exports = TarantoolConnection; +module.exports = TarantoolConnection; \ No newline at end of file diff --git a/lib/connector.js b/lib/connector.js index 33a8acd..3a0d3d1 100755 --- a/lib/connector.js +++ b/lib/connector.js @@ -1,24 +1,19 @@ -/* global Promise */ var net = require('net'); var tls = require('tls'); var { TarantoolError } = require('./utils'); -function Connector(options) { - this.options = options; -} - -Connector.prototype.disconnect = function () { +exports._disconnect = function () { this.connecting = false; if (this.socket) { this.socket.end(); } }; -Connector.prototype.connect = function (callback) { +exports._connect = function (callback) { this.connecting = true; var _this = this; - process.nextTick(function () { + setImmediate(function () { if (!_this.connecting) { callback(new TarantoolError('Connection is closed.')); return; @@ -38,6 +33,4 @@ Connector.prototype.connect = function (callback) { } callback(null, _this.socket); }); -}; - -module.exports = Connector; \ No newline at end of file +}; \ No newline at end of file diff --git a/lib/event-handler.js b/lib/event-handler.js index 3591e9b..771879d 100755 --- a/lib/event-handler.js +++ b/lib/event-handler.js @@ -2,33 +2,31 @@ var debug = require('debug')('tarantool-driver:handler'); var {TarantoolError} = require('./utils'); var {UnpackrStream} = require('msgpackr'); -exports.connectHandler = function (self) { - return function () { - self.retryAttempts = 0; - switch(self.state){ - case self.states.CONNECTING: - self.dataState = self.states.PREHELLO; +exports.connectHandler = function () { + this.retryAttempts = 0; + switch(this.state){ + case this.states.CONNECTING: + this.dataState = this.states.PREHELLO; break; - case self.states.CONNECTED: - if(self.options.password){ - self.setState(self.states.AUTH); - self._auth(self.options.username, self.options.password) + case this.states.CONNECTED: + if(this.options.password){ + this.setState(this.states.AUTH); + this._auth(this.options.username, this.options.password) .then(function(){ - self.setState(self.states.CONNECT, {host: self.options.host, port: self.options.port}); - debug('authenticated [%s]', self.options.username); - sendOfflineQueue.call(self); + this.setState(this.states.CONNECT, {host: this.options.host, port: this.options.port}); + debug('authenticated [%s]', this.options.username); + sendOfflineQueue.call(this); }, function(err){ - self.flushQueue(err); - self.errorHandler(err); - self.disconnect(true); + this.flushQueue(err); + this.errorHandler(err); + this.disconnect(true); }); } else { - self.setState(self.states.CONNECT, {host: self.options.host, port: self.options.port}); - sendOfflineQueue.call(self); + this.setState(this.states.CONNECT, {host: this.options.host, port: this.options.port}); + sendOfflineQueue.call(this); } break; } - }; }; function sendOfflineQueue(){ @@ -43,18 +41,20 @@ function sendOfflineQueue(){ } } +const unpackrOpts = { + mapsAsObjects: true, + int64AsType: "auto", +}; + function createUnpackrStream () { - var self = this; + var _this = this; var decodedHeaders = {}; var decodingStep = 0; - var unpackrStream = new UnpackrStream({ - mapsAsObjects: true, - int64AsType: "auto", - }); + var unpackrStream = new UnpackrStream(unpackrOpts); unpackrStream.on("error", function (error) { - self.errorHandler( + _this.errorHandler( new TarantoolError("Msgpack unpacker errored", { cause: error, }) @@ -74,11 +74,11 @@ function createUnpackrStream () { decodingStep = 1; break; case 1: - self.processResponse(decodedHeaders, data); + _this.processResponse(decodedHeaders, data); decodingStep = 2; break; default: - self.errorHandler( + _this.errorHandler( new TarantoolError( "Unknown step detected while decoding response data, maybe network loss occured with some bytes?" ) @@ -86,7 +86,7 @@ function createUnpackrStream () { } break; default: - self.errorHandler( + _this.errorHandler( new TarantoolError( "Type of decoded value does not satisfy requirements: " + type ) @@ -97,17 +97,17 @@ function createUnpackrStream () { return unpackrStream; } -exports.dataHandler = function (self) { - var unpackrStream = createUnpackrStream.call(self); - return function (data) { - switch (self.dataState) { - case self.states.PREHELLO: - self.salt = data.toString("utf8").split('\n')[1]; - self.dataState = self.states.CONNECTED; - self.setState(self.states.CONNECTED); - exports.connectHandler(self)(); +exports.dataHandler = function () { + var unpackrStream = createUnpackrStream.call(this); + return (data) => { + switch (this.dataState) { + case this.states.PREHELLO: + this.salt = data.toString("utf8").split('\n')[1]; + this.dataState = this.states.CONNECTED; + this.setState(this.states.CONNECTED); + exports.connectHandler.call(this); break; - case self.states.CONNECTED: + case this.states.CONNECTED: unpackrStream.write(data) break; } @@ -120,42 +120,42 @@ function errorHandler (error){ }; exports.errorHandler = errorHandler; -exports.closeHandler = function (self) { +exports.closeHandler = function () { function close () { - self.setState(self.states.END); - self.flushQueue(new TarantoolError('Connection is closed.')); + this.setState(this.states.END); + this.flushQueue(new TarantoolError('Connection is closed.')); } return function(){ - process.nextTick(self.emit.bind(self, 'close')); - if (self.manuallyClosing) { - self.manuallyClosing = false; + process.nextTick(this.emit.bind(this, 'close')); + if (this.manuallyClosing) { + this.manuallyClosing = false; debug('skip reconnecting since the connection is manually closed.'); return close(); } - if (typeof self.options.retryStrategy !== 'function') { + if (typeof this.options.retryStrategy !== 'function') { debug('skip reconnecting because `retryStrategy` is not a function'); return close(); } - var retryDelay = self.options.retryStrategy(++self.retryAttempts); + var retryDelay = this.options.retryStrategy(++this.retryAttempts); if (typeof retryDelay !== 'number') { debug('skip reconnecting because `retryStrategy` doesn\'t return a number'); return close(); } - self.setState(self.states.RECONNECTING, retryDelay); - if (self.options.reserveHosts) { - if (self.retryAttempts-1 == self.options.beforeReserve){ - self.useNextReserve(); - self.connect().catch(function(){}); + this.setState(this.states.RECONNECTING, retryDelay); + if (this.options.reserveHosts) { + if (this.retryAttempts-1 == this.options.beforeReserve){ + this.useNextReserve(); + this.connect().catch(function(){}); return; } } debug('reconnect in %sms', retryDelay); - self.reconnectTimeout = setTimeout(function () { - self.reconnectTimeout = null; - self.connect().catch(function(){}); + this.reconnectTimeout = setTimeout(function () { + this.reconnectTimeout = null; + this.connect().catch(function(){}); }, retryDelay); }; }; \ No newline at end of file diff --git a/lib/parser.js b/lib/parser.js index 9a2e8f0..a1dcc08 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,10 +1,12 @@ var { KeysCode, RequestCode } = require('./const'); var { TarantoolError } = require('./utils'); +var debug = require('debug')('tarantool-driver:parser'); exports.processResponse = function(headers, data){ var schemaId = headers[KeysCode.schema_version] var reqId = headers[KeysCode.sync] var code = headers[KeysCode.code] + debug(`processing response for request №${reqId}; code: ${code}, data: `, data, ', headers: ', headers) if (this.schemaId) { if (this.schemaId != schemaId) { @@ -54,17 +56,18 @@ exports.processResponse = function(headers, data){ } break; default: + if (reqId) return dfd[1](new TarantoolError(errDecription)); + this.errorHandler( new TarantoolError( "Processed response with an unsuccessful response code: " + errDecription ) ); - return; } } }; -function _returnBool(task, data){ +exports._returnBool = async function _returnBool (task, data){ var cmd = task[0]; switch (cmd){ case RequestCode.rqAuth: @@ -103,6 +106,7 @@ function _returnBool(task, data){ // should load the new space shema in case of change if (!this.namespace[task[2][0]]) { return []; + } return convertTupleToObject(data[KeysCode.data], this.namespace[task[2][0]].tupleKeys) }; @@ -110,7 +114,6 @@ function _returnBool(task, data){ return data[KeysCode.data]; } } -exports._returnBool = _returnBool function changeHost (errDecription) { this.setState(512, errDecription) // event 'changing_host' @@ -119,15 +122,9 @@ function changeHost (errDecription) { } function convertTupleToObject (rows, tupleKeys) { - var arr = new Array(rows.length).fill([]); - var num = 0; - for (var row of rows) { - var obj = createObject(tupleKeys, row) - arr[num] = obj - num++; - } - - return arr; + return rows.map(function (row) { + return createObject(tupleKeys, row); + }) } function createObject (keys, values) { diff --git a/lib/pipeline.js b/lib/pipeline.js index 97fdc17..c520561 100644 --- a/lib/pipeline.js +++ b/lib/pipeline.js @@ -1,6 +1,4 @@ -var { - prototype: commandsPrototype -} = require('./commands'); +var commandsPrototype = require('./commands'); function commandInterceptorFactory (method) { return function commandInterceptor () { @@ -15,36 +13,23 @@ for (var commandKey of commandsPrototypeKeys) { setOfCommandInterceptors[commandKey] = commandInterceptorFactory(commandKey) } -function sendCommandInterceptorFactory (self) { - return function sendCommandInterceptor (requestCode, reqId, buffer, callbacks, commandArguments, opts) { - // If the Select request was made to search for a system space/index name, then bypass this - if (opts.autoPipeline === false) { - return self._parent.sendCommand(requestCode, reqId, buffer, callbacks, commandArguments, opts) - } - - self._buffersConcatenedCounter++ - if (self.pipelinedBuffer) { - self.pipelinedBuffer = Buffer.concat([self.pipelinedBuffer, buffer]) - } else { - self.pipelinedBuffer = buffer - } +function sendCommandInterceptor (requestCode, reqId, buffer, callbacks, commandArguments, opts) { + // If the Select request was made to search for a system space/index name, then bypass this + if (opts.autoPipeline === false) { + return this._parent.sendCommand(requestCode, reqId, buffer, callbacks, commandArguments, opts) + } - opts._pipelined = true; - // firstly add the command to 'sentCommands' Map - var send = self._parent.sendCommand(requestCode, reqId, buffer, callbacks, commandArguments, opts) - // then write the buffer - // in order to avoid the possible race conditions (?) - self._trySendConcatenedBuffer() + this._buffersConcatenedCounter++ + this.buffers.push(buffer) - return send; - } -} + opts._pipelined = true; + // firstly add the command to 'sentCommands' Map + var send = this._parent.sendCommand(requestCode, reqId, buffer, callbacks, commandArguments, opts) + // then write the buffer + // in order to avoid the possible race conditions (?) + this._trySendConcatenedBuffer() -function _trySendConcatenedBuffer () { - if (this._buffersConcatenedCounter == this.pipelinedCommands.length) { - this._parent.socket.write(this.pipelinedBuffer) - this.flushPipelined() - } + return send; } function exec () { @@ -71,13 +56,17 @@ function exec () { return new Promise(function (resolve) { Promise.all(promises) .then(function (result) { - result.findPipelineError = findPipelineError - result.findPipelineErrors = findPipelineErrors + makePipelineResponse(result) resolve(result) }) }) } +function makePipelineResponse (array) { + array.__proto__.findPipelineError = findPipelineError; + array.__proto__.findPipelineErrors = findPipelineErrors; +} + function findPipelineError () { var error = this.find(element => element[0]) if (error !== undefined) { @@ -97,27 +86,36 @@ function findPipelineErrors () { return aoe; }; -function flushPipelined () { - this.pipelinedCommands = []; -} +class Pipeline { + constructor (self) { + var _this = this; + this._parent = self; + this.buffers = []; + this.pipelinedCommands = []; + this._buffersConcatenedCounter = 0; + this._originalMethods = Object.assign({}, + commandsPrototype, + self, + { + _id: self._id, + sendCommand: sendCommandInterceptor.bind(_this) + } + ) + this.exec = exec; + Object.assign(this, setOfCommandInterceptors); + }; -function Pipeline (self) { - var _this = this; - this._parent = self; - this.pipelinedCommands = []; - this._buffersConcatenedCounter = 0; - this._originalMethods = Object.assign({}, - commandsPrototype, - self, - { - _id: self._id, - sendCommand: sendCommandInterceptorFactory(_this) + flushPipelined () { + this.pipelinedCommands = []; + this.buffers = []; + }; + + _trySendConcatenedBuffer () { + if (this._buffersConcatenedCounter == this.pipelinedCommands.length) { + this._parent.socket.write(Buffer.concat(this.buffers)) + this.flushPipelined() } - ) - this.exec = exec - this._trySendConcatenedBuffer = _trySendConcatenedBuffer - this.flushPipelined = flushPipelined - Object.assign(this, setOfCommandInterceptors); + }; } module.exports.pipeline = function () { diff --git a/lib/utils.js b/lib/utils.js index 017959c..373d8b2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,24 +1,24 @@ -var preallocatedBuf = Buffer.allocUnsafe(Buffer.poolSize); -exports.createBuffer = function (size){ +var preallocBuf = Buffer.allocUnsafe(Buffer.poolSize); +exports.createBuffer = function createBuffer (size){ return Buffer.allocUnsafe(size); - // if running in autopipeling mode, this prevents the older buffers from being modified - if (this.autoPipeliningId) { + // this prevents the old existing buffers from being modified + if (this.enableAutoPipelining || this.nonWritableHostPolicy) { return Buffer.allocUnsafe(size); } // it is faster to reuse the existing buffer than allocating a new one - if (size > preallocatedBuf.length) { - preallocatedBuf = Buffer.allocUnsafe(size); - return preallocatedBuf + if (size > preallocBuf.length) { + preallocBuf = Buffer.allocUnsafe(size); + return preallocBuf } else { - return preallocatedBuf.subarray(0, size) + return preallocBuf.subarray(0, size) } }; -// flush preallocatedBuf every 3 sec +// flush preallocBuf every 5 sec setTimeout(function () { - preallocatedBuf = Buffer.allocUnsafe(Buffer.poolSize); -}, 3000) + preallocBuf = Buffer.allocUnsafe(Buffer.poolSize); +}, 5000) exports.parseURL = function(str, reserve){ var result = {}; @@ -69,7 +69,7 @@ function withResolversPoly () { }; }; -function withResolversOrig () { +function withResolvers () { return Promise.withResolvers() } -exports.withResolvers = Promise.withResolvers ? withResolversOrig : withResolversPoly \ No newline at end of file +exports.withResolvers = Promise.withResolvers ? withResolvers : withResolversPoly \ No newline at end of file diff --git a/package.json b/package.json index 824adf3..ccdb198 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tarantool-driver", - "version": "3.2.2", + "version": "3.3.0", "description": "Tarantool driver for 1.7+", "main": "index.js", "scripts": { From 1d45a9220451961b63b6467dd8a61f887795cab7 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:59:31 +0200 Subject: [PATCH 06/12] Add files via upload --- benchmark/box.lua | 20 ++++++++++- benchmark/read.js | 12 +++++-- benchmark/write.js | 16 +++++++++ lib/autopipelining.js | 6 ++-- lib/commands.js | 21 ++++++++---- lib/connection.js | 74 +++++++++++++++------------------------- lib/const.js | 42 +++++++++++++++++++---- lib/event-handler.js | 37 ++++++++++---------- lib/parser.js | 14 ++++++-- lib/pipeline-response.js | 34 ++++++++++++++++++ lib/pipeline.js | 30 +++------------- lib/utils.js | 6 ++-- package.json | 3 +- 13 files changed, 196 insertions(+), 119 deletions(-) create mode 100644 lib/pipeline-response.js diff --git a/benchmark/box.lua b/benchmark/box.lua index 54d888f..1fcead2 100755 --- a/benchmark/box.lua +++ b/benchmark/box.lua @@ -18,7 +18,19 @@ end) c = box.space.counter if not c then - c = box.schema.space.create('counter') + c = box.schema.space.create('counter', {engine = 'memtx'}) + c:format({ + {name = 'primary', type = 'string'}, + {name = 'num', type = 'unsigned'}, + {name = 'text', type = 'string'} + }) + pr = c:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'string'}}) + c:insert({'test', 1337, 'Some text.'}) +end + +c = box.space.counter_vinyl +if not c then + c = box.schema.space.create('counter_vinyl', {engine = 'vinyl'}) c:format({ {name = 'primary', type = 'string'}, {name = 'num', type = 'unsigned'}, @@ -34,6 +46,12 @@ if not s then p = s:create_index('primary', {type = 'hash', parts = {1, 'unsigned'}}) end +s = box.space.bench_vinyl +if not s then + s = box.schema.space.create('bench_vinyl', {engine = 'vinyl'}) + p = s:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) +end + function clear() box.session.su('admin') box.space.bench:truncate{} diff --git a/benchmark/read.js b/benchmark/read.js index 6f3c0fb..4a4b6e0 100755 --- a/benchmark/read.js +++ b/benchmark/read.js @@ -11,7 +11,7 @@ var connectionArg = process.argv[process.argv.length - 1] var conn = new Driver(connectionArg, { lazyConnect: true, - tuplesToObjects: false + tupleToObject: false }); conn.connect() @@ -32,9 +32,9 @@ conn.connect() conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, console.error); }}); - suite.add('non-deferred select, tuplesToObjects', {defer: false, fn: function(){ + suite.add('non-deferred select, tupleToObject', {defer: false, fn: function(){ conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, console.error, { - tuplesToObjects: true + tupleToObject: true }); }}); @@ -63,6 +63,12 @@ conn.connect() }, }); + suite.add('non-deferred select from vinyl', {defer: false, fn: function(){ + conn.selectCb('counter_vinyl', 0, 1, 0, 'eq', ['test'], noop, console.error, { + tupleToObject: true + }); + }}); + suite.add('non-deferred sql select', {defer: false, fn: function(){ conn.sql('SELECT * FROM "counter" WHERE "primary" = ? LIMIT 1 OFFSET 0', ['test']); }}); diff --git a/benchmark/write.js b/benchmark/write.js index 87b8d0c..fe00801 100755 --- a/benchmark/write.js +++ b/benchmark/write.js @@ -16,6 +16,22 @@ conn.connect() defer.reject(e); }); }}); + + suite.add('non-deferred insert', {defer: false, fn: function(){ + conn.insert('bench', [c++, {user: 'username', data: 'Some data.'}]) + .catch(function(e){ + console.error(e); + }); + }}); + + suite.add('insert to vinyl', {defer: true, fn: function(defer){ + conn.insert('bench_vinyl', [c++, {user: 'username', data: 'Some data.'}]) + .then(function(){defer.resolve();}) + .catch(function(e){ + console.error(e, e.stack); + defer.reject(e); + }); + }}); suite.add('insert parallel 50', {defer: true, fn: function(defer){ try{ diff --git a/lib/autopipelining.js b/lib/autopipelining.js index de98d5a..4e410c4 100644 --- a/lib/autopipelining.js +++ b/lib/autopipelining.js @@ -3,15 +3,13 @@ function AutoPipeline() {} AutoPipeline.prototype._processAutoPipeliningQueue = function () { var concatenatedBuffer = Buffer.concat(this.autoPipelineQueue); this.autoPipelineQueue = []; - this.autoPipeliningId = 0; this.socket.write(concatenatedBuffer); - // check if this feature is still enabled and continue processing if so - if (this.autoPipeliningEnabled) setImmediate(this._processAutoPipeliningQueue.bind(this)); + this.autoPipeliningStarted = false; }; AutoPipeline.prototype._addToAutoPipeliningQueue = function (buffer) { this.autoPipelineQueue.push(buffer); - // check if auto pipelining is already started in order to don't run 'setImmediate' each time + // check if auto pipelining is already started in order to don't execute 'setImmediate' each time if (!this.autoPipeliningStarted) { setImmediate(this._processAutoPipeliningQueue.bind(this)) this.autoPipeliningStarted = true; diff --git a/lib/commands.js b/lib/commands.js index c9fff42..c829d3e 100755 --- a/lib/commands.js +++ b/lib/commands.js @@ -8,7 +8,8 @@ var { var { Packr } = require('msgpackr'); var packr = new Packr({ variableMapSize: true, - useRecords: false + useRecords: false, + encodeUndefinedAsNil: true }); var bufferCache = new Map(); @@ -26,11 +27,19 @@ function getCachedMsgpackBuffer (value) { var maxSmi = 1<<30 exports._getRequestId = function _getRequestId (){ var _id = this._id - if (_id[0] > maxSmi) - _id[0] = 1; + if (_id[0] > maxSmi) _id[0] = 1; return _id[0]++; }; +// exports._loadSchemas = function _loadSchemas () { +// return Promise.all([ +// this.select(281, 0, 2147483647, 0, 'all', []) +// .then( + +// ) +// ]) +// } + exports._getMetadata = function _getMetadata (spaceName, indexName){ var _this = this; var spName = this.namespace[spaceName] // reduce overhead of lookup @@ -65,7 +74,7 @@ exports._getIndexId = function _getIndexId (spaceId, indexName){ var _this = this; return this.select(tarantoolConstants.Space.index, tarantoolConstants.IndexSpace.indexName, 1, 0, 'eq', [spaceId, indexName], { - tuplesToObjects: false, + tupleToObject: false, autoPipeline: false }) .then(function(value) { @@ -87,7 +96,7 @@ exports._getSpaceId = function _getSpaceId (name){ var _this = this; return this.select(tarantoolConstants.Space.space, tarantoolConstants.IndexSpace.name, 1, 0, 'eq', [name], { - tuplesToObjects: false, + tupleToObject: false, autoPipeline: false }) .then(function(value){ @@ -121,7 +130,6 @@ exports.writePacketHeaders = function writePacketHeaders (buffer, length) { buffer.writeUInt32BE(length, 1); buffer[5] = 0x83; buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqSelect; buffer[8] = tarantoolConstants.KeysCode.sync; buffer[9] = 0xce; buffer.writeUInt32BE(reqId, 10); @@ -156,6 +164,7 @@ exports.select = function select (spaceId, indexId, limit, offset = 0, iterator var buffer = this.createBuffer(len+5); const reqId = this.writePacketHeaders(buffer, len); + buffer[7] = tarantoolConstants.RequestCode.rqSelect; buffer[20] = 0x86; buffer[21] = tarantoolConstants.KeysCode.space_id; buffer[22] = 0xcd; diff --git a/lib/connection.js b/lib/connection.js index 700cd7b..6ff807b 100755 --- a/lib/connection.js +++ b/lib/connection.js @@ -10,46 +10,22 @@ const { parseURL, TarantoolError, withResolvers, - createBuffer + createBuffer, } = require('./utils'); const msgpackExt = require('./msgpack-extensions'); -const tarantoolConstants = require('./const'); +const { + revertStates, + states +} = require('./const'); const Commands = require('./commands'); const Pipeline = require('./pipeline'); +const {findPipelineError, findPipelineErrors} = require('./pipeline-response'); const Transaction = require('./transaction'); const Parser = require('./parser'); const Connector = require('./connector'); const AutoPipeline = require('./autopipelining'); const eventHandler = require('./event-handler'); -const revertStates = { - 0: 'connecting', - 1: 'connected', - 2: 'awaiting', - 4: 'inited', - 8: 'prehello', - 16: 'awaiting_length', - 32: 'end', - 64: 'reconnecting', - 128: 'auth', - 256: 'connect', - 512: 'changing_host' -}; - -const states = { - CONNECTING: 0, - CONNECTED: 1, - AWAITING: 2, - INITED: 4, - PREHELLO: 8, - AWAITING_LENGTH: 16, - END: 32, - RECONNECTING: 64, - AUTH: 128, - CONNECT: 256, - CHANGING_HOST: 512 -}; - const defaultOptions = { host: 'localhost', port: 3301, @@ -62,7 +38,7 @@ const defaultOptions = { noDelay: true, requestTimeout: 0, keepAlive: true, - tuplesToObjects: false, + tupleToObject: false, nonWritableHostPolicy: null, /* What to do when Tarantool server rejects write operation, e.g. because of box.cfg.read_only set to 'true' or during fetching snapshot. Possible values are: @@ -79,6 +55,12 @@ const defaultOptions = { }; class TarantoolConnection extends EventEmitter { + static packUuid = msgpackExt.packUuid; + static packInteger = msgpackExt.packInteger; + static packDecimal = msgpackExt.packDecimal; + static findPipelineError = findPipelineError; + static findPipelineErrors = findPipelineErrors; + constructor () { super(); this.reserve = []; @@ -88,9 +70,7 @@ class TarantoolConnection extends EventEmitter { this.options = {}; this.parseOptions(arguments[0], arguments[1], arguments[2]); this.schemaId = 0; - this.states = states; - this.schemas = {}; - this.dataState = this.states.PREHELLO; + this.dataState = states.PREHELLO; this.commandsQueue = []; this.offlineQueue = []; this.namespace = {}; @@ -120,7 +100,7 @@ class TarantoolConnection extends EventEmitter { Object.assign(this, msgpackExt); if (this.options.lazyConnect) { - this.setState(this.states.INITED); + this.setState(states.INITED); } else { this.connect().catch(noop); } @@ -217,9 +197,9 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o }; switch (this.state){ - case this.states.INITED: + case states.INITED: this.connect().catch(noop); - case this.states.CONNECT: + case states.CONNECT: if(!this.socket || !this.socket.writable){ debug('queue -> %s(%s)', requestCode, reqId); this.offlineQueue.push([ @@ -231,18 +211,18 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o opts ]); } else { - const tuplesToObjects = opts.tuplesToObjects ?? this.options.tuplesToObjects; + const tupleToObject = opts.tupleToObject ?? this.options.tupleToObject; // create an array which will be stored till the response is received var setValue = [ requestCode, callbacks, - [], - [], - tuplesToObjects + null, + null, + tupleToObject ]; - if ((this.options.nonWritableHostPolicy === 'changeAndRetry') || tuplesToObjects) setValue[2] = Object.values(commandArguments); + if ((this.options.nonWritableHostPolicy === 'changeAndRetry') || tupleToObject) setValue[2] = Object.values(commandArguments); var requestTimeout = this.options.requestTimeout || opts.requestTimeout; if (requestTimeout) setValue[3] = setTimeout(function () { @@ -266,7 +246,7 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o }; } break; - case this.states.END: + case states.END: callbacks[1](new TarantoolError('Connection is closed.')); break; default: @@ -305,18 +285,18 @@ function setState (state, arg) { function connect (){ return new Promise(function (resolve, reject) { - if (this.state === this.states.CONNECTING || this.state === this.states.CONNECT || this.state === this.states.CONNECTED || this.state === this.states.AUTH) { + if (this.state === states.CONNECTING || this.state === states.CONNECT || this.state === states.CONNECTED || this.state === states.AUTH) { reject(new TarantoolError('Tarantool is already connecting/connected')); return; } - this.setState(this.states.CONNECTING); + this.setState(states.CONNECTING); var _this = this; this._connect(function(err, socket){ if(err){ _this.flushQueue(err); _this.silentEmit('error', err); reject(err); - _this.setState(_this.states.END); + _this.setState(states.END); return; } _this.socket = socket; @@ -402,7 +382,7 @@ function disconnect (reconnect){ clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } - if (this.state === this.states.INITED) { + if (this.state === states.INITED) { eventHandler.closeHandler(this)(); } else { this._disconnect(); diff --git a/lib/const.js b/lib/const.js index 678c361..a30d2d0 100755 --- a/lib/const.js +++ b/lib/const.js @@ -88,12 +88,42 @@ var IndexSpace = { indexName: 2 }; +const revertStates = { + 0: 'connecting', + 1: 'connected', + 2: 'awaiting', + 4: 'inited', + 8: 'prehello', + 16: 'awaiting_length', + 32: 'end', + 64: 'reconnecting', + 128: 'auth', + 256: 'connect', + 512: 'changing_host' +}; + +const states = { + CONNECTING: 0, + CONNECTED: 1, + AWAITING: 2, + INITED: 4, + PREHELLO: 8, + AWAITING_LENGTH: 16, + END: 32, + RECONNECTING: 64, + AUTH: 128, + CONNECT: 256, + CHANGING_HOST: 512 +}; + module.exports = { - RequestCode: RequestCode, - KeysCode: KeysCode, - IteratorsType: IteratorsType, - OkCode: OkCode, + states, + revertStates, + RequestCode, + KeysCode, + IteratorsType, + OkCode, passEnter: Buffer.from('a9636861702d73686131', 'hex') /* from msgpack.encode('chap-sha1') */, - Space: Space, - IndexSpace: IndexSpace + Space, + IndexSpace }; \ No newline at end of file diff --git a/lib/event-handler.js b/lib/event-handler.js index 771879d..d6b3646 100755 --- a/lib/event-handler.js +++ b/lib/event-handler.js @@ -1,28 +1,29 @@ var debug = require('debug')('tarantool-driver:handler'); -var {TarantoolError} = require('./utils'); -var {UnpackrStream} = require('msgpackr'); +var { TarantoolError } = require("./utils"); +var { UnpackrStream } = require("msgpackr"); +const { states } = require("./const"); exports.connectHandler = function () { this.retryAttempts = 0; switch(this.state){ - case this.states.CONNECTING: - this.dataState = this.states.PREHELLO; + case states.CONNECTING: + this.dataState = states.PREHELLO; break; - case this.states.CONNECTED: + case states.CONNECTED: if(this.options.password){ - this.setState(this.states.AUTH); + this.setState(states.AUTH); this._auth(this.options.username, this.options.password) - .then(function(){ - this.setState(this.states.CONNECT, {host: this.options.host, port: this.options.port}); + .then(() => { + this.setState(states.CONNECT, {host: this.options.host, port: this.options.port}); debug('authenticated [%s]', this.options.username); sendOfflineQueue.call(this); - }, function(err){ + }, (err) => { this.flushQueue(err); this.errorHandler(err); this.disconnect(true); }); } else { - this.setState(this.states.CONNECT, {host: this.options.host, port: this.options.port}); + this.setState(states.CONNECT, {host: this.options.host, port: this.options.port}); sendOfflineQueue.call(this); } break; @@ -98,16 +99,16 @@ function createUnpackrStream () { } exports.dataHandler = function () { - var unpackrStream = createUnpackrStream.call(this); + const unpackrStream = createUnpackrStream.call(this); return (data) => { switch (this.dataState) { - case this.states.PREHELLO: - this.salt = data.toString("utf8").split('\n')[1]; - this.dataState = this.states.CONNECTED; - this.setState(this.states.CONNECTED); + case states.PREHELLO: + this.salt = data.toString("utf8").split('\n')[1].replaceAll(' ', ''); + this.dataState = states.CONNECTED; + this.setState(states.CONNECTED); exports.connectHandler.call(this); break; - case this.states.CONNECTED: + case states.CONNECTED: unpackrStream.write(data) break; } @@ -122,7 +123,7 @@ exports.errorHandler = errorHandler; exports.closeHandler = function () { function close () { - this.setState(this.states.END); + this.setState(states.END); this.flushQueue(new TarantoolError('Connection is closed.')); } @@ -143,7 +144,7 @@ exports.closeHandler = function () { debug('skip reconnecting because `retryStrategy` doesn\'t return a number'); return close(); } - this.setState(this.states.RECONNECTING, retryDelay); + this.setState(states.RECONNECTING, retryDelay); if (this.options.reserveHosts) { if (this.retryAttempts-1 == this.options.beforeReserve){ this.useNextReserve(); diff --git a/lib/parser.js b/lib/parser.js index a1dcc08..684d8ae 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -24,6 +24,13 @@ exports.processResponse = function(headers, data){ if (timeoutId) clearTimeout(timeoutId); + if (!dfd) { + return this.errorHandler( + new TarantoolError( + `Request with id ${reqId} was not found, maybe a duplicate` + ) + ); + } if (code === 0) { dfd[0](this._returnBool(task, data)); } else { @@ -67,7 +74,7 @@ exports.processResponse = function(headers, data){ } }; -exports._returnBool = async function _returnBool (task, data){ +exports._returnBool = function _returnBool (task, data){ var cmd = task[0]; switch (cmd){ case RequestCode.rqAuth: @@ -105,8 +112,9 @@ exports._returnBool = async function _returnBool (task, data){ if (task[4]) { // should load the new space shema in case of change if (!this.namespace[task[2][0]]) { - return []; - + const err = new TarantoolError('Failed to convert tuple to object'); + err.tuples = data[KeysCode.data]; + throw err; } return convertTupleToObject(data[KeysCode.data], this.namespace[task[2][0]].tupleKeys) }; diff --git a/lib/pipeline-response.js b/lib/pipeline-response.js new file mode 100644 index 0000000..71b2b0b --- /dev/null +++ b/lib/pipeline-response.js @@ -0,0 +1,34 @@ +function findPipelineError (array = []) { + var error = (array || this).find(element => element[0]) + return error[0] ?? null; +}; + +function findPipelineErrors (array = []) { + var aoe = []; + for (var subarray of (array || this)) { + var errored_element = subarray[0] + if (errored_element) aoe.push(errored_element) + } + + return aoe; +}; + +class PipelineResponse extends Array { + findPipelineError = findPipelineError; + findPipelineErrors = findPipelineErrors; + static findPipelineError = findPipelineError; + static findPipelineErrors = findPipelineErrors; + + constructor (arr) { + super(...arr) + }; + + get pipelineError () { + return this.findPipelineError() + }; + + get pipelineErrors () { + return this.findPipelineErrors() + } +} +module.exports = PipelineResponse; \ No newline at end of file diff --git a/lib/pipeline.js b/lib/pipeline.js index c520561..0c62702 100644 --- a/lib/pipeline.js +++ b/lib/pipeline.js @@ -1,4 +1,5 @@ var commandsPrototype = require('./commands'); +var PipelineResponse = require('./pipeline-response'); function commandInterceptorFactory (method) { return function commandInterceptor () { @@ -56,36 +57,13 @@ function exec () { return new Promise(function (resolve) { Promise.all(promises) .then(function (result) { - makePipelineResponse(result) - resolve(result) + resolve( + new PipelineResponse(result) + ) }) }) } -function makePipelineResponse (array) { - array.__proto__.findPipelineError = findPipelineError; - array.__proto__.findPipelineErrors = findPipelineErrors; -} - -function findPipelineError () { - var error = this.find(element => element[0]) - if (error !== undefined) { - return error[0] - } else { - return null; - } -}; - -function findPipelineErrors () { - var aoe = []; - for (var subarray of this) { - var errored_element = subarray[0] - if (errored_element) aoe.push(errored_element) - } - - return aoe; -}; - class Pipeline { constructor (self) { var _this = this; diff --git a/lib/utils.js b/lib/utils.js index 373d8b2..e1c67d8 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,6 @@ var preallocBuf = Buffer.allocUnsafe(Buffer.poolSize); exports.createBuffer = function createBuffer (size){ - return Buffer.allocUnsafe(size); + // return Buffer.allocUnsafe(size); // this prevents the old existing buffers from being modified if (this.enableAutoPipelining || this.nonWritableHostPolicy) { return Buffer.allocUnsafe(size); @@ -9,9 +9,9 @@ exports.createBuffer = function createBuffer (size){ // it is faster to reuse the existing buffer than allocating a new one if (size > preallocBuf.length) { preallocBuf = Buffer.allocUnsafe(size); - return preallocBuf + return preallocBuf; } else { - return preallocBuf.subarray(0, size) + return preallocBuf.subarray(0, size); } }; diff --git a/package.json b/package.json index ccdb198..726400f 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tarantool-driver", - "version": "3.3.0", + "version": "3.4.0", "description": "Tarantool driver for 1.7+", "main": "index.js", "scripts": { @@ -47,7 +47,6 @@ "chai": "^4.1.0", "coveralls": "^2.13.3", "eslint": "^2.5.3", - "ioredis": "^3.1.2", "istanbul": "^0.4.5", "mocha": "^2.2.4", "nanotimer": "^0.3.14", From f911e4a7c2e1e32c8fe63d6f03c4fec9dd4a198d Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Wed, 23 Apr 2025 22:39:37 +0200 Subject: [PATCH 07/12] 4.0.0 --- README.md | 16 +++++++--- lib/commands.js | 36 ++++++++++++++-------- lib/connection.js | 73 +++++++++++++++++++++++--------------------- lib/connector.js | 2 -- lib/const.js | 2 +- lib/event-handler.js | 14 +++++++-- lib/parser.js | 27 ++++++++-------- lib/utils.js | 58 +++++++++++++++++++++++++++-------- package.json | 2 +- 9 files changed, 149 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index f30d008..ed6553f 100755 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Connection related custom events: | [options.keepAlive] | boolean | true | Enables keep-alive functionality (recommended). | | [options.noDelay] | boolean | true | Disables the use of Nagle's algorithm (recommended). | | [options.lazyConnect] | boolean | false | By default, When a new `Tarantool` instance is created, it will connect to Tarantool server automatically. If you want to keep disconnected util a command is called, you can pass the `lazyConnect` option to the constructor. | -| [options.nonWritableHostPolicy] | string | null | What to do when Tarantool server rejects write operation, e.g. because of `box.cfg.read_only` set to `true` or during snapshot fetching.
Possible values are:
- `null`: just rejects Promise with an error
- `changeHost`: disconnect from the current host and connect to the next from `reserveHosts`. Pending Promise will be rejected.
- `changeAndRetry`: same as `changeHost`, but after reconnecting tries to run the command again in order to fullfil the Promise | +| [options.nonWritableHostPolicy] | string | null | What to do when Tarantool server rejects write operation, e.g. because of `box.cfg.read_only` set to `true` or during snapshot fetching.
Possible values are:
- `0`: just rejects Promise with an error
- `1`: disconnect from the current host and connect to the next from `reserveHosts`. Pending Promise will be rejected.
- `2`: same as `1`, but after reconnecting tries to run the command again in order to fullfil the Promise | | [options.enableAutoPipelining] | boolean | false | In auto pipelining mode, all commands issued during an event loop are enqueued in a pipeline automatically managed by tarantool-driver. At the end of the iteration, the pipeline is executed and thus all commands are sent to the server at the same time. | | [options.maxRetriesPerRequest] | number | 5 | Number of attempts to find the alive host if `nonWritableHostPolicy` is not null. | | [options.enableOfflineQueue] | boolean | true | By default, if there is no active connection to the Tarantool server, commands are added to a queue and are executed once the connection is "ready", meaning the connection to the Tarantool server has been established and auth passed (`connect` event is also executed at this moment). If this option is false, when execute the command when the connection isn't ready, an error will be returned. | @@ -358,6 +358,14 @@ It's ok you can do whatever you need. I add log options for some technical infor ## Changelog +### 4.0.0 + +- `nonWritableHostPolicy` option changed it's type from `string` to `number` +- `nonWritableHostPolicy` moved to a development state, consider not using it in production +- Reworked offline queue +- Fixed bug with buffer reuse +- Fixed use of Transactions if connection is not inited yet + ### 3.2.0 - Now supports [prepared](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/#box-sql-box-prepare) SQL statements. @@ -365,9 +373,9 @@ It's ok you can do whatever you need. I add log options for some technical infor - Now using 'msgpackr' instead of 'msgpack-lite' - Buffer reuse - Decreased memory consumption -- New AutoPipelining mode: send a batch of commands periodically with no need to rewrite your existing code. Benchmark showed x3 performance for the Select requests: - - 330k/sec without AutoPipelining - - 1100k/sec with AutoPipelining feature enabled +- New AutoPipelining mode: optimise performance with no need to rewrite your existing code. Benchmark showed x4 performance for the `select` requests: + - 350k/sec without AutoPipelining + - 1400k/sec with AutoPipelining feature enabled - [IPROTO_ID](https://www.tarantool.io/en/doc/latest/reference/internals/iproto/requests/#iproto-id) can be invoked as 'conn.id()' function. - [Streams](https://www.tarantool.io/en/doc/latest/platform/atomic/txn_mode_mvcc/#streams-and-interactive-transactions) support. diff --git a/lib/commands.js b/lib/commands.js index c829d3e..4ffbba6 100755 --- a/lib/commands.js +++ b/lib/commands.js @@ -31,15 +31,6 @@ exports._getRequestId = function _getRequestId (){ return _id[0]++; }; -// exports._loadSchemas = function _loadSchemas () { -// return Promise.all([ -// this.select(281, 0, 2147483647, 0, 'all', []) -// .then( - -// ) -// ]) -// } - exports._getMetadata = function _getMetadata (spaceName, indexName){ var _this = this; var spName = this.namespace[spaceName] // reduce overhead of lookup @@ -218,8 +209,8 @@ exports.ping = function ping (opts = {}){ exports.begin = function begin (transTimeoutSec = 60.01 /* prevent JS from converting 60.0 to 60 */, isolationLevel = 0, opts = {}) { if (!this.streamId) { - reject( - new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') + return Promise.reject( + new TarantoolError('Cannot get streamId, maybe called method outside of transaction?') ); } @@ -260,7 +251,7 @@ exports.begin = function begin (transTimeoutSec = 60.01 /* prevent JS from conve exports.commit = function commit (opts = {}) { if (!this.streamId) { - reject( + return Promise.reject( new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') ); } @@ -293,7 +284,7 @@ exports.commit = function commit (opts = {}) { exports.rollback = function rollback (opts = {}) { if (!this.streamId) { - reject( + return Promise.reject( new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') ); } @@ -824,6 +815,25 @@ exports._auth = function _auth (username, password){ }); }; +exports.rqCommands = { + 0x01: exports.select, + 0x02: exports.insert, + 0x03: exports.replace, + 0x04: exports.update, + 0x05: exports.delete, + 0x06: exports.call, + 0x07: exports._auth, + 0x08: exports.eval, + 0x09: exports.upsert, + 0x10: exports.rollback, + 0x0b: exports.sql, + 0x0d: exports.prepare, + 0x0e: exports.begin, + 0x0f: exports.commit, + 0x40: exports.ping, + 0x49: exports.id, +}; + function shatransform(t){ return createHash('sha1').update(t).digest(); } diff --git a/lib/connection.js b/lib/connection.js index 6ff807b..e1e4fd1 100755 --- a/lib/connection.js +++ b/lib/connection.js @@ -36,15 +36,15 @@ const defaultOptions = { beforeReserve: 2, timeout: 0, noDelay: true, - requestTimeout: 0, + requestTimeout: 10000, keepAlive: true, tupleToObject: false, - nonWritableHostPolicy: null, /* What to do when Tarantool server rejects write operation, + nonWritableHostPolicy: 0, /* What to do when Tarantool server rejects write operation, e.g. because of box.cfg.read_only set to 'true' or during fetching snapshot. Possible values are: - - null: reject Promise - - 'changeHost': disconnect from the current host and connect to the next of 'reserveHosts'. Pending Promise will be rejected. - - 'changeAndRetry': same as 'changeHost', but after connecting tries to run the command again in order to fullfil the Promise + - 0: reject Promise + - 1: ('changeHost') disconnect from the current host and connect to the next of 'reserveHosts'. Pending Promise will be rejected. + - 2: ('changeAndRetry') same as 'changeHost', but after connecting tries to run the command again in order to fullfil the Promise */ maxRetriesPerRequest: 5, // If 'nonWritableHostPolicy' specified, Promise will be rejected only after exceeding this setting enableOfflineQueue: true, @@ -61,12 +61,24 @@ class TarantoolConnection extends EventEmitter { static findPipelineError = findPipelineError; static findPipelineErrors = findPipelineErrors; + connect = connect; + destroy = disconnect; + disconnect = disconnect; + parseOptions = parseOptions; + resetOfflineQueue = resetOfflineQueue; + useNextReserve = useNextReserve; + sendCommand = sendCommand; + setState = setState; + flushQueue = flushQueue; + silentEmit = silentEmit; + errorHandler = eventHandler.errorHandler; + createBuffer = createBuffer; + constructor () { super(); this.reserve = []; this.connecting = false; this.socket = null; - this.parseOptions = parseOptions; this.options = {}; this.parseOptions(arguments[0], arguments[1], arguments[2]); this.schemaId = 0; @@ -81,16 +93,7 @@ class TarantoolConnection extends EventEmitter { this.autoPipeliningEnabled = false; this.autoPipeliningStarted = false; this.enableAutoPipelining = this.options.enableAutoPipelining; - this.resetOfflineQueue = resetOfflineQueue; - this.useNextReserve = useNextReserve; - this.sendCommand = sendCommand; - this.setState = setState; - this.connect = connect; - this.flushQueue = flushQueue; - this.silentEmit = silentEmit; - this.destroy = this.disconnect = disconnect; - this.errorHandler = eventHandler.errorHandler; - this.createBuffer = createBuffer; + this._state = [null]; Object.assign(this, Connector); Object.assign(this, Commands); Object.assign(this, AutoPipeline.prototype); @@ -122,6 +125,7 @@ function resetOfflineQueue () { this.offlineQueue = []; }; +var warnedAboutUnstability = false; function parseOptions (){ var i; for (i = 0; i < arguments.length; ++i) { @@ -150,7 +154,11 @@ function parseOptions (){ } defaults(this.options, defaultOptions); var reserveHostsLength = this.options.reserveHosts && this.options.reserveHosts.length || 0 - if ((this.options.nonWritableHostPolicy != null) && (reserveHostsLength == 0)) { + if (this.options.nonWritableHostPolicy && !warnedAboutUnstability) { + warnedAboutUnstability = true; + process.emitWarning(new TarantoolError(`'nonWritableHostPolicy' is under development, use carefully`)) + } + if (this.options.nonWritableHostPolicy && !reserveHostsLength) { throw new TarantoolError('\'nonWritableHostPolicy\' option is specified, but there are no reserve hosts. Specify them in connection options via \'reserveHosts\'') } if (typeof this.options.port === 'string') { @@ -196,19 +204,17 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o callbacks = [resolve, reject]; }; - switch (this.state){ + switch (this._state[0]){ case states.INITED: this.connect().catch(noop); case states.CONNECT: if(!this.socket || !this.socket.writable){ - debug('queue -> %s(%s)', requestCode, reqId); + debug('queue (no writable socket) -> %s(%s)', requestCode, reqId); this.offlineQueue.push([ - requestCode, - reqId, - buffer, + requestCode, callbacks, - commandArguments, - opts + commandArguments, + this ]); } else { const tupleToObject = opts.tupleToObject ?? this.options.tupleToObject; @@ -222,7 +228,7 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o tupleToObject ]; - if ((this.options.nonWritableHostPolicy === 'changeAndRetry') || tupleToObject) setValue[2] = Object.values(commandArguments); + if ((this.options.nonWritableHostPolicy === 2) || tupleToObject) setValue[2] = Object.values(commandArguments); var requestTimeout = this.options.requestTimeout || opts.requestTimeout; if (requestTimeout) setValue[3] = setTimeout(function () { @@ -255,12 +261,10 @@ function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, o return callbacks[1](new TarantoolError('Connection not established yet!')); }; this.offlineQueue.push([ - requestCode, - reqId, - buffer, + requestCode, callbacks, - commandArguments, - opts + commandArguments, + this ]); } @@ -278,14 +282,15 @@ function setState (state, arg) { address = this.options.host + ':' + this.options.port; } } - debug('state[%s]: %s -> %s', address, revertStates[this.state] || '[empty]', revertStates[state]); - this.state = state; + debug('state[%s]: %s -> %s', address, revertStates[this._state[0]] || '[empty]', revertStates[state]); + this._state[0] = state; process.nextTick(this.emit.bind(this, revertStates[state], arg)); }; function connect (){ return new Promise(function (resolve, reject) { - if (this.state === states.CONNECTING || this.state === states.CONNECT || this.state === states.CONNECTED || this.state === states.AUTH) { + var state = this._state[0]; + if (state === states.CONNECTING || state === states.CONNECT || state === states.CONNECTED || state === states.AUTH) { reject(new TarantoolError('Tarantool is already connecting/connected')); return; } @@ -382,7 +387,7 @@ function disconnect (reconnect){ clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } - if (this.state === states.INITED) { + if (this._state[0] === states.INITED) { eventHandler.closeHandler(this)(); } else { this._disconnect(); diff --git a/lib/connector.js b/lib/connector.js index 3a0d3d1..1f05cbc 100755 --- a/lib/connector.js +++ b/lib/connector.js @@ -13,7 +13,6 @@ exports._connect = function (callback) { this.connecting = true; var _this = this; - setImmediate(function () { if (!_this.connecting) { callback(new TarantoolError('Connection is closed.')); return; @@ -32,5 +31,4 @@ exports._connect = function (callback) { return; } callback(null, _this.socket); - }); }; \ No newline at end of file diff --git a/lib/const.js b/lib/const.js index a30d2d0..32f9146 100755 --- a/lib/const.js +++ b/lib/const.js @@ -125,5 +125,5 @@ module.exports = { OkCode, passEnter: Buffer.from('a9636861702d73686131', 'hex') /* from msgpack.encode('chap-sha1') */, Space, - IndexSpace + IndexSpace, }; \ No newline at end of file diff --git a/lib/event-handler.js b/lib/event-handler.js index d6b3646..f13e9e8 100755 --- a/lib/event-handler.js +++ b/lib/event-handler.js @@ -5,7 +5,7 @@ const { states } = require("./const"); exports.connectHandler = function () { this.retryAttempts = 0; - switch(this.state){ + switch(this._state[0]){ case states.CONNECTING: this.dataState = states.PREHELLO; break; @@ -37,7 +37,17 @@ function sendOfflineQueue(){ this.resetOfflineQueue(); while (offlineQueue.length > 0) { var command = offlineQueue.shift(); - this.sendCommand.apply(this, command); + var rqCode = command[0]; + var cb = command[1]; + var args = command[2]; + var rqFunc = this.rqCommands[rqCode]; + if (!rqFunc) return cb[1]( + new Error(`Unknown request code [${rqCode}] to resend a request, maybe a bug?`) + ); + + rqFunc.apply(command[3], args) + .then(cb[0]) + .catch(cb[1]) } } } diff --git a/lib/parser.js b/lib/parser.js index 684d8ae..2eccfdd 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -42,21 +42,24 @@ exports.processResponse = function(headers, data){ case 7: /* ER_READONLY */ case 116: /* ER_LOADING */ switch (this.options.nonWritableHostPolicy) { - case "changeAndRetry": - var attemptsCount = task[2].attempt; - if (attemptsCount) { - task[2].attempt++; - } else { - task[2].attempt = 1; - } + case 2: + // should remake 'attemptsCount' + // var attemptsCount = task[2].attempt; + // if (attemptsCount) { + // task[2].attempt++; + // } else { + // task[2].attempt = 1; + // } - if (this.options.maxRetriesPerRequest <= attemptsCount) { - return dfd[1](new TarantoolError(errDecription)); - } + // if (this.options.maxRetriesPerRequest <= attemptsCount) { + // return dfd[1](new TarantoolError(errDecription)); + // } - this.offlineQueue.push([[task[0], task[1], task[2]], task[3]]); + this.offlineQueue.push([ + task[0], task[1], task[2] + ]); return changeHost.call(this, errDecription); - case "changeHost": + case 1: changeHost.call(this, errDecription); default: dfd[1](new TarantoolError(errDecription)); diff --git a/lib/utils.js b/lib/utils.js index e1c67d8..a384fe1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,24 +1,58 @@ +var { states } = require("./const"); + +// it is faster to reuse the existing buffer than allocating a new one var preallocBuf = Buffer.allocUnsafe(Buffer.poolSize); +var shouldSetImmediate = true; +var bufferSizeMultiplier = 2; + exports.createBuffer = function createBuffer (size){ - // return Buffer.allocUnsafe(size); - // this prevents the old existing buffers from being modified - if (this.enableAutoPipelining || this.nonWritableHostPolicy) { + // prevent the old existing buffers from being overwritten + if (this.enableAutoPipelining || this.nonWritableHostPolicy || !this.socket || !this.socket.writable || (this.state !== states.CONNECT)) { return Buffer.allocUnsafe(size); } - // it is faster to reuse the existing buffer than allocating a new one + // flush preallocated buffer at the end of loop to prevent OOM + if (shouldSetImmediate) { + shouldSetImmediate = false; + setImmediate(function () { + preallocBuf = Buffer.allocUnsafe(Buffer.poolSize); + shouldSetImmediate = true; + }); + } + + // create a bigger buffer if (size > preallocBuf.length) { - preallocBuf = Buffer.allocUnsafe(size); - return preallocBuf; - } else { - return preallocBuf.subarray(0, size); + preallocBuf = Buffer.allocUnsafe(size * bufferSizeMultiplier); + } + + return preallocBuf.subarray(0, size); +}; + +// draft; Performance is a bit worse in autopipelining mode +var offset = 0; +exports.createBuffer2 = function createBuffer (size){ + // flush preallocated buffer at the end of event loop to prevent OOM + if (shouldSetImmediate) { + shouldSetImmediate = false; + setImmediate(function () { + preallocateBuf(Buffer.poolSize) + shouldSetImmediate = true; + }); + } + + // check if should create a bigger buffer + var fullLen = size + offset; + if (fullLen > preallocBuf.length) { + preallocateBuf(fullLen) } + + return preallocBuf.subarray(offset, offset += size); }; -// flush preallocBuf every 5 sec -setTimeout(function () { - preallocBuf = Buffer.allocUnsafe(Buffer.poolSize); -}, 5000) +function preallocateBuf (size) { + preallocBuf = Buffer.allocUnsafe(size * bufferSizeMultiplier); + offset = 0; +} exports.parseURL = function(str, reserve){ var result = {}; diff --git a/package.json b/package.json index 726400f..a007db6 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tarantool-driver", - "version": "3.4.0", + "version": "4.0.0", "description": "Tarantool driver for 1.7+", "main": "index.js", "scripts": { From eb860038ce83c58cc185f27a5bc5d586f9fc0b10 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:05:32 +0100 Subject: [PATCH 08/12] 4.0.0 ### BREAKING - `nonWritableHostPolicy` is deprecated and no more supported due to the overhead, low-frequency usability and complexity. Finally, it may be better to handle the connection's 'close' errors by developer and reconnect. - For a syntax clearance and new features you should pass parameters of `eval` and `call` methods as an array, e.g.: ```javascript tarantool.eval('return ...', [a, b]) ``` instead of the previous: ```javascript tarantool.eval('return ...', a, b) ``` - Dropping support for old Node.JS versions: now this module is guaranteed to work on a `Maintenance`, `Active` and `Current` [versions](https://nodejs.org/en/about/previous-releases#release-schedule) in order to introduce new features and stability. - `selectCb()` method is deprecated. This (`.select()`) and all other commands now can be invoked in a callback style (but `then/catch`-style is also supported, just don't specify the `cb` parameter and the function will return a Promise): ```javascript tarantool.select( spaceId, indexId, limit, offset, iterator, key, opts, (error, success) => { // process the result as usual } ) ``` ### Bug fixes - Fixed bug of a buffer reuse approach - Fixed use of keepAlive and noDelay - Fixed use of Transactions if connection is not inited yet - Fixed memory leak of 'connect' / 'close' events (appeared during many reconnects) ### Improvements - Optimized offline queue - New option `tupleToObject`, which allows you to receive an array of objects instead of array of arrays: - Keys are similar to the Tarantool space's key names, and value is a corresponding value from the tuple - Note that there is an extra overhead of converting array to object - This option is usable only for the following request types: `select`, `update`, `delete`, `insert`, `replace` - Introduced 2 custom error classes, which are exported on the connector class. They are useful for a better development experience when you can handle errors more properly: - ReplyError: errors created by the Tarantool server in a similar [format](https://www.tarantool.io/ru/doc/latest/reference/internals/msgpack_extensions/#the-error-type) - TarantoolError: general errors thrown by the driver - New option `commandTimeout` to define the max execution time of the sent request. Can be configured on the class globally or per each command - `packDecimal()` function now also accepts BigInt - Improved performance of the built-in MsgPack extensions (used in a `packDecimal()`, `packUuid()`, etc) - Improved compression of the 'update' / 'upsert' operations by converting a string field names to their corresponding ID's - New iterators: OVERLAPS and NEIGHBOR - Extended tests (with help of a native `node:test` and `node:assert`) - Updated benchmarks, README, eslint config - New guides - Even better performance - New `.quit()` method - awaits for the sent commands to become fullfilled before the connection closes - Pipeline response is now an instance of `PipelineResponse` with 2 additional methods: `findPipelineError()` and `findPipelineErrors()` --- BENCHMARK.md | 19 + CHANGELOG.md | 130 ++++ PERFORMANCE.md | 15 + README.md | 840 +++++++++++++------- TODO.md | 8 + assets/box.lua | 76 ++ assets/docker.sh | 10 + assets/read-results-table.png | Bin 0 -> 400286 bytes benchmark/read.js | 385 +++++----- benchmark/write.js | 278 +++++-- eslint.config.js | 114 +++ lib/Command.js | 140 ++++ lib/Commander.js | 1119 +++++++++++++++++++++++++++ lib/MsgPack.js | 300 ++++++++ lib/OfflineQueue.js | 109 +++ lib/PreparedStatement.js | 12 + lib/StandaloneConnector.js | 66 ++ lib/Transaction.js | 49 ++ lib/connection.js | 821 ++++++++++++-------- lib/const.js | 256 ++++--- lib/errors.js | 25 + lib/event-handler.js | 256 +++---- lib/parser.js | 249 +++--- lib/pipeline/AutoPipeliner.js | 30 + lib/pipeline/Pipeline.js | 109 +++ lib/pipeline/PipelineResponse.js | 36 + lib/utils/FastTimer.js | 427 +++++++++++ lib/utils/SliderBuffer.js | 44 ++ lib/utils/debug.js | 2 + lib/utils/index.js | 189 +++++ package.json | 31 +- test/app.js | 1235 +++++++++++++----------------- test/autopipeline.js | 127 +++ test/commander.js | 48 ++ test/msgpack_extensions.js | 173 +++++ test/pipeline.js | 265 +++++++ test/transaction.js | 176 +++++ 37 files changed, 6172 insertions(+), 1997 deletions(-) create mode 100644 BENCHMARK.md create mode 100644 CHANGELOG.md create mode 100644 PERFORMANCE.md create mode 100644 TODO.md create mode 100644 assets/box.lua create mode 100644 assets/docker.sh create mode 100644 assets/read-results-table.png create mode 100644 eslint.config.js create mode 100644 lib/Command.js create mode 100644 lib/Commander.js create mode 100644 lib/MsgPack.js create mode 100644 lib/OfflineQueue.js create mode 100644 lib/PreparedStatement.js create mode 100644 lib/StandaloneConnector.js create mode 100644 lib/Transaction.js create mode 100644 lib/errors.js create mode 100644 lib/pipeline/AutoPipeliner.js create mode 100644 lib/pipeline/Pipeline.js create mode 100644 lib/pipeline/PipelineResponse.js create mode 100644 lib/utils/FastTimer.js create mode 100644 lib/utils/SliderBuffer.js create mode 100644 lib/utils/debug.js create mode 100644 lib/utils/index.js create mode 100644 test/autopipeline.js create mode 100644 test/commander.js create mode 100644 test/msgpack_extensions.js create mode 100644 test/pipeline.js create mode 100644 test/transaction.js diff --git a/BENCHMARK.md b/BENCHMARK.md new file mode 100644 index 0000000..09f3b5c --- /dev/null +++ b/BENCHMARK.md @@ -0,0 +1,19 @@ +# How to + +1. Start the Tarantool server on your machine with a [box.lua](assets/box.lua) config located in a `test` folder. +2. Execute the following: + ```Bash + npm run benchmark-write + ``` + to fill all of the spaces with data and check the insert performance. +3. Execute the following: + ```Bash + npm run benchmark-write + ``` + to test the read performance of previously added tuples + +# "Read" results +- Machine: Apple Macbook Air M2 2022 8GB RAM +- `node -v`: 24.3.0 + +![results table of "read" benchmark](assets/read-results-table.png) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d2124e4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,130 @@ +## 4.0.0 + +### BREAKING +- `nonWritableHostPolicy` is deprecated and no more supported due to the overhead, low-frequency usability and complexity. Finally, it may be better to handle the connection's 'close' errors by developer and reconnect. +- For a syntax clearance and new features you should pass parameters of `eval` and `call` methods as an array, e.g.: + ```javascript + tarantool.eval('return ...', [a, b]) + ``` + instead of the previous: + ```javascript + tarantool.eval('return ...', a, b) + ``` +- Dropping support for old Node.JS versions: + now this module is guaranteed to work on a `Maintenance`, `Active` and `Current` [versions](https://nodejs.org/en/about/previous-releases#release-schedule) in order to introduce new features and stability. +- `selectCb()` method is deprecated. This (`.select()`) and all other commands now can be invoked in a callback style (but `then/catch`-style is also supported, just don't specify the `cb` parameter and the function will return a Promise): + ```javascript + tarantool.select( + spaceId, + indexId, + limit, + offset, + iterator, + key, + opts, + (error, success) => { + // process the result as usual + } + ) + ``` + +### Bug fixes +- Fixed bug of a buffer reuse approach +- Fixed use of keepAlive and noDelay +- Fixed use of Transactions if connection is not inited yet +- Fixed memory leak of 'connect' / 'close' events (appeared during many reconnects) + +### Improvements +- Optimized offline queue +- New option `tupleToObject`, which allows you to receive an array of objects instead of array of arrays: + - Keys are similar to the Tarantool space's key names, and value is a corresponding value from the tuple + - Note that there is an extra overhead of converting array to object + - This option is usable only for the following request types: `select`, `update`, `delete`, `insert`, `replace` +- Introduced 2 custom error classes, which are exported on the connector class. They are useful for a better development experience when you can handle errors more properly: + - ReplyError: errors created by the Tarantool server in a similar [format](https://www.tarantool.io/ru/doc/latest/reference/internals/msgpack_extensions/#the-error-type) + - TarantoolError: general errors thrown by the driver +- New option `commandTimeout` to define the max execution time of the sent request. Can be configured on the class globally or per each command +- `packDecimal()` function now also accepts BigInt +- Improved performance of the built-in MsgPack extensions (used in a `packDecimal()`, `packUuid()`, etc) +- Improved compression of the 'update' / 'upsert' operations by converting a string field names to their corresponding ID's +- New iterators: OVERLAPS and NEIGHBOR +- Extended tests (with help of a native `node:test` and `node:assert`) +- Updated benchmarks, README, eslint config +- New guides +- Even better performance +- New `.quit()` method - awaits for the sent commands to become fullfilled before the connection closes +- Pipeline response is now an instance of `PipelineResponse` with 2 additional methods: `findPipelineError()` and `findPipelineErrors()` + +## 3.2.0 + +- Now supports [prepared](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/#box-sql-box-prepare) SQL statements. +- Huge rewrite of the codebase, which improved the performance: + - Now using 'msgpackr' instead of 'msgpack-lite' + - Buffer reuse + - Decreased memory consumption +- New AutoPipelining mode: optimise performance with no need to rewrite your existing code. Benchmark showed x4 performance for the `select` requests: + - 350k/sec without AutoPipelining + - 1400k/sec with AutoPipelining feature enabled +- [IPROTO_ID](https://www.tarantool.io/en/doc/latest/reference/internals/iproto/requests/#iproto-id) can be invoked as 'conn.id()' function. +- [Streams](https://www.tarantool.io/en/doc/latest/platform/atomic/txn_mode_mvcc/#streams-and-interactive-transactions) support. + +## 3.1.0 + +- Added 3 new msgpack extensions: UUID, Datetime, Decimal. +- Connection object now accepts all options of `net.createConnection()`, including Unix socket path. +- New `nonWritableHostPolicy` and related options, which improves a high availability capabilities without any 3rd parties. +- Ability to disable the offline queue. +- Fixed [bug with int32](https://github.com/tarantool/node-tarantool-driver/issues/48) numbers when it was encoded as floating. Use method `packInteger()` to solve this. +- `selectCb()` now also accepts `spaceId` and `indexId` as their String names, not only their IDs. +- Some performance improvements by caching internal values. +- TLS (SSL) support. +- New `pipeline()`+`exec()` methods kindly borrowed from the [ioredis](https://github.com/redis/ioredis?tab=readme-ov-file#pipelining), which lets you to queue some commands in memory and then send them simultaneously to the server in a single (or several, if request body is too big) network call(s). Thanks to the Tarantool, which [made this possible](https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/format/#packet-structure). +This way the performance is significantly improved by 500-1600% - you can check it yourself by running `npm run benchmark-read` or `npm run benchmark-write`. +Note that this feature doesn't replaces the Transaction model, which has some level of isolation. +- Changed `const` declaration to `var` in order to support old Node.JS versions. + +## 3.0.7 + +Fix in header decoding to support latest Tarantool versions. Update to tests to support latest Tarantool versions. + +## 3.0.6 + +Remove let for support old nodejs version + +## 3.0.5 + +Add support SQL + +## 3.0.4 + +Fix eval and call + +## 3.0.3 + +Increase request id limit to SMI Maximum + +## 3.0.2 + +Fix parser thx @tommiv + +## 3.0.0 + +New version with reconnect in alpha. + +## 1.0.0 + +Fix test for call changes and remove unuse upsert parameter (critical change API for upsert) + +## 0.4.1 + +Add clear schema cache on change schema id + +## 0.4.0 + +Change msgpack5 to msgpack-lite(thx to @arusakov). +Add msgpack as option for connection. +Bump msgpack5 for work at new version. + +## 0.3.0 +Add upsert operation. +Key is now can be just a number. \ No newline at end of file diff --git a/PERFORMANCE.md b/PERFORMANCE.md new file mode 100644 index 0000000..f00fb57 --- /dev/null +++ b/PERFORMANCE.md @@ -0,0 +1,15 @@ +# Performance optimization guide + +- Consider connecting to Tarantool server via an UNIX-socket path. Benchmark shows that it could double the performance (more throughput, less latency) than connection to a basic `host:port`. +- Increase `sliderBufferInitialSize` option if you are facing a high-frequency or big (for example: large `.insert()` tuple, long multi-part indexes for `.select()`, etc) requests. +- Set `enableAutoPipelining` option to `true`. This approach may significantly improve the performance by 2-4 times with a small trade-off in the form of a bit increased query execution time (just a several milliseconds usually). + - This works by collecting request's buffers and not sending them immediately to server. Instead of this, connector stores the pending buffers, waits till the next tick, concatenates all the data and sends it in a big single batch. + - You may also use `.pipeline()` and `.exec()` to manually control which data should be sent in a single batch and which not. In this case the requests latency will not be increased (because there will be no dispatching to the next tick) and your code will benefit from less calls to `socket.write()` + - This approach was gently borrowed from [ioredis - read more](https://github.com/redis/ioredis?tab=readme-ov-file#autopipelining). +- Don't set `tupleToObject` and `commandTimeout` options to `true` unless it is reasonable. + - However, `commandTimeout` has a low overhead due to the usage of `FastTimer` and is very usable in the fact that it can help you rejecting long-running requests, which may have been stale due to the network connection issue. So use this advice with caution. + +## "Show me the difference" +- Here is a table which illustrates the significance of some of the possible optimization methods: + +![results table of "read" benchmark](assets/read-results-table.png) \ No newline at end of file diff --git a/README.md b/README.md index ed6553f..9683524 100755 --- a/README.md +++ b/README.md @@ -1,446 +1,714 @@ -# Node.js driver for tarantool 1.7+ +# Node.js driver for Tarantool 1.7+ ⚡ [![Build Status](https://travis-ci.org/tarantool/node-tarantool-driver.svg)](https://travis-ci.org/tarantool/node-tarantool-driver) -Node tarantool driver for 1.7+ support Node.js v.4+. +High-performance Node.js driver for Tarantool 1.7+ with support for Node.js v20+. -Based on [go-tarantool](https://github.com/tarantool/go-tarantool) and implements [Tarantool’s binary protocol](http://tarantool.org/doc/dev_guide/box-protocol.html), for more information you can read them or basic documentation at [Tarantool manual](http://tarantool.org/doc/). +Based on [go-tarantool](https://github.com/tarantool/go-tarantool) and implements [Tarantool's binary protocol](http://tarantool.org/doc/dev_guide/box-protocol.html). Code architecture and performance features in version 4 borrowed from [ioredis](https://github.com/luin/ioredis). -Code architecture and some features in version 3 borrowed from the [ioTarantool](https://github.com/luin/ioTarantool). +Uses [msgpackr](https://github.com/kriszyp/msgpackr) as the high-performance MsgPack encoder/decoder by default. -[msgpack-lite](https://github.com/kawanet/msgpack-lite) package used as MsgPack encoder/decoder. +⚠️ **Note**: Connection failures result in connection destruction. Subscribe to `TarantoolConnection.socket.on('close')` for closure notifications or handle rejected promise errors. - +## Table of Contents - -## Table of contents - -* [Installation](#installation) -* [Configuration](#configuration) -* [Usage example](#usage-example) -* [Msgpack implementation](#msgpack-implementation) -* [API reference](#api-reference) -* [Debugging](#debugging) -* [Contributions](#contributions) -* [Changelog](#changelog) +- 📦 [Installation](#installation) +- ⚙️ [Configuration](#configuration) +- 📝 [Usage Example](#usage-example) +- 📚 [API Reference](#api-reference) + - [Connection Methods](#connection-methods) + - [Data Query Methods](#data-query-methods) + - [Transaction Methods](#transaction-methods) + - [Server Methods](#server-methods) + - [Pipelining Methods](#pipelining-methods) + - [Utility Methods](#utility-methods) +- 🔍 [Debugging](#debugging) +- 📖 [Related Documentation](#related-documentation) + - [Performance Guide](./PERFORMANCE.md) - Optimization tips for maximum throughput 🚀 + - [Benchmarks](./BENCHMARK.md) - Performance comparisons and results 📊 + - [Changelog](./CHANGELOG.md) - Release notes and version history 📋 +- 🤝 [Contributions](#contributions) ## Installation -```Bash +```bash npm install --save tarantool-driver ``` + ## Configuration -new Tarantool([port], [host], [options]) ⇐ [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) +### Constructor + +```javascript +new Tarantool([port], [host], [options]) +``` Creates a Tarantool instance, extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter). -Connection related custom events: -* "reconnecting" - emitted when the client try to reconnect, first argument is retry delay in ms. -* "connect" - emitted when the client connected and auth passed (if username and password provided), first argument is an object with host and port of the Taranool server. -* "change_host" - emitted when `nonWritableHostPolicy` option is set and write error occurs, first argument is the text of error which provoked the host to be changed. +**Connection Events:** +- `reconnecting` - Emitted when the client attempts to reconnect; first argument is retry delay in milliseconds +- `connect` - Emitted when successfully connected and authenticated (if credentials provided); first argument is an object with `host` and `port` of the Tarantool server + +### Options -| Param | Type | Default | Description | +| Option | Type | Default | Description | | --- | --- | --- | --- | -| [port] | number \| string \| Object | 3301 | Port of the Tarantool server, or a URI string (see the examples in [tarantool configuration doc](https://tarantool.org/en/doc/reference/configuration/index.html#uri)), or the `options` object(see the third argument). | -| [host] | string \| Object | "localhost" | Host of the Tarantool server, when the first argument is a URL string, this argument is an object represents the options. | -| [path] | string \| Object | null | Unix socket path of the Tarantool server. | -| [options] | Object | | Other options, including all from [net.createConnection](https://nodejs.org/api/net.html#netcreateconnection). | -| [options.port] | number | 6379 | Port of the Tarantool server. | -| [options.host] | string | "localhost" | Host of the Tarantool server. | -| [options.username] | string | null | If set, client will authenticate with the value of this option when connected. | -| [options.password] | string | null | If set, client will authenticate with the value of this option when connected. | -| [options.timeout] | number | 0 | The milliseconds before a timeout occurs during the initial connection to the Tarantool server. | -| [options.tls] | Object | null | If specified, forces to use `tls` module instead of the default `net`. In object properties you can specify any TLS-related options, e.g. from the [tls.createSecureContext()](https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions) | -| [options.keepAlive] | boolean | true | Enables keep-alive functionality (recommended). | -| [options.noDelay] | boolean | true | Disables the use of Nagle's algorithm (recommended). | -| [options.lazyConnect] | boolean | false | By default, When a new `Tarantool` instance is created, it will connect to Tarantool server automatically. If you want to keep disconnected util a command is called, you can pass the `lazyConnect` option to the constructor. | -| [options.nonWritableHostPolicy] | string | null | What to do when Tarantool server rejects write operation, e.g. because of `box.cfg.read_only` set to `true` or during snapshot fetching.
Possible values are:
- `0`: just rejects Promise with an error
- `1`: disconnect from the current host and connect to the next from `reserveHosts`. Pending Promise will be rejected.
- `2`: same as `1`, but after reconnecting tries to run the command again in order to fullfil the Promise | -| [options.enableAutoPipelining] | boolean | false | In auto pipelining mode, all commands issued during an event loop are enqueued in a pipeline automatically managed by tarantool-driver. At the end of the iteration, the pipeline is executed and thus all commands are sent to the server at the same time. | -| [options.maxRetriesPerRequest] | number | 5 | Number of attempts to find the alive host if `nonWritableHostPolicy` is not null. | -| [options.enableOfflineQueue] | boolean | true | By default, if there is no active connection to the Tarantool server, commands are added to a queue and are executed once the connection is "ready", meaning the connection to the Tarantool server has been established and auth passed (`connect` event is also executed at this moment). If this option is false, when execute the command when the connection isn't ready, an error will be returned. | -| [options.reserveHosts] | array | [] | Array of [strings](https://tarantool.org/en/doc/reference/configuration/index.html?highlight=uri#uri) - reserve hosts. Client will try to connect to hosts from this array after loosing connection with current host and will do it cyclically. See example below.| -| [options.beforeReserve] | number | 2 | Number of attempts to reconnect before connect to next host from the reserveHosts | -| [options.retryStrategy] | function | | See below | - -### Reserve hosts example: +| `port` | `number` \| `string` \| `Object` | `3301` | Port of the Tarantool server, or a URI string (see [Tarantool configuration docs](https://tarantool.org/en/doc/reference/configuration/index.html#uri)), or options object | +| `host` | `string` \| `Object` | `"localhost"` | Host of the Tarantool server; when first argument is a URL string, this argument becomes the options object | +| `path` | `string` | `null` | Unix socket path of the Tarantool server (overrides host/port) | +| `username` | `string` | `null` | Username for authentication when connected | +| `password` | `string` | `null` | Password for authentication when connected | +| `timeout` | `number` | `10000` | Milliseconds before timeout during initial connection or `.disconnect()` call | +| `tls` | `Object` | `null` | If specified, uses TLS instead of plain TCP. Accepts any options from [tls.createSecureContext()](https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions) | +| `keepAlive` | `boolean` | `true` | Enables TCP keep-alive functionality (recommended) | +| `noDelay` | `boolean` | `true` | Disables Nagle's algorithm (recommended for lower latency) | +| `lazyConnect` | `boolean` | `false` | If `true`, delays automatic connection until first command is called | +| `tupleToObject` | `boolean` | `false` | Converts response tuples from arrays to objects with field names as keys | +| `enableAutoPipelining` | `boolean` | `false` | 🚀 Auto-pipelines all commands during event loop iteration (improves throughput 2-4x with slight latency trade-off) | +| `enableOfflineQueue` | `boolean` | `true` | If `false`, rejects commands when not connected instead of queuing them | +| `commandTimeout` | `number` | `null` | Maximum execution time in milliseconds before rejecting the command (recommended: 500+) | +| `sliderBufferInitialSize` | `number` | `Buffer.poolSize * 10` | Initial size of buffer pool; increase for high-load scenarios, decrease for resource-constrained systems | +| `prefetchSchema` | `boolean` | `true` | Automatically loads space schema on connection | +| `reserveHosts` | `array` | `[]` | Array of fallback hosts for failover (as connection strings, objects, or port numbers) | +| `beforeReserve` | `number` | `2` | Number of reconnection attempts before trying next reserve host | +| `connectRetryAttempts` | `number` | `10` | Maximum connection attempts (including reserve hosts) before rejecting `.connect()` promise | +| `retryStrategy` | `function` | See below | Custom retry delay calculation function | +| `MsgPack` | `Class` | `MsgPack` | Custom MsgPack encoder/decoder implementation | +| `Connector` | `Class` | `StandaloneConnector` | Custom connection implementation | + +### Retry Strategy + +By default, the driver automatically reconnects when the connection is lost (except after manual `.disconnect()` or `.quit()`). + +Control reconnection timing with the `retryStrategy` option: + +```javascript +const tarantool = new Tarantool({ + // Default implementation + retryStrategy: function (times) { + return Math.min(times * 50, 2000); + } +}); +``` + +The function receives `times` (nth reconnection attempt) and returns milliseconds to wait before next attempt. Return a non-numeric value to stop retrying; manually call `.connect()` to resume. + +*This feature is inspired by [ioredis](https://github.com/luin/ioredis)* + +### Reserve Hosts Example ```javascript -let connection = new Tarantool({ - host: 'mail.ru', - port: 33013, - username: 'user' +const connection = new Tarantool({ + host: 'primary.example.com', + port: 3301, + username: 'user', password: 'secret', reserveHosts: [ - 'anotheruser:difficultpass@mail.ru:33033', + 'user:pass@secondary.example.com:3301', + '/var/run/tarantool.sock', '127.0.0.1:3301' ], beforeReserve: 1 -}) -// connect to mail.ru:33013 -> dead -// ↓ -// trying connect to mail.ru:33033 -> dead -// ↓ -// trying connect to 127.0.0.1:3301 -> dead -// ↓ -// trying connect to mail.ru:33013 ...etc +}); +// Attempts: primary → secondary → localhost → primary (cycle repeats) ``` -### Retry strategy +## Usage Example -By default, node-tarantool-driver client will try to reconnect when the connection to Tarantool is lost -except when the connection is closed manually by `tarantool.disconnect()`. +```javascript +const Tarantool = require('tarantool-driver'); +const conn = new Tarantool('user:password@localhost:3301'); -It's very flexible to control how long to wait to reconnect after disconnection -using the `retryStrategy` option: +// Select data +conn.select(512, 0, 10, 0, 'eq', [50]) + .then(results => { + console.log('Results:', results); + }) + .catch(err => { + console.error('Error:', err); + }); -```Javascript -var tarantool = new Tarantool({ - // This is the default value of `retryStrategy` - retryStrategy: function (times) { - var delay = Math.min(times * 50, 2000); - return delay; - } +// Simple callback style +conn.select(512, 0, 10, 0, 'eq', [50], {}, (err, results) => { + if (err) { + console.error('Error:', err); + } else { + console.log('Results:', results); + } }); ``` +## API Reference -`retryStrategy` is a function that will be called when the connection is lost. -The argument `times` means this is the nth reconnection being made and -the return value represents how long (in ms) to wait to reconnect. When the -return value isn't a number, node-tarantool-driver will stop trying to reconnect, and the connection -will be lost forever if the user doesn't call `tarantool.connect()` manually. +### 🔗 Connection Methods -**This feature is borrowed from the [ioTarantool](https://github.com/luin/ioTarantool)** +#### tarantool.connect() ⇒ `Promise` -## Usage example +Establishes connection to the Tarantool server. -We use TarantoolConnection instance and connect before other operations. Methods call return promise(https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Promise). Available methods with some testing: select, update, replace, insert, delete, auth, destroy. -```Javascript -var TarantoolConnection = require('tarantool-driver'); -var conn = new TarantoolConnection('notguest:sesame@mail.ru:3301'); +**Returns:** Promise that resolves when connected and authenticated, rejects on error. -// select arguments space_id, index_id, limit, offset, iterator, key -conn.select(512, 0, 1, 0, 'eq', [50]) - .then(funtion(results){ - doSomeThingWithResults(results); - }); +```javascript +await conn.connect(); ``` +#### tarantool.disconnect() ⇒ `undefined` -## Msgpack implementation +Closes the connection immediately. Pending commands may be lost. -You can use any implementation that can be duck typing with next interface: +**Returns:** `undefined` -```Javascript -//msgpack implementation example -/* - @interface - decode: (Buffer buf) - encode: (Object obj) - */ -var exampleCustomMsgpack = { - encode: function(obj){ - return yourmsgpack.encode(obj); - }, - decode: function(buf){ - return yourmsgpack.decode(obj); - } -}; +```javascript +conn.disconnect(); ``` -By default use msgpack-lite package. +#### tarantool.quit() ⇒ `Promise` -## API reference +Gracefully closes the connection after all sent commands complete. -### tarantool.connect() ⇒ Promise +**Returns:** Promise that resolves after all pending commands finish and connection closes. -Resolve if connected. Or reject if not. +```javascript +await conn.quit(); +``` + +#### tarantool._auth(login: `string`, password: `string`) ⇒ `Promise` + +**Internal method.** Authenticates with Tarantool using CHAP-SHA1 mechanism. -### tarantool._auth(login: String, password: String) ⇒ Promise +**Note:** Called automatically during connection if credentials are provided. -**An internal method. The connection should be established before invoking.** +See [Tarantool authentication docs](http://tarantool.org/doc/book/box/authentication.html) for details. -Auth with using [chap-sha1](http://tarantool.org/doc/book/box/box_space.html). About authenthication more here: [authentication](http://tarantool.org/doc/book/box/authentication.html) +--- -### tarantool.packUuid(uuid: String) +### 📊 Data Query Methods -**Method for converting [UUID values](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#uuid) to Tarantool-compatible format.** +#### tarantool.select(spaceId, indexId, limit, offset, iterator, key, [opts], [cb]) ⇒ `Promise` -If passing UUID without converion via this method, server will accept it as simple String. +Performs a SELECT query on the database. -### tarantool.packDecimal(numberToConvert: Number) +**Parameters:** +- `spaceId` (`number` | `string`) - Space ID or name +- `indexId` (`number` | `string`) - Index ID or name +- `limit` (`number`) - Maximum records to return +- `offset` (`number`, default: `0`) - Number of records to skip +- `iterator` (`string`, default: `'eq'`) - Iterator type: `'eq'`, `'req'`, `'all'`, `'lt'`, `'le'`, `'ge'`, `'gt'`, `'bitsAllSet'`, `'bitsAnySet'`, `'bitsAllNotSet'`, `'overlaps'`, `'neighbor'` +- `key` (`Array`) - Search key tuple +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, results)` -**Method for converting Numbers (Float or Integer) to Tarantool [Decimal](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#decimal) type.** +**Returns:** Promise resolving to array of tuples. If callback provided, returns `undefined`. -If passing number without converion via this method, server will accept it as Integer or Double (for JS Float type). +**Examples:** -### tarantool.packInteger(numberToConvert: Number) +```javascript +// By space/index ID +const results = await conn.select(512, 0, 10, 0, 'eq', [50]); -**Method for safely passing numbers up to int64 to bind params** +// By space/index name +const results = await conn.select('users', 'primary', 10, 0, 'eq', [50]); -Otherwise msgpack will encode anything bigger than int32 as a double number. +// With callback +conn.select(512, 0, 10, 0, 'eq', [50], {}, (err, results) => { + if (err) console.error(err); + else console.log(results); +}); -### tarantool.select(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple) ⇒ Promise +// With UUID +const results = await conn.select( + 'users', 'id', 1, 0, 'eq', + [conn.packUuid('550e8400-e29b-41d4-a716-446655440000')] +); +``` -[Iterators](http://tarantool.org/doc/book/box/box_index.html). Available iterators: 'eq', 'req', 'all', 'lt', 'le', 'ge', 'gt', 'bitsAllSet', 'bitsAnySet', 'bitsAllNotSet'. +#### tarantool.selectCb(spaceId, indexId, limit, offset, iterator, key, successCb, errorCb, [opts]) ⇒ `undefined` -It's just select. Promise resolve array of tuples. +**Deprecated.** Use `.select()` with callback parameter instead. -Some examples: +Legacy callback-style select. Parameters order differs from `.select()`. -```Javascript -conn.select(512, 0, 1, 0, 'eq', [50]); -//same as -conn.select('test', 'primary', 1, 0, 'eq', [50]); +```javascript +conn.selectCb(512, 0, 10, 0, 'eq', [50], + (results) => console.log(results), + (error) => console.error(error) +); ``` -You can use space name or index name instead of id, but this way some requests will be made to get and cache metadata. This stored information will be actual for delete, replace, insert, update too. +#### tarantool.insert(spaceId, tuple, [opts], [cb]) ⇒ `Promise` -For tests, we will create a Space named 'users' on the Tarantool server-side, where the 'id' index is of UUID type: +Inserts a tuple into the database. -```lua --- example schema of such space -box.schema.space.create("users", {engine = 'memtx'}) -box.space.users:format({ - {name = 'id', type = 'uuid', is_nullable = false}, - {name = 'username', type = 'string', is_nullable = false} -}) +**Parameters:** +- `spaceId` (`number` | `string`) - Space ID or name +- `tuple` (`Array`) - Data tuple to insert +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to the inserted tuple. + +```javascript +const result = await conn.insert('users', [1, 'Alice', 'alice@example.com']); ``` -And then select some tuples on a client side: -```Javascript -conn.select('users', 'id', 1, 0, 'eq', [conn.packUuid('550e8400-e29b-41d4-a716-446655440000')]); + +#### tarantool.replace(spaceId, tuple, [opts], [cb]) ⇒ `Promise` + +Replaces a tuple in the database (inserts if not exists). + +**Parameters:** +- `spaceId` (`number` | `string`) - Space ID or name +- `tuple` (`Array`) - Data tuple to replace +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to the replaced or inserted tuple. + +See [Tarantool replace docs](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.replace). + +```javascript +const result = await conn.replace('users', [1, 'Alice Updated', 'alice.new@example.com']); ``` -### tarantool.selectCb(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple, callback: function(success), callback: function(error)) +#### tarantool.update(spaceId, indexId, key, ops, [opts], [cb]) ⇒ `Promise` + +Updates a tuple in the database. + +**Parameters:** +- `spaceId` (`number` | `string`) - Space ID or name +- `indexId` (`number` | `string`) - Index ID or name +- `key` (`Array`) - Key tuple to identify record +- `ops` (`Array`) - Update operations: `[operator, fieldId/fieldName, value]` +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to the updated tuple. -Same as [tarantool.select](#select) but with callbacks. +**Operators:** `'='`, `'+'`, `'-'`, `'&'`, `'|'`, `'^'`, `':'`, `'!'`, `'#'`, `'%'` - see [Tarantool update docs](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.update). + +```javascript +// Update field 2 to new value +const result = await conn.update('users', 'primary', [1], [['=', 2, 'New Name']]); + +// Increment field by 1 +const result = await conn.update('users', 'primary', [1], [['+', 'counter', 1]]); +``` -### tarantool.delete(spaceId: Number or String, indexId: Number or String, key: tuple) ⇒ Promise +#### tarantool.delete(spaceId, indexId, key, [opts], [cb]) ⇒ `Promise` -Promise resolve an array of deleted tuples. +Deletes a tuple from the database. -### tarantool.update(spaceId: Number or String, indexId: Number or String, key: tuple, ops) ⇒ Promise +**Parameters:** +- `spaceId` (`number` | `string`) - Space ID or name +- `indexId` (`number` | `string`) - Index ID or name +- `key` (`Array`) - Key tuple to identify record +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` -[Possible operators.](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.update) +**Returns:** Promise resolving to the deleted tuple. -Promise resolve an array of updated tuples. +```javascript +const deleted = await conn.delete('users', 'primary', [1]); +``` -### tarantool.insert(spaceId: Number or String, tuple) ⇒ Promise +#### tarantool.upsert(spaceId, tuple, ops, [opts], [cb]) ⇒ `Promise` -More you can read here: [Insert](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.insert) +Updates or inserts a tuple (insert if not exists, update if exists). -Promise resolve a new tuple. +**Parameters:** +- `spaceId` (`number` | `string`) - Space ID or name +- `tuple` (`Array`) - Tuple to insert if key not found +- `ops` (`Array`) - Update operations if key exists +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` -### tarantool.upsert(spaceId: Number or String, ops: array of operations, tuple: tuple) ⇒ Promise +**Returns:** Promise that resolves (typically with no value). -About operation: [Upsert](http://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.upsert) +See [Tarantool upsert docs](http://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.upsert). -[Possible operators.](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.update) +```javascript +await conn.upsert('users', [1, 'Alice', 'alice@example.com'], [['+', 'counter', 1]]); +``` -Promise resolve nothing. +--- -### tarantool.replace(spaceId: Number or String, tuple: tuple) ⇒ Promise +### 🔄 Transaction Methods -More you can read here: [Replace](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.replace) +Transactions use streams to maintain isolation. Use within `connection.transaction()` context. -Promise resolve a new or replaced tuple. +#### tarantool.transaction() ⇒ `Transaction` -### tarantool.call(functionName: String, args...) ⇒ Promise +Creates a new transaction context for executing commands. -Call a function with arguments. +**Returns:** Transaction object that routes commands to same stream. -You can create function on tarantool side: -```Lua -function myget(id) - val = box.space.batched:select{id} - return val[1] -end +```javascript +const txn = conn.transaction(); +await txn.insert('users', [1, 'Alice']); +await txn.begin(); +await txn.update('users', 'primary', [1], [['=', 2, 'Alice Updated']]); +await txn.commit(); ``` -And then use something like this: -```Javascript -conn.call('myget', 4) - .then(function(value){ - console.log(value); - }); +#### transaction.begin([transTimeoutSec], [isolationLevel], [opts], [cb]) ⇒ `Promise` + +Starts a transaction. + +**Parameters:** +- `transTimeoutSec` (`number`, default: `60`) - Transaction timeout in seconds +- `isolationLevel` (`number`, default: `0`) - Isolation level (0 = default) +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise that resolves when transaction begins. + +```javascript +const txn = conn.transaction(); +await txn.begin(120, 0); + +try { + await txn.insert('users', [1, 'Alice']); + await txn.insert('logs', ['INSERT user 1']); + await txn.commit(); +} catch (err) { + await txn.rollback(); + throw err; +} ``` -If you have a 2 arguments function just send a second arguments in this way: -```Javascript -conn.call('my2argumentsfunc', 'first', 'second argument') +#### transaction.commit([opts], [cb]) ⇒ `Promise` + +Commits the transaction. + +**Parameters:** +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise that resolves when transaction commits. + +#### transaction.rollback([opts], [cb]) ⇒ `Promise` + +Rolls back the transaction. + +**Parameters:** +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise that resolves when transaction rolls back. + +--- + +### 🔧 Server Methods + +#### tarantool.call(functionName, [args], [opts], [cb]) ⇒ `Promise` + +Calls a Lua function on the server. + +**Parameters:** +- `functionName` (`string`) - Name of the function to call +- `args` (`Array`, optional) - Function arguments passed as array +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to function result (typically array for multiple returns). + +**Note:** Arguments must be passed as array since v4.0.0. + +```javascript +// Server-side function +// box.schema.func.create('get_user', {if_not_exists = true}) +// function get_user(id) return box.space.users:select{id} end + +const results = await conn.call('get_user', [42]); +``` + +#### tarantool.eval(expression, [args], [opts], [cb]) ⇒ `Promise` + +Evaluates Lua code on the server. + +**Parameters:** +- `expression` (`string`) - Lua code to evaluate +- `args` (`Array`, optional) - Variables passed to Lua code +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to evaluation result. + +```javascript +const userId = await conn.eval('return box.session.user()', []); + +const customResult = await conn.eval( + 'return select(1, ...)', + [1, 2, 3, 4, 5] +); +``` + +#### tarantool.sql(sqlQuery, [bindParams], [opts], [cb]) ⇒ `Promise` + +Executes SQL query on the server (Tarantool 2.1+). + +**Parameters:** +- `sqlQuery` (`string` | `PreparedStatement`) - SQL query string or prepared statement instance +- `bindParams` (`Array`, optional) - Bind parameters +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to query results. + +⚠️ **Note:** For spaces with lowercase names, use double quotes: `"space_name"` + +See [Tarantool SQL tutorial](https://www.tarantool.io/en/doc/2.1/tutorials/sql_tutorial/). + +```javascript +await conn.sql('INSERT INTO tags VALUES (?, ?)', ['tag_1', 1]); +const prepStmt = await conn.prepare('INSERT INTO tags VALUES (?, ?)'); +await conn.sql(prepStmt, ['tag_2', 50]); + +const results = await conn.sql('SELECT * FROM "tags"'); +``` + +#### tarantool.prepare(sqlQuery, [opts], [cb]) ⇒ `Promise` + +Prepares an SQL statement for repeated execution. + +**Parameters:** +- `sqlQuery` (`string`) - SQL query string +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to PreparedStatement object which can be passed later as `.sql(prepStmtObj, ...)` parameter. + +```javascript +const stmt = await conn.prepare('SELECT * FROM users WHERE id = ?'); +const results = await conn.sql(stmt, [1]); +``` + +#### tarantool.ping([opts], [cb]) ⇒ `Promise` + +Sends a PING command to the server. + +**Parameters:** +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to `true` if server responds. + +```javascript +const isAlive = await conn.ping(); +``` + +#### tarantool.id([version], [features], [auth_type], [opts], [cb]) ⇒ `Promise` + +Sends an ID (handshake) command to negotiate protocol version, features, and authentication type with the server. + +**Parameters:** +- `version` (`number`, default: `3`) - Protocol version to use +- `features` (`Array`, default: `[1]`) - List of supported features +- `auth_type` (`string`, default: `'chap-sha1'`) - Authentication type (e.g., `'chap-sha1'`) +- `opts` (`Object`, optional) - Options object +- `cb` (`Function`, optional) - Callback function `(error, result)` + +**Returns:** Promise resolving to server identity and capabilities information. + +```javascript +// Basic handshake with defaults +const serverInfo = await conn.id(); + +// Custom protocol negotiation +const serverInfo = await conn.id(3, [1, 2], 'chap-sha1'); ``` -And etc like this. -Because lua support a multiple return it's always return array or undefined. +--- -### tarantool.eval(expression: String) ⇒ Promise +### ⚡ Pipelining Methods -Evaluate and execute the expression in Lua-string. [Eval](https://tarantool.org/doc/reference/reference_lua/net_box.html?highlight=eval#lua-function.conn.eval) +#### tarantool.pipeline() ⇒ `Pipeline` -Promise resolve result:any. +Starts a pipeline for batching commands. Commands are queued in memory without sending to server immediately. -Example: +**Returns:** Pipeline object for method chaining. +**Performance:** 🚀 Improves throughput by 300%+ compared to individual requests. -```Javascript -conn.eval('return box.session.user()') - .then(function(res){ - console.log('current user is:' res[0]) +```javascript +const results = await conn.pipeline() + .insert('users', [1, 'Alice']) + .insert('users', [2, 'Bob'], null /* opts */, (error, result) => { + // some processing logic + // "error" and "result" arguments are corresponding exactly to this request }) + .select('users', 'primary', 10, 0, 'eq', [1]) + .exec(); + +// Results format: [[err1, res1], [err2, res2], [err3, res3]] ``` -### tarantool.sql(query: String, bindParams: Array) ⇒ Promise +#### pipeline.exec() ⇒ `Promise` -It's accessible only in 2.1 tarantool. +Executes all queued commands in a single network call. -You can use SQL query that is like sqlite dialect to query a tarantool database. +**Returns:** Promise resolving to `PipelineResponse` instance (extends Array) containing `[error, result]` pairs for each command. -You can insert or select or create database. +#### pipeline.flushPipelined() ⇒ `undefined` -More about it [here](https://www.tarantool.io/en/doc/2.1/tutorials/sql_tutorial/). +Clears the pipelined commands queue. -Example: +**Returns:** `undefined` -```Javascript -await connection.insert('tags', ['tag_1', 1]) -await connection.insert('tags', ['tag_2', 50]) -connection.sql('select * from "tags"') -.then((res) => { - console.log('Successful get tags', res); -}) -.catch((error) => { - console.log(error); -}); +**PipelineResponse Methods:** +- `findPipelineError()` - Returns the first error found in results, or `null` if all succeeded +- `findPipelineErrors()` - Returns array of all errors found, or empty array if all succeeded + +```javascript +const pipeline = conn.pipeline(); +pipeline.select(512, 0, 10, 0, 'eq', [1]); +pipeline.select(512, 0, 10, 0, 'eq', [2]); +pipeline.update('metadata', 'primary', ['counter'], [['+', 'value', 1]]); + +const pipelineResponse = await pipeline.exec(); + +// Access individual results +const [selectErr1, selectRes1] = pipelineResponse[0]; +const [selectErr2, selectRes2] = pipelineResponse[1]; +const [updateErr, updateRes] = pipelineResponse[2]; + +// Find errors easily +const firstError = pipelineResponse.findPipelineError(); +if (firstError) { + console.error('First error:', firstError); +} + +const allErrors = pipelineResponse.findPipelineErrors(); +if (allErrors.length > 0) { + console.error('All errors:', allErrors); +} ``` -P.S. If you using lowercase in your space name you need to use a double quote for their name. +--- -It doesn't work for space without format. +### 🔧 Utility Methods -### tarantool.pipeline().<...>.exec() +#### tarantool.packUuid(uuid: `string`) ⇒ `Buffer` -Queue some commands in memory and then send them simultaneously to the server in a single (or several, if request body is too big) network call(s). -This way the performance is significantly improved by more than 300% (depending on the number of pipelined commands - the bigger, the better) +Converts UUID string to Tarantool-compatible format. -Example: +**Parameters:** +- `uuid` (`string`) - UUID string (e.g., `'550e8400-e29b-41d4-a716-446655440000'`) -```Javascript -tarantool.pipeline() -.insert('tags', ['tag_1', 1]) -.insert('tags', ['tag_2', 50]) -.sql('update "tags" set "amount" = 10 where "tag_id" = \'tag_1\'') -.update('tags', 'tag_id', ['tag_2'], [['=', 'amount', 30]]) -.sql('select * from "tags"') -.call('truncateTags') -.exec() +**Returns:** Encoded buffer for use in queries. + +**Note:** Without conversion, UUIDs are sent as plain strings. + +```javascript +const uuid = conn.packUuid('550e8400-e29b-41d4-a716-446655440000'); +const result = await conn.select('users', 'id', 1, 0, 'eq', [uuid]); ``` -### tarantool.ping() ⇒ Promise +#### tarantool.packDecimal(number) ⇒ `Buffer` -Promise resolve true. +Converts JavaScript number to Tarantool Decimal type. -### ~~tarantool.destroy(interupt: Boolean) ⇒ Promise~~ -***Deprecated*** -### tarantool.disconnect() -Disconnect from Tarantool. +**Parameters:** +- `number` (`number` | `bigint`) - Number to convert -This method closes the connection immediately, -and may lose some pending replies that haven't written to client. +**Returns:** Encoded buffer for use in queries. -## Debugging +**Note:** Without conversion, large numbers are sent as Double/Integer. -Set environment variable "DEBUG" to "tarantool-driver:*" +See [Tarantool Decimal docs](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#decimal). -## Contributions +```javascript +const decimal = conn.packDecimal(123.456); +await conn.insert('prices', [1, 'Item', decimal]); +``` -It's ok you can do whatever you need. I add log options for some technical information it can be help for you. If i don't answer i just miss email :( it's a lot emails from github so please write me to newbiecraft@gmail.com directly if i don't answer in one day. +#### tarantool.packInteger(number) ⇒ `Buffer` -## Changelog +Safely converts numbers to Tarantool integer format (up to int64). -### 4.0.0 +**Parameters:** +- `number` (`number`) - Number to convert -- `nonWritableHostPolicy` option changed it's type from `string` to `number` -- `nonWritableHostPolicy` moved to a development state, consider not using it in production -- Reworked offline queue -- Fixed bug with buffer reuse -- Fixed use of Transactions if connection is not inited yet +**Returns:** Encoded buffer for use in queries. -### 3.2.0 +**Note:** Without conversion, numbers > int32 are encoded as Double. -- Now supports [prepared](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/#box-sql-box-prepare) SQL statements. -- Huge rewrite of the codebase, which improved performance by N%: - - Now using 'msgpackr' instead of 'msgpack-lite' - - Buffer reuse - - Decreased memory consumption -- New AutoPipelining mode: optimise performance with no need to rewrite your existing code. Benchmark showed x4 performance for the `select` requests: - - 350k/sec without AutoPipelining - - 1400k/sec with AutoPipelining feature enabled -- [IPROTO_ID](https://www.tarantool.io/en/doc/latest/reference/internals/iproto/requests/#iproto-id) can be invoked as 'conn.id()' function. -- [Streams](https://www.tarantool.io/en/doc/latest/platform/atomic/txn_mode_mvcc/#streams-and-interactive-transactions) support. +```javascript +const bigInt = conn.packInteger(9223372036854775807); // Max int64 +await conn.insert('bigdata', [1, bigInt]); +``` -### 3.1.0 +#### tarantool.packInterval(value) ⇒ `Buffer` -- Added 3 new msgpack extensions: UUID, Datetime, Decimal. -- Connection object now accepts all options of `net.createConnection()`, including Unix socket path. -- New `nonWritableHostPolicy` and related options, which improves a high availability capabilities without any 3rd parties. -- Ability to disable the offline queue. -- Fixed [bug with int32](https://github.com/tarantool/node-tarantool-driver/issues/48) numbers when it was encoded as floating. Use method `packInteger()` to solve this. -- `selectCb()` now also accepts `spaceId` and `indexId` as their String names, not only their IDs. -- Some performance improvements by caching internal values. -- TLS (SSL) support. -- New `pipeline()`+`exec()` methods kindly borrowed from the [ioredis](https://github.com/redis/ioredis?tab=readme-ov-file#pipelining), which lets you to queue some commands in memory and then send them simultaneously to the server in a single (or several, if request body is too big) network call(s). Thanks to the Tarantool, which [made this possible](https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/format/#packet-structure). -This way the performance is significantly improved by 500-1600% - you can check it yourself by running `npm run benchmark-read` or `npm run benchmark-write`. -Note that this feature doesn't replaces the Transaction model, which has some level of isolation. -- Changed `const` declaration to `var` in order to support old Node.JS versions. +Converts value to Tarantool Interval type. -### 3.0.7 +**Parameters:** +- `value` (`Object` | `number`) - Interval specification -Fix in header decoding to support latest Tarantool versions. Update to tests to support latest Tarantool versions. +**Returns:** Encoded buffer for use in queries. -### 3.0.6 +```javascript +const interval = conn.packInterval({ years: 1, months: 2, days: 3 }); +``` -Remove let for support old nodejs version +#### tarantool.fetchSchema() ⇒ `Promise` -### 3.0.5 +Fetches and caches database schema (spaces and indexes). -Add support SQL +**Returns:** Promise resolving to namespace object with space/index metadata. -### 3.0.4 +**Use case:** Required if using space/index by name instead of ID without `prefetchSchema: true`. -Fix eval and call +```javascript +await conn.fetchSchema(); +const userId = conn.namespace['users'].id; +``` -### 3.0.3 +--- -Increase request id limit to SMI Maximum +## 🔍 Debugging -### 3.0.2 +Enable debug logging by setting the `DEBUG` environment variable: -Fix parser thx @tommiv +```bash +DEBUG=tarantool-driver:* node app.js +``` -### 3.0.0 +This displays detailed information about: +- Connection state changes +- Sent requests and received responses +- Buffer operations +- Schema fetching +- Event loop handling -New version with reconnect in alpha. +--- -### 1.0.0 +## Related Documentation -Fix test for call changes and remove unuse upsert parameter (critical change API for upsert) +- 📖 **[Performance Guide](./PERFORMANCE.md)** — Tips and tricks for maximum throughput 🚀 + - Buffer size tuning + - Auto-pipelining optimization + - Unix socket advantages + +- 📊 **[Benchmarks](./BENCHMARK.md)** — Performance measurements and comparisons + - Read/write throughput comparisons + - Impact of different options + +- 📋 **[Changelog](./CHANGELOG.md)** — Version history and breaking changes + - New features by version + - Migration guides -### 0.4.1 +--- -Add clear schema cache on change schema id +## 🤝 Contributions -### 0.4.0 +Contributions are welcome! If you have questions or suggestions: -Change msgpack5 to msgpack-lite(thx to @arusakov). -Add msgpack as option for connection. -Bump msgpack5 for work at new version. +1. Check existing [issues](https://github.com/tarantool/node-tarantool-driver/issues) +2. Create a new issue with details +3. Submit pull requests for bug fixes or features -### 0.3.0 -Add upsert operation. -Key is now can be just a number. +For urgent matters, email directly: newbiecraft@gmail.com -## ToDo +--- -1. Events and subscriptions -2. Graceful shutdown protocol \ No newline at end of file +**Made with ❤️ for Tarantool community** diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..dbeb67a --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +1. Events and subscriptions +2. Graceful shutdown protocol +3. Support of nanoseconds for `DateTime` format using +```javascript +require('node:process').hrtime.bigint() +``` +4. Add SQL tests +5. Add coverage reports \ No newline at end of file diff --git a/assets/box.lua b/assets/box.lua new file mode 100644 index 0000000..436b57e --- /dev/null +++ b/assets/box.lua @@ -0,0 +1,76 @@ +box.cfg{ + listen={ + 3301, + '/tmp/tarantoolTest.sock' -- most of the UNIX-like systems have a free access for '/tmp' folder + }, + memtx_use_mvcc_engine=true, + -- log_level=10, + iproto_threads = 2, + net_msg_max = 768 * 2, -- double the default + memtx_memory = 512 * 2^20, -- 512 mb should be enough + readahead = 16320 * 10 -- multiply the default by 10 +} + +if not box.schema.user.exists('test') then + box.schema.user.create('test', {password = 'notStrongPass :('}) + box.schema.user.grant('test', 'read,write,execute', 'universe') +end + +box.once('grant_user_right', function() + box.schema.user.grant('guest', 'read,write,execute', 'universe') +end) + +s = box.space.bench_memtx +if not s then + s = box.schema.space.create('bench_memtx') + s:format({ + {name = 'id', type = 'unsigned'}, + {name = 'line', type = 'array'} + }) + s:create_index('hash_idx', {type = 'hash', parts = {1, 'unsigned'}}) + s:create_index('tree_idx', {type = 'tree', parts = {1, 'unsigned'}}) + s:create_index('rtree_idx', {type = 'rtree', parts = {2, 'array'}}) +end + +s = box.space.bench_vinyl +if not s then + s = box.schema.space.create('bench_vinyl', {engine = 'vinyl'}) + s:format({ + {name = 'id', type = 'unsigned'}, + {name = 'line', type = 'array'} + }) + p = s:create_index('tree_idx', {type = 'tree', parts = {1, 'unsigned'}}) +end + +function clear() + box.session.su('admin') + box.space.bench_memtx:truncate{} + box.space.bench_vinyl:truncate{} +end + +function func_arg(arg) + return arg +end + +function sum (a, b) + return a + b +end + +if not box.schema.func.exists('func_arg') then + box.schema.func.create('func_arg') + box.schema.user.grant('test', 'execute', 'function', 'func_arg') +end + +if not box.schema.func.exists('sum') then + box.schema.func.create('sum') + box.schema.user.grant('test', 'execute', 'function', 'sum') +end + +function sleep(s) + os.execute("sleep " .. tonumber(n)) +end + +if not box.schema.func.exists('sleep') then + box.schema.func.create('sleep') + box.schema.user.grant('test', 'execute', 'function', 'sleep') +end \ No newline at end of file diff --git a/assets/docker.sh b/assets/docker.sh new file mode 100644 index 0000000..80816bd --- /dev/null +++ b/assets/docker.sh @@ -0,0 +1,10 @@ +docker run \ + --name tarantool-test-box \ + -v `pwd`/assets:/opt/tarantool \ + -v /tmp:/tmp \ + --rm \ + -d \ + -p 3301:3301 \ + -e TT_DATABASE_USE_MVCC_ENGINE=true \ + -e TT_IPROTO_THREADS=2 \ + tarantool/tarantool:latest tarantool /opt/tarantool/box.lua \ No newline at end of file diff --git a/assets/read-results-table.png b/assets/read-results-table.png new file mode 100644 index 0000000000000000000000000000000000000000..731a3559319aa37649fcb2a81e60fc8b3d3ca2ae GIT binary patch literal 400286 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∾BQ5iD_HPD5IRI{TA&Oq1lN*fG@)zg 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

sV*O5~yI=esE2>e!`)iA$J{1(&`=Q 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%0AXUq28q`hk!8Ceo}m8i)(HK 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

L9PDktOz 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%0D2PfG55n5`;w89gi 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

L#XgS$9DA$d;>egA7+800#?PiO9weRw(F* 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℘1&JqUJ>{&8-m0 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�lHHs=695>lC(byojk=cb+fRd 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 AP5=M^ literal 0 HcmV?d00001 diff --git a/benchmark/read.js b/benchmark/read.js index 4a4b6e0..c348a61 100755 --- a/benchmark/read.js +++ b/benchmark/read.js @@ -1,193 +1,202 @@ 'use strict'; - -var Benchmark = require('benchmark'); -var suite = new Benchmark.Suite(); -var Driver = require('../lib/connection.js'); -var noop = require('lodash/noop'); -var promises; -var preparedSelectStmtId = 0; - -var connectionArg = process.argv[process.argv.length - 1] - -var conn = new Driver(connectionArg, { - lazyConnect: true, - tupleToObject: false +const { Bench } = require('tinybench'); +const Driver = require('../lib/connection.js'); +const conn = new Driver('localhost:3301', { + lazyConnect: true +}); +const connUnixPath = new Driver('/tmp/tarantoolTest.sock', { + lazyConnect: true +}); +const {format} = require('node:util'); + +const bench = new Bench({ + name: 'read benchmark', + iterations: 10000, + setup: (task) => { + console.info( + format('starting task "%s"', task.name) + ); + }, + warmup: false, + threshold: 1, + concurrency: null }); -conn.connect() -.then(function () { - return Promise.all([ - // preload schemas - conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, noop), - // create a prepared SQL statement - conn.prepare('SELECT * FROM "counter" WHERE "primary" = ? LIMIT 1 OFFSET 0') - .then(function (result) { - preparedSelectStmtId = result; - }) - ]) -}) -.then(function(){ - // non-deferred benchmarks measures the real performance of code, e.g. not awaiting for the response to be received - suite.add('non-deferred select', {defer: false, fn: function(){ - conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, console.error); - }}); - - suite.add('non-deferred select, tupleToObject', {defer: false, fn: function(){ - conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], noop, console.error, { - tupleToObject: true - }); - }}); - - // show the performance improvement when using an autopipelining - suite.add("non-deferred select + autopipelining", { - defer: false, - onStart: () => { - // enable the autopipelining feature - conn.enableAutoPipelining = true; - }, - onComplete: () => { - // disable the autopipelining feature - conn.enableAutoPipelining = false; - }, - fn: function () { - conn.selectCb( - "counter", - 0, - 1, - 0, - "eq", - ["test"], - noop, - console.error - ); - }, - }); - - suite.add('non-deferred select from vinyl', {defer: false, fn: function(){ - conn.selectCb('counter_vinyl', 0, 1, 0, 'eq', ['test'], noop, console.error, { - tupleToObject: true - }); - }}); - - suite.add('non-deferred sql select', {defer: false, fn: function(){ - conn.sql('SELECT * FROM "counter" WHERE "primary" = ? LIMIT 1 OFFSET 0', ['test']); - }}); - - suite.add('non-deferred sql prepared select', {defer: false, fn: function(){ - conn.sql(preparedSelectStmtId, ['test']); - }}); - - suite.add('select cb', {defer: true, fn: function(defer){ - function callback(){ - defer.resolve(); - } - conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], callback, console.error); - }}); - - suite.add('select promise', {defer: true, fn: function(defer){ - conn.select('counter', 0, 1, 0, 'eq', ['test']) - .then(function(){ defer.resolve();}); - }}); - - suite.add('paralell 500', {defer: true, fn: function(defer){ - try{ - promises = []; - for (let l=0;l<500;l++){ - promises.push(conn.select('counter', 0, 1, 0, 'eq', ['test'])); - } - var chain = Promise.all(promises); - chain.then(function(){ defer.resolve(); }) - .catch(function(e){ - console.error(e, e.stack); - defer.reject(e); - }); - } catch(e){ - defer.reject(e); - console.error(e, e.stack); - } - }}); - - suite.add('paralel by 10', {defer: true, fn: function(defer){ - var chain = Promise.resolve(); - try{ - for (var i=0;i<50;i++) - { - chain = chain.then(function(){ - promises = []; - for (var l=0;l<10;l++){ - promises.push( - conn.select('counter', 0, 1, 0, 'eq', ['test']) - ); - } - return Promise.all(promises); - }); - } - - chain.then(function(){ defer.resolve(); }) - .catch(function(e){ - console.error(e, e.stack); - }); - } catch(e){ - console.error(e, e.stack); - } - }}); - - suite.add('paralel by 50', {defer: true, fn: function(defer){ - var chain = Promise.resolve(); - try{ - for (var i=0;i<10;i++) - { - chain = chain.then(function(){ - promises = []; - for (var l=0;l<50;l++){ - promises.push( - conn.select('counter', 0, 1, 0, 'eq', ['test']) - ); - } - return Promise.all(promises); - }); - } - - chain.then(function(){ defer.resolve(); }) - .catch(function(e){ - console.error(e, e.stack); - }); - } catch(e){ - console.error(e, e.stack); - } - }}); - - suite.add('pipelined select by 10', {defer: true, fn: function(defer){ - var pipelinedConn = conn.pipeline() - - for (var i=0;i<10;i++) { - pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']); - } - - pipelinedConn.exec() - .then(function(){ defer.resolve(); }) - .catch(function(e){ defer.reject(e); }); - }}); - - suite.add('pipelined select by 50', {defer: true, fn: function(defer){ - var pipelinedConn = conn.pipeline() - - for (var i=0;i<50;i++) { - pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']); - } - - pipelinedConn.exec() - .then(function(){ defer.resolve(); }) - .catch(function(e){ defer.reject(e); }); - }}); +bench.addEventListener('cycle', (evt) => { + const result = evt.task.result; + if (result.state != 'completed') return; - suite - .on('cycle', function(event) { - console.log(String(event.target)); - }) - .on('complete', function() { - console.log('complete'); - process.exit(); - }) - .run({ 'async': true, 'queued': true }); + console.info(`\x1b[36mCompleted task\x1b[0m "${evt.task.name}":`); + console.info(` Latency: ${result.latency.mean.toFixed(5)} ms (min: ${result.latency.min.toFixed(5)}, max: ${result.latency.max.toFixed(5)})`); + console.info(` Throughput: ${Math.floor(result.throughput.mean)} ops/s`); }); + +let counter = 0; +let preparedStmt; +const sqlStmt = `SELECT * FROM "bench_memtx" INDEXED BY "tree_idx" WHERE "id" = ?`; +const pipelinedTaskName = '[pipeline] - non-deferred pipelined select by 20; HASH index'; + +/** + * Resets the counter variable + */ +const resetCounter = () => { + counter = 0; +}; + +Promise.all([ + conn.connect(), + connUnixPath.connect() +]) + .then(() => { + bench + // memtx + .add( + '[memtx] - select; HASH index', + () => { + return conn.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [counter++]); + }, + { + async: true, + afterAll: resetCounter + } + ) + .add( + '[memtx] - select; HASH index; UNIX-path socket', + () => { + return connUnixPath.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [counter++]); + }, + { + async: true, + afterAll: resetCounter + } + ) + .add('[memtx] - non-deferred select; HASH index', () => { + conn.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [counter++]); + }, { + async: false, + afterAll: resetCounter + }) + .add('[memtx] - non-deferred select; HASH index; UNIX-path socket', () => { + connUnixPath.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [counter++]); + }, { + async: false, + afterAll: resetCounter + }) + .add('[memtx] - non-deferred select; HASH index; UNIX-path socket; using "commandTimeout" option', () => { + connUnixPath.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [counter++]); + }, { + async: false, + beforeAll: () => connUnixPath.options.commandTimeout = 10000, + afterAll: () => { + connUnixPath.options.commandTimeout = null; + resetCounter(); + } + }) + .add( + '[memtx] - select; TREE index', + () => { + return conn.select('bench_memtx', 'tree_idx', 1, 0, 'eq', [counter++]); + }, + { + async: true, + afterAll: resetCounter + } + ) + .add( + '[memtx] - select; RTREE index', + () => { + const prevC = counter; + counter++; + const nextC = counter; + return conn.select('bench_memtx', 'rtree_idx', 1, 0, 'ge', [prevC, nextC]); + }, + { + async: true, + afterAll: resetCounter + } + ) + + // vinyl + .add('[vinyl] - select; TREE index', () => { + return conn.select('bench_vinyl', 'tree_idx', 1, 0, 'eq', [counter++]); + }, { + async: true, + afterAll: resetCounter + }) + .add('[vinyl] - non-deferred select; TREE index', () => { + conn.select('bench_vinyl', 'tree_idx', 1, 0, 'eq', [counter++]); + }, { + async: false, + afterAll: resetCounter + }) + + // SQL + // .add(`[SQL] - select; TREE index (memtx)`, () => { + // return conn.sql(sqlStmt, [counter++]); + // }, { + // async: true, + // afterAll: resetCounter + // }) + .add(`[SQL] - non-deferred select; TREE index (memtx)`, () => { + conn.sql(sqlStmt, [counter++]); + }, { + async: false, + afterAll: resetCounter + }) + .add(`[SQL] - non-deferred prepared select; TREE index (memtx)`, () => { + conn.sql(preparedStmt, [counter++]); + }, { + async: false, + beforeAll: async () => { + preparedStmt = await conn.prepare(sqlStmt); + }, + afterAll: resetCounter + }) + + // pipeline + const pipelinedConn = conn.pipeline(); // it is possible to create the pipelined instance only once and reuse it in future + bench + .add('[pipeline] - non-deferred autopipelined select; HASH index', () => { + conn.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [counter++]); + }, { + async: false, + beforeAll: () => conn.options.enableAutoPipelining = true, + afterAll: () => conn.options.enableAutoPipelining = false + }) + .add(pipelinedTaskName, () => { + for (let i = 0; i < 20; i++) { + pipelinedConn.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [counter++]); + } + pipelinedConn.exec(); + }, { + async: false, + afterAll: resetCounter + }); + + return bench.run(); + }) + .then(() => { + // pipelined benchmark measures loops by default, but we need to count exact 'insert' requests + const task = bench.getTask(pipelinedTaskName); + if (task?.result?.state == 'completed') { + const throughput = task.result.throughput; + throughput.min = throughput.min * 20; + throughput.mean = throughput.mean * 20; + throughput.max = throughput.max * 20; + throughput.p50 = throughput.p50 * 20; + } + + console.info(`Benchmark "${bench.name}" finished, results: `) + console.table(bench.table()); + + return Promise.all([ + conn.quit(), + connUnixPath.quit() + ]) + }) + .catch(function (e) { + console.error('bench failed: ', e); + }) + .finally(async () => { + process.exit(); + }); \ No newline at end of file diff --git a/benchmark/write.js b/benchmark/write.js index fe00801..7a64733 100755 --- a/benchmark/write.js +++ b/benchmark/write.js @@ -1,93 +1,207 @@ 'use strict'; -var Benchmark = require('benchmark'); -var suite = new Benchmark.Suite(); -var Driver = require('../lib/connection.js'); -var conn = new Driver(process.argv[process.argv.length - 1], {lazyConnect: true}); -var promises; -var c = 0; +const { Bench } = require('tinybench'); +const Driver = require('../lib/connection.js'); +const conn = new Driver(process.argv[process.argv.length - 1], { + lazyConnect: true +}); +const {format} = require('node:util'); +const {noop} = require('lodash'); -conn.connect() - .then(function(){ - suite.add('insert', {defer: true, fn: function(defer){ - conn.insert('bench', [c++, {user: 'username', data: 'Some data.'}]) - .then(function(){defer.resolve();}) - .catch(function(e){ - console.error(e, e.stack); - defer.reject(e); - }); - }}); +/** + * Truncates a space by deleting all records + * @async + * @param {string} spaceName - Name of the space to truncate + * @returns {Promise} + */ +const truncateSpace = async (spaceName) => { + return conn.sql(`DELETE FROM "${spaceName}" INDEXED BY "tree_idx" WHERE true`); +}; + +/** + * Truncates all benchmark spaces + * @async + * @returns {Promise} + */ +const truncateAll = async () => { + return Promise.all([ + truncateSpace('bench_memtx'), + truncateSpace('bench_vinyl') + ]) +}; + +/** + * Awaits for all sent commands to be drained + * @async + * @returns {Promise} + */ +const awaitCommandsDrain = async () => { + return conn._awaitSentCommandsDrain().catch(noop); +}; - suite.add('non-deferred insert', {defer: false, fn: function(){ - conn.insert('bench', [c++, {user: 'username', data: 'Some data.'}]) - .catch(function(e){ - console.error(e); - }); - }}); +const bench = new Bench({ + name: 'write benchmark', + iterations: 10000, + setup: (task) => { + console.info( + format('starting task "%s"', task.name) + ); + }, + warmup: false, + threshold: 1, + concurrency: null +}); - suite.add('insert to vinyl', {defer: true, fn: function(defer){ - conn.insert('bench_vinyl', [c++, {user: 'username', data: 'Some data.'}]) - .then(function(){defer.resolve();}) - .catch(function(e){ - console.error(e, e.stack); - defer.reject(e); - }); - }}); - - suite.add('insert parallel 50', {defer: true, fn: function(defer){ - try{ - promises = []; - for (let l=0;l<50;l++){ - promises.push(conn.insert('bench', [c++, {user: 'username', data: 'Some data.'}])); - } - var chain = Promise.all(promises); - chain.then(function(){ defer.resolve(); }) - .catch(function(e){ - console.error(e, e.stack); - defer.reject(e); - }); - } catch(e){ - defer.reject(e); - console.error(e, e.stack); - } - }}); +bench.addEventListener('cycle', (evt) => { + const result = evt.task.result; + if (result.state != 'completed') return; - suite.add('pipelined insert by 10', {defer: true, fn: function(defer){ - var pipelinedConn = conn.pipeline() + console.info(`\x1b[36mCompleted task\x1b[0m "${evt.task.name}":`); + console.info(` Latency: ${result.latency.mean.toFixed(5)} ms (min: ${result.latency.min.toFixed(5)}, max: ${result.latency.max.toFixed(5)})`); + console.info(` Throughput: ${Math.floor(result.throughput.mean)} ops/s`); +}); - for (var i=0;i<10;i++) { - pipelinedConn.insert('bench', [c++, {user: 'username', data: 'Some data.'}]) - } +let counter = 0; +let preparedStmt; +const sqlStmt = `INSERT INTO "bench_memtx" VALUES (?, ?)`; +const pipelinedTaskName = '[pipelined] - non-deferred insert by 20'; + +conn.connect() +.then(() => { + return truncateAll(); +}) + .then(async () => { + bench + // memtx + .add( + '[memtx] - insert', + () => { + const prevC = counter; + counter++; + const nextC = counter; + return conn.insert('bench_memtx', [prevC, [prevC, nextC]]); + }, + { + async: true, + afterAll: awaitCommandsDrain + } + ) + .add('[memtx] - non-deferred insert', () => { + const prevC = counter; + counter++; + const nextC = counter; + conn.insert('bench_memtx', [prevC, [prevC, nextC]]); + }, { + async: false, + afterAll: async () => { + await conn._awaitSentCommandsDrain().catch(noop); // prevent unexpected race conditions of non-deferred task + return truncateSpace('bench_memtx') + } + }) - pipelinedConn.exec() - .then(function(){ defer.resolve(); }) - .catch(function(e){ defer.reject(e); }) - }}); + // vinyl + .add('[vinyl] - insert', () => { + const prevC = counter; + counter++; + const nextC = counter; + return conn.insert('bench_vinyl', [prevC, [prevC, nextC]]); + }, { + async: true, + beforeAll: () => counter = 0, + afterAll: awaitCommandsDrain + }) + .add('[vinyl] - non-deferred insert', () => { + const prevC = counter; + counter++; + const nextC = counter; + conn.insert('bench_vinyl', [prevC, [prevC, nextC]]); + }, { + async: false, + afterAll: async () => { + counter = 0; + await conn._awaitSentCommandsDrain().catch(noop); + } + }) - suite.add('pipelined insert by 50', {defer: true, fn: function(defer){ - var pipelinedConn = conn.pipeline() + // SQL + .add(`[SQL] - insert (memtx)`, () => { + const prevC = counter; + counter++; + const nextC = counter; + return conn.sql(sqlStmt, [prevC, [prevC, nextC]]); + }, { + async: true, + afterAll: awaitCommandsDrain + }) + .add(`[SQL] - non-deferred insert (memtx)`, () => { + const prevC = counter; + counter++; + const nextC = counter; + conn.sql(sqlStmt, [prevC, [prevC, nextC]]); + }, { + async: false, + afterAll: awaitCommandsDrain + }) + .add(`[SQL] - non-deferred prepared insert (memtx)`, () => { + const prevC = counter; + counter++; + const nextC = counter; + conn.sql(preparedStmt, [prevC, [prevC, nextC]]); + }, { + async: false, + beforeAll: async () => { + preparedStmt = await conn.prepare(sqlStmt); + }, + afterAll: async () => { + counter = 0; + await conn._awaitSentCommandsDrain().catch(noop); // prevent unexpected race conditions of non-deferred task + return truncateSpace('bench_memtx') + } + }) - for (var i=0;i<50;i++) { - pipelinedConn.insert('bench', [c++, {user: 'username', data: 'Some data.'}]) - } + // pipeline + const pipelinedConn = conn.pipeline(); // it is possible to create the pipelined instance only once and reuse it in future + bench + .add('[pipeline] - non-deferred autopipelined insert', () => { + const prevC = counter; + counter++; + const nextC = counter; + conn.insert('bench_memtx', [prevC, [prevC, nextC]]); + }, { + async: false, + beforeAll: () => conn.options.enableAutoPipelining = true, + afterAll: () => conn.options.enableAutoPipelining = false + }) + .add(pipelinedTaskName, () => { + for (let i = 0; i < 20; i++) { + const prevC = counter; + counter++; + const nextC = counter; + pipelinedConn.insert('bench_memtx', [prevC, [prevC, nextC]]); + } + pipelinedConn.exec(); + }, { + async: false + }); - pipelinedConn.exec() - .then(function(){ defer.resolve(); }) - .catch(function(e){ defer.reject(e); }) - }}); + return bench.run(); + }) + .then(() => { + // pipelined benchmark measures loops by default, but we need to count exact 'insert' requests + const task = bench.getTask(pipelinedTaskName); + if (task && task.result) { + const throughput = task.result.throughput; + throughput.min = throughput.min * 20; + throughput.mean = throughput.mean * 20; + throughput.max = throughput.max * 20; + throughput.p50 = throughput.p50 * 20; + } - suite - .on('cycle', function(event) { - console.log(String(event.target)); - }) - .on('complete', function() { - conn.eval('return clear()') - .then(function(){ - console.log('complete'); - process.exit(); - }) - .catch(function(e){ - console.error(e); - }); - }) - .run({ 'async': true, 'queued': true }); - }); \ No newline at end of file + console.info(`Benchmark "${bench.name}" finished, results: `, bench.table()) + }) + .catch(function (e) { + console.error('bench failed: ', e); + }) + .finally(async () => { + await conn.disconnect(); + process.exit(); + }); \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..fe16418 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,114 @@ +const { rules } = require('@eslint/js').configs.recommended; + +module.exports = [ + // Configuration for JavaScript files + { + files: ['lib/*'], + languageOptions: { + ecmaVersion: 2024, + sourceType: 'commonjs', + globals: { + // Node.js globals + global: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + console: 'readonly', + setImmediate: 'readonly', + clearImmediate: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + }, + }, + rules: { + ...rules, // use recommended defaults + // Error prevention rules + 'block-scoped-var': 'error', + 'no-cond-assign': 'error', + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-dupe-args': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-ex-assign': 'error', + 'no-extra-semi': 'error', + 'no-func-assign': 'error', + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-obj-calls': 'error', + 'no-redeclare': 'error', + 'no-regex-spaces': 'error', + 'no-sparse-arrays': 'error', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'error', + 'no-delete-var': 'error', + 'no-shadow': [ + 'error', + { builtinGlobals: false, hoist: 'all', allow: [] }, + ], + 'no-undef': 'error', + 'use-isnan': 'error', + 'valid-typeof': 'error', + + // Code quality and best practices + 'no-eval': 'error', + 'no-implied-eval': 'error', + 'no-with': 'error', + 'no-multi-spaces': 'error', + 'no-multiple-empty-lines': [ + 'error', + { max: 2, maxBOF: 0, maxEOF: 0 }, + ], + 'no-trailing-spaces': 'error', + 'eol-last': ['error', 'always'], + semi: ['error', 'always'], + indent: ['error', 4, { SwitchCase: 1 }], + quotes: ['error', 'single', { avoidEscape: true }], + 'comma-dangle': ['error', 'never'], + 'no-console': [ + 'warn', + { allow: ['warn', 'error', 'table', 'info'] }, + ], + + // Security-related rules + 'no-new-func': 'error', + 'no-script-url': 'error', + + // Performance considerations + 'no-constant-condition': 'warn', + 'no-unused-expressions': 'error', + 'no-useless-escape': 'warn', + 'no-useless-catch': 'error', + 'no-useless-return': 'error', + + // Readability improvements + curly: ['error', 'all'], + 'brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'keyword-spacing': 'error', + 'space-before-blocks': 'error', + 'space-before-function-paren': [ + 'error', + { anonymous: 'always', named: 'never', asyncArrow: 'always' }, + ], + 'space-infix-ops': 'error', + 'space-unary-ops': 'error', + 'prefer-const': 'warn', + 'no-var': 'error', + }, + }, + // Configuration for test files + { + files: ['**/test/**/*.js', '**/*.js', '**/*.spec.js'], + rules: { + 'no-console': 'off', + 'no-unused-expressions': 'off', + }, + }, + // Ignore certain patterns + { + ignores: ['node_modules/**', 'coverage/**', '.git/**', '*.log'], + }, +]; diff --git a/lib/Command.js b/lib/Command.js new file mode 100644 index 0000000..c938c3f --- /dev/null +++ b/lib/Command.js @@ -0,0 +1,140 @@ +const { prototype } = require('./Commander'); +const { + RequestCode, + symbols: { begin: beginSym, commit: commitSym, rollback: rollbackSym } +} = require('./const'); + +const cmds = [ + { + flags: ['readonly'], + rqCode: RequestCode.rqSelect, + function: prototype.select, + argsLen: 7, + methodName: 'select' + }, + { + flags: ['write'], + rqCode: RequestCode.rqInsert, + function: prototype.insert, + argsLen: 3, + methodName: 'insert' + }, + { + flags: ['no_auth', 'readonly'], + rqCode: RequestCode.rqPing, + function: prototype.ping, + argsLen: 1, + methodName: 'ping' + }, + { + flags: ['no_auth', 'readonly'], + rqCode: RequestCode.rqAuth, + function: prototype._auth, + argsLen: 3, + methodName: '_auth' + }, + { + flags: ['transaction'], + rqCode: RequestCode.rqBegin, + function: prototype[beginSym], + argsLen: 3, + methodName: beginSym + }, + { + flags: ['transaction'], + rqCode: RequestCode.rqCommit, + function: prototype[commitSym], + argsLen: 1, + methodName: commitSym + }, + { + flags: ['transaction'], + rqCode: RequestCode.rqRollback, + function: prototype[rollbackSym], + argsLen: 1, + methodName: rollbackSym + }, + { + flags: ['write'], + rqCode: RequestCode.rqDelete, + function: prototype.delete, + argsLen: 4, + methodName: 'delete' + }, + { + flags: ['write'], + rqCode: RequestCode.rqUpdate, + function: prototype.update, + argsLen: 5, + methodName: 'update' + }, + { + flags: ['write'], + rqCode: RequestCode.rqUpsert, + function: prototype.upsert, + argsLen: 4, + methodName: 'upsert' + }, + { + flags: ['script'], + rqCode: RequestCode.rqEval, + function: prototype.eval, + argsLen: 3, + methodName: 'eval' + }, + { + flags: ['script'], + rqCode: RequestCode.rqCall, + function: prototype.call, + argsLen: 3, + methodName: 'call' + }, + { + flags: ['script', 'sql'], + rqCode: RequestCode.rqExecute, + function: prototype.sql, + argsLen: 3, + methodName: 'sql' + }, + { + flags: ['script', 'sql'], + rqCode: RequestCode.rqPrepare, + function: prototype.prepare, + argsLen: 2, + methodName: 'prepare' + }, + { + flags: ['readonly', 'no_auth'], + rqCode: RequestCode.rqId, + function: prototype.id, + argsLen: 4, + methodName: 'id' + }, + { + flags: ['write'], + rqCode: RequestCode.rqReplace, + function: prototype.replace, + argsLen: 3, + methodName: 'replace' + } +]; +// calculate 'opts' position in advance +cmds.map((obj) => { + obj.optsPos = obj.argsLen - 1; +}); + +module.exports = class Commands { + static list = cmds.map((obj) => obj.methodName); + static commands = Object.fromEntries( + cmds.map((obj) => [obj.methodName, obj]) + ); + static commandsNum = Object.fromEntries( + cmds.map((obj) => [obj.rqCode, obj]) + ); + + constructor() {} + + static hasFlag(cmdName, flag) { + return Commands.commands[cmdName]?.flags?.includes(flag); + } +}; diff --git a/lib/Commander.js b/lib/Commander.js new file mode 100644 index 0000000..4f92c34 --- /dev/null +++ b/lib/Commander.js @@ -0,0 +1,1119 @@ +const { createHash } = require('crypto'); +const { + KeysCode, + RequestCode, + IteratorsType, + Iterators, + passEnter, + symbols: { + streamId: streamIdSym, + bypassOfflineQueue: bypassOfflineQueueSym, + begin: beginSym, + commit: commitSym, + rollback: rollbackSym + } +} = require('./const'); +const { TarantoolError } = require('./errors'); +const { deprecate } = require('node:util'); +const PreparedStatement = require('./PreparedStatement'); +const SliderBuffer = require('./utils/SliderBuffer'); + +const bufferCache = new Map(); +const maxSmi = 2147483647; // max 32-bit integer + +const schemaFetchOptions = { + tupleToObject: false, + autoPipeline: false, + [bypassOfflineQueueSym]: true +}; + +/** + * Helper function to handle parameter validation with callback + * @private + * @param {*} parameter - The parameter to check (typically a spaceId or indexId) + * @param {Function} [cb] - Callback function + * @returns {*|false|Promise} Returns the parameter value, false if error was handled via callback, or rejects promise with the error + */ +const checkParameterError = (parameter, cb) => { + if (parameter instanceof TarantoolError) { + if (cb) { + cb(parameter, null); + return false; // Signal that error was handled and function should stop execution + } else { + return Promise.reject(parameter); + } + } + return parameter; +}; + +let shouldSetImmediate = true; +/** + * Clears the msgpack buffer cache after the current event loop iteration + * to free up memory and prevent a possible OOM error + * @private + */ +const clearCacheAfterEventLoop = () => { + if (shouldSetImmediate) { + shouldSetImmediate = false; + setImmediate(() => { + bufferCache.clear(); + shouldSetImmediate = true; + }); + } +}; + +const ERR_SCHEMA_NOT_FETCHED = new TarantoolError( + 'Space schema was not fetched, so you cannot specify space or index by their corresponding name. Call \'fetchSchema()\' or set \'prefetchSchema\' option to \'true\'' +); + +module.exports = class Commander { + schemaFetched = false; + namespace = {}; + _id = [1]; + + /** + * Creates an instance of Commander + * @param {Object} opts - Configuration options + */ + constructor(opts) { + this.options = opts || {}; + this.bufferPool = new SliderBuffer(this.options.sliderBufferInitialSize || Buffer.poolSize); + } + + /** + * Creates a buffer from the buffer pool + * @private + * @param {number} size - Size of the buffer to create + * @returns {Buffer} A buffer of specified size + */ + _createBuffer(size) { + return this.bufferPool.getBuffer(size); + } + + /** + * Gets a cached msgpack buffer for a given value + * @private + * @param {*} value - Value to encode + * @returns {Buffer} Encoded msgpack buffer + */ + getCachedMsgpackBuffer(value) { + const search = bufferCache.get(value); + if (search) { + return search; + } else { + const encoded = this.msgpacker.encode(value); + bufferCache.set(value, encoded); + clearCacheAfterEventLoop(); + return encoded; + } + } + + /** + * @public + * Fetches the database schema (spaces and indexes) + * @returns {Promise} Namespace with spaces and indexes information + */ + fetchSchema() { + return this.fetchSpaces() + .then(() => this.fetchIndexes()) + .then(() => { + this.schemaFetched = true; + return this.namespace; + }); + } + + /** + * Fetches spaces from the database + * @private + * @returns {Promise} Namespace with space information + */ + fetchSpaces() { + return this.pendingPromises.fetchSpaces ||= this.select(281, 0, 99999, 0, 'all', [], schemaFetchOptions) + .then((result) => { + result.map((arr) => { + const id = arr[0]; + const name = arr[2]; + const format = arr[6]; + const tupleFields = {}; + let n = 0; + format.map((obj) => { + tupleFields[n] = tupleFields[obj.name] = { + type: obj.type, + name: obj.name, + id: n + }; + n++; + }); + + this.namespace[name] = this.namespace[id] = { + id, + name, + engine: arr[3], + tupleKeys: format.map((obj) => obj.name), + indexes: {}, + fields: tupleFields + }; + }); + + return this.namespace; + }) + .finally(() => { + this.pendingPromises.fetchSpaces = null; + }); + } + + /** + * Fetches indexes from the database + * @private + * @returns {Promise} Namespace with index information + */ + fetchIndexes() { + return this.pendingPromises.fetchIndexes ||= this.select(289, 0, 99999, 0, 'all', [], schemaFetchOptions) + .then((result) => { + result.map((arr) => { + const spaceId = arr[0]; + const indexId = arr[1]; + const name = arr[2]; + this.namespace[spaceId].indexes[indexId] = name; + this.namespace[spaceId].indexes[name] = indexId; + }); + + this.pendingPromises.fetchIndexes = null; + + return this.namespace; + }) + .finally(() => { + this.pendingPromises.fetchIndexes = null; + }); + } + + /** + * Gets the space ID from the namespace by name + * @private + * @param {string|number} name - Space name or ID + * @returns {number|TarantoolError} Space ID or error object if space is not found + */ + _getSpaceId(name) { + if (this.offlineQueue.enabled) {return name;} // bypass temporary + if (!this.schemaFetched) {return ERR_SCHEMA_NOT_FETCHED;} + if (!this.namespace?.[name]) {return new TarantoolError(`Space "${name}" does not exist or user has no "read" permission`);} + + return this.namespace[name].id; + } + + /** + * Gets the index ID from the namespace by space and index name + * @private + * @param {number} space - Space ID + * @param {string|number} index - Index name or ID + * @returns {number|TarantoolError} Index ID or error object if index is not found + */ + _getIndexId(space, index) { + if (this.offlineQueue.enabled) {return index;} // bypass temporary + if (!this.schemaFetched) {return ERR_SCHEMA_NOT_FETCHED;} + if (!(index in this.namespace[space].indexes)) { + return new TarantoolError( + `Index "${index}" does not exist in space with id №${space} or user does not have "read" permission` + ); + } + + return this.namespace[space].indexes[index]; + } + + /** + * Gets the next request ID + * @private + * @returns {number} Request ID + */ + _getRequestId() { + const _id = this._id; + if (_id[0] > maxSmi) {_id[0] = 0;} + return _id[0]++; + } + + /** + * Performs a SELECT query on the database + * @public + * @param {number|string} spaceId - Space ID or name + * @param {number|string} indexId - Index ID or name + * @param {number} limit - Maximum number of records to return + * @param {number} [offset=0] - Number of records to skip + * @param {string} [iterator='eq'] - Iterator type + * @param {Array} [key] - Search key + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and result (or null) + * @returns {Promise|undefined} Selected records wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + select( + spaceId, + indexId, + limit, + offset, + iterator, + key, + opts, + cb + ) { + limit ||= 1; // consider there will be no practical usage to select zero records, so setting the default to 1 in order to prevent developer's mistakes :) + offset ||= 0; // consider using 'logical OR assignment' instead of default function parameters (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters) because the last one replaces value only if it set to 'undefined' (despite the fact that there can also be passed falsy values like '0', 'null', 'false') + iterator ||= Iterators.EQ; + opts ||= {}; + if (typeof spaceId == 'string') {spaceId = this._getSpaceId(spaceId);} + // Check for errors using helper function to avoid code duplication + spaceId = checkParameterError(spaceId, cb); + if (spaceId === false || spaceId instanceof Promise) {return spaceId;} // false = error handled via callback, Promise = Promise.reject() + if (typeof indexId == 'string') {indexId = this._getIndexId(spaceId, indexId);} + indexId = checkParameterError(indexId, cb); + if (indexId === false || indexId instanceof Promise) {return indexId;} + + if (iterator == Iterators.ALL) {key = [];} + if (!Array.isArray(key)) {key = [key];} + + const bufKey = this.msgpacker.encode(key); + const len = 37 + bufKey.length; + const buffer = this._createBuffer(len + 5, opts); + + const reqId = this._getRequestId(); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqSelect; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x86; + buffer[21] = KeysCode.space_id; + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = KeysCode.limit; + buffer[28] = 0xce; + buffer.writeUInt32BE(limit, 29); + buffer[33] = KeysCode.offset; + buffer[34] = 0xce; + buffer.writeUInt32BE(offset, 35); + buffer[39] = KeysCode.iterator; + buffer.writeUInt8(IteratorsType[iterator], 40); + buffer[41] = KeysCode.key; + bufKey.copy(buffer, 42); + + return this.sendCommand( + RequestCode.rqSelect, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Performs a SELECT query with callback style + * @public + * @deprecated Use select() with callback parameter instead + * @param {number|string} spaceId - Space ID or name + * @param {number|string} indexId - Index ID or name + * @param {number} limit - Maximum number of records to return + * @param {number} [offset=0] - Number of records to skip + * @param {string} [iterator='eq'] - Iterator type + * @param {Array} [key] - Search key + * @param {Function} successCb - Success callback + * @param {Function} errorCb - Error callback + * @param {Object} [opts={}] - Options + * @returns {undefined} + */ + selectCb( + spaceId, + indexId, + limit, + offset, + iterator, + key, + successCb, + errorCb, + opts + ) { + offset ||= 0; + iterator ||= Iterators.EQ; + opts ||= {}; + return this.select( + spaceId, + indexId, + limit, + offset, + iterator, + key, + opts, + (error, success) => { + if (error) {return errorCb(error);} + + return successCb(success); + } + ); + } + + /** + * Sends a PING command to the server + * @public + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and result (or null) + * @returns {Promise|undefined} Server response wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + ping(opts, cb) { + opts ||= {}; + const reqId = this._getRequestId(); + const len = 9; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x82; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqPing; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + + return this.sendCommand( + RequestCode.rqPing, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Begins a transaction + * @public + * @param {number} [transTimeoutSec=60] - Transaction timeout in seconds + * @param {number} [isolationLevel=0] - Transaction isolation level + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function + * @returns {Promise} + */ + [beginSym](transTimeoutSec, isolationLevel, opts, cb) { + transTimeoutSec ||= 60; + isolationLevel ||= 0; + opts ||= {}; + + const reqId = this._getRequestId(); + // transTimeoutSec should be decimal number in order to be properly encoded by msgpacker + if (Number.isInteger(transTimeoutSec)) {transTimeoutSec += 0.1;} + const transTimeoutBuf = this.msgpacker.encode(transTimeoutSec); + + const len = 19 + transTimeoutBuf.length; + const buffer = this._createBuffer(5 + len, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqBegin; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x82; + buffer[21] = KeysCode.iproto_txn_isolation; + buffer.writeUInt8(isolationLevel, 22); + buffer[23] = KeysCode.iproto_timeout; + transTimeoutBuf.copy(buffer, 24); + + return this.sendCommand( + RequestCode.rqBegin, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Commits a transaction + * @public + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function + * @returns {Promise} + */ + [commitSym](opts, cb) { + opts ||= {}; + const reqId = this._getRequestId(); + + const len = 15; + const buffer = this._createBuffer(5 + len, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqCommit; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + + return this.sendCommand( + RequestCode.rqCommit, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Rolls back a transaction + * @public + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function + * @returns {Promise} + */ + [rollbackSym](opts, cb) { + opts ||= {}; + const reqId = this._getRequestId(); + + const len = 15; + const buffer = this._createBuffer(5 + len, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqRollback; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym], 16); + + return this.sendCommand( + RequestCode.rqRollback, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Deletes a tuple from the database + * @public + * @param {number|string} spaceId - Space ID or name + * @param {number|string} indexId - Index ID or name + * @param {Array} key - Delete key + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and deleted tuple (or null) + * @returns {Promise|undefined} Deleted tuple wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + delete(spaceId, indexId, key, opts, cb) { + opts ||= {}; + if (typeof spaceId == 'string') {spaceId = this._getSpaceId(spaceId);} + spaceId = checkParameterError(spaceId, cb); + if (spaceId === false || spaceId instanceof Promise) {return spaceId;} + + if (typeof indexId == 'string') {indexId = this._getIndexId(spaceId, indexId);} + indexId = checkParameterError(indexId, cb); + if (indexId === false || indexId instanceof Promise) {return indexId;} + + const reqId = this._getRequestId(); + const bufKey = this.msgpacker.encode(key); + + const len = 23 + bufKey.length; + const buffer = this._createBuffer(5 + len, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqDelete; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x83; + buffer.writeUInt8(KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = KeysCode.key; + bufKey.copy(buffer, 28); + + return this.sendCommand( + RequestCode.rqDelete, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Formats operations array by converting field names to their corresponding IDs + * Makes msgpack buffers even more compact + * @private + * @param {Array} arr - Operations array + * @param {number} spaceId - Space ID + * @returns {Array} Formatted operations array + */ + #formatOpsArr = (arr, spaceId) => { + return arr.map((value) => { + const name = value[1]; + const id = this.namespace[spaceId].fields[name]?.id; + if (typeof name === 'string' && id) { + value[1] = id; + } + + return value; + }); + }; + + /** + * Updates tuple in the database + * @public + * @param {number|string} spaceId - Space ID or name + * @param {number|string} indexId - Index ID or name + * @param {Array} key - Index value + * @param {Array} ops - Update operations + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and updated tuple (or null) + * @returns {Promise|undefined} Updated tuple wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + update(spaceId, indexId, key, ops, opts, cb) { + opts ||= {}; + if (typeof spaceId == 'string') {spaceId = this._getSpaceId(spaceId);} + spaceId = checkParameterError(spaceId, cb); + if (spaceId === false || spaceId instanceof Promise) {return spaceId;} + + if (typeof indexId == 'string') {indexId = this._getIndexId(spaceId, indexId);} + indexId = checkParameterError(indexId, cb); + if (indexId === false || indexId instanceof Promise) {return indexId;} + + const reqId = this._getRequestId(); + const bufKey = this.msgpacker.encode(key); + ops = this.#formatOpsArr(ops, spaceId); + const bufOps = this.msgpacker.encode(ops); + + const len = 24 + bufKey.length + bufOps.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqUpdate; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x84; + buffer.writeUInt8(KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = KeysCode.index_id; + buffer.writeUInt8(indexId, 26); + buffer[27] = KeysCode.key; + bufKey.copy(buffer, 28); + buffer[28 + bufKey.length] = KeysCode.tuple; + bufOps.copy(buffer, 29 + bufKey.length); + + return this.sendCommand( + RequestCode.rqUpdate, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Performs an upsert operation (update or insert) + * @public + * @param {number|string} spaceId - Space ID or name + * @param {Array} ops - Update operations + * @param {Array} tuple - Tuple to insert if update fails + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and result tuple (or null) + * @returns {Promise|undefined} Result tuple wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + upsert(spaceId, ops, tuple, opts, cb) { + opts ||= {}; + if (typeof spaceId == 'string') {spaceId = this._getSpaceId(spaceId);} + spaceId = checkParameterError(spaceId, cb); + if (spaceId === false || spaceId instanceof Promise) {return spaceId;} + + const reqId = this._getRequestId(); + const bufTuple = this.msgpacker.encode(tuple); + ops = this.#formatOpsArr(ops, spaceId); + const bufOps = this.msgpacker.encode(ops); + + const len = 22 + bufTuple.length + bufOps.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqUpsert; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x83; + buffer.writeUInt8(KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = KeysCode.tuple; + bufTuple.copy(buffer, 26); + buffer[26 + bufTuple.length] = KeysCode.def_tuple; + bufOps.copy(buffer, 27 + bufTuple.length); + + return this.sendCommand( + RequestCode.rqUpsert, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Evaluates a Lua expression on the server + * @public + * @param {string} expression - Lua expression to evaluate + * @param {Array} [args=[]] - Arguments to pass to the expression + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and expression result (or null) + * @returns {Promise<*>|undefined} Result of the expression wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + eval(expression, tuple, opts, cb) { + tuple ||= []; + opts ||= {}; + const reqId = this._getRequestId(); + const bufExp = this.getCachedMsgpackBuffer(expression); + const bufTuple = this.msgpacker.encode(tuple); + const len = 18 + bufExp.length + bufTuple.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqEval; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(KeysCode.expression, 21); + bufExp.copy(buffer, 22); + buffer[22 + bufExp.length] = KeysCode.tuple; + bufTuple.copy(buffer, 23 + bufExp.length); + + return this.sendCommand( + RequestCode.rqEval, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Calls a stored procedure on the server + * @public + * @param {string} functionName - Function name to call + * @param {Array} [tuple=[]] - Arguments to pass to the function + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and function result (or null) + * @returns {Promise<*>|undefined} Result of the function wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + call(functionName, tuple, opts, cb) { + tuple ||= []; + opts ||= {}; + const reqId = this._getRequestId(); + const bufName = this.getCachedMsgpackBuffer(functionName); + const bufTuple = this.msgpacker.encode(tuple); + const len = 18 + bufName.length + bufTuple.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqCall; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(KeysCode.function_name, 21); + bufName.copy(buffer, 22); + buffer[22 + bufName.length] = KeysCode.tuple; + bufTuple.copy(buffer, 23 + bufName.length); + + return this.sendCommand( + RequestCode.rqCall, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Executes an SQL query + * @public + * @param {string|PreparedStatement} query - SQL query or prepared statement + * @param {Array} [bindParams=[]] - Bind parameters for prepared statements + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and query result (or null) + * @returns {Promise|undefined} Query result wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + sql(query, bindParams, opts, cb) { + bindParams ||= []; + opts ||= {}; + const reqId = this._getRequestId(); + const bufParams = this.msgpacker.encode(bindParams); + const isPreparedStatement = query instanceof PreparedStatement; + const bufQuery = isPreparedStatement + ? this.getCachedMsgpackBuffer(query.stmt_id) + : this.msgpacker.encode(query); // cache only prepared queries, considering them to be frequently used + + const len = 18 + bufQuery.length + bufParams.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqExecute; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8( + isPreparedStatement ? KeysCode.stmt_id : KeysCode.sql_text, + 21 + ); + bufQuery.copy(buffer, 22); + buffer[22 + bufQuery.length] = KeysCode.sql_bind; + bufParams.copy(buffer, 23 + bufQuery.length); + + return this.sendCommand( + RequestCode.rqExecute, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Prepares an SQL statement + * @public + * @param {string} query - SQL query to prepare + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and prepared statement (or null) + * @returns {Promise|undefined} Prepared statement wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + prepare(query, opts, cb) { + opts ||= {}; + const reqId = this._getRequestId(); + const bufQuery = this.msgpacker.encode(query); + + const len = 13 + bufQuery.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x82; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqPrepare; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = 0x82; + buffer.writeUInt8(KeysCode.sql_text, 15); + bufQuery.copy(buffer, 16); + + return this.sendCommand( + RequestCode.rqPrepare, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Sends an ID (handshake) command to negotiate protocol version and features + * @public + * @param {number} [version=3] - Protocol version + * @param {Array} [features=[1]] - Supported features + * @param {string} [auth_type='chap-sha1'] - Authentication type + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and server response (or null) + * @returns {Promise|undefined} Server response wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + id(version, features, auth_type, opts, cb) { + version ||= 3; + features ||= [1]; + auth_type ||= 'chap-sha1'; + // eslint-disable-next-line no-useless-assignment + opts ||= {}; + const reqId = this._getRequestId(); + + const headersMap = new Map(); + headersMap.set(KeysCode.code, RequestCode.rqId); + headersMap.set(KeysCode.sync, reqId); + const headersBuffer = this.msgpacker.encode(headersMap); + + const bodyMap = new Map(); + bodyMap.set(KeysCode.iproto_version, version); + bodyMap.set(KeysCode.iproto_features, features); + bodyMap.set(KeysCode.iproto_auth_type, auth_type); + const bodyBuffer = this.msgpacker.encode(bodyMap); + + const dataLengthBuffer = this.msgpacker.encode( + headersBuffer.length + bodyBuffer.length + ); + const concatenatedBuffers = Buffer.concat([ + dataLengthBuffer, + headersBuffer, + bodyBuffer + ]); + + return this.sendCommand( + RequestCode.rqId, + reqId, + concatenatedBuffers, + cb, + arguments + ); + } + + /** + * Inserts a tuple into the database + * @public + * @param {number|string} spaceId - Space ID or name + * @param {Array} tuple - Tuple to insert + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and inserted record (or null) + * @returns {Promise|undefined} Inserted record wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + insert(spaceId, tuple, opts, cb) { + opts ||= {}; + if (typeof spaceId == 'string') {spaceId = this._getSpaceId(spaceId);} + spaceId = checkParameterError(spaceId, cb); + if (spaceId === false || spaceId instanceof Promise) {return spaceId;} + + const reqId = this._getRequestId(); + const bufTuple = this.msgpacker.encode(tuple); + const len = 21 + bufTuple.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqInsert; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = KeysCode.tuple; + bufTuple.copy(buffer, 26); + + return this.sendCommand( + RequestCode.rqInsert, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Replaces a record in the database + * @public + * @param {number|string} spaceId - Space ID or name + * @param {Array} tuple - Tuple to replace + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function (error, result). Receives two arguments: error (or null) and replaced record (or null) + * @returns {Promise|undefined} Replaced record wrapped in a Promise if no callback provided. Returns undefined when callback is provided + */ + replace(spaceId, tuple, opts, cb) { + opts ||= {}; + if (typeof spaceId == 'string') {spaceId = this._getSpaceId(spaceId);} + spaceId = checkParameterError(spaceId, cb); + if (spaceId === false || spaceId instanceof Promise) {return spaceId;} + + const reqId = this._getRequestId(); + const bufTuple = this.msgpacker.encode(tuple); + const len = 21 + bufTuple.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x83; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqReplace; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = KeysCode.iproto_stream_id; + buffer[15] = 0xce; + buffer.writeUInt32BE(opts[streamIdSym] || 0, 16); + buffer[20] = 0x82; + buffer.writeUInt8(KeysCode.space_id, 21); + buffer[22] = 0xcd; + buffer.writeUInt16BE(spaceId, 23); + buffer[25] = KeysCode.tuple; + bufTuple.copy(buffer, 26); + + return this.sendCommand( + RequestCode.rqReplace, + reqId, + buffer, + cb, + arguments + ); + } + + /** + * Authenticates with the database server + * @public + * @param {string} username - Username + * @param {string} password - Password + * @param {Object} [opts={}] - Options + * @param {Function} [cb] - Callback function + * @returns {Promise} + */ + _auth(username, password, opts, cb) { + opts ||= {}; + const reqId = this._getRequestId(); + + const user = this.msgpacker.encode(username); + const scrambled = scramble(password, this.salt); + const len = 44 + user.length; + const buffer = this._createBuffer(len + 5, opts); + + buffer[0] = 0xce; + buffer.writeUInt32BE(len, 1); + buffer[5] = 0x82; + buffer[6] = KeysCode.code; + buffer[7] = RequestCode.rqAuth; + buffer[8] = KeysCode.sync; + buffer[9] = 0xce; + buffer.writeUInt32BE(reqId, 10); + buffer[14] = 0x82; + buffer.writeUInt8(KeysCode.username, 15); + user.copy(buffer, 16); + buffer[16 + user.length] = KeysCode.tuple; + buffer[17 + user.length] = 0x92; + passEnter.copy(buffer, 18 + user.length); + buffer[28 + user.length] = 0xb4; + scrambled.copy(buffer, 29 + user.length); + + return this.sendCommand( + RequestCode.rqAuth, + reqId, + buffer, + cb, + arguments + ); + } +}; + +/** + * Transforms password using SHA1 hash + * @private + * @param {string|Buffer} t - Data to hash + * @returns {Buffer} SHA1 hash + */ +const shatransform = (t) => createHash('sha1').update(t).digest(); + +/** + * XOR operation between two buffers + * @private + * @param {Buffer|string} a - First operand + * @param {Buffer|string} b - Second operand + * @returns {Buffer} Result of XOR operation + */ +const xor = (a, b) => { + if (!Buffer.isBuffer(a)) {a = Buffer.from(a);} + if (!Buffer.isBuffer(b)) {b = Buffer.from(b);} + const res = []; + let i; + if (a.length > b.length) { + for (i = 0; i < b.length; i++) { + res.push(a[i] ^ b[i]); + } + } else { + for (i = 0; i < a.length; i++) { + res.push(a[i] ^ b[i]); + } + } + return Buffer.from(res); +}; + +/** + * Scrambles a password for authentication + * @private + * @param {string} password - Password to scramble + * @param {string} salt - Salt in base64 format + * @returns {Buffer} Scrambled password + */ +const scramble = (password, salt) => { + const encSalt = Buffer.from(salt, 'base64'); + const step1 = shatransform(password); + const step2 = shatransform(step1); + const step3 = shatransform(Buffer.concat([encSalt.subarray(0, 20), step2])); + return xor(step1, step3); +}; + +module.exports.prototype.selectCb = deprecate( + module.exports.prototype.selectCb, + `'selectCb()' method is deprecated. Use 'select( + spaceId, + indexId, + limit, + offset, + iterator, + key, + opts, + (error, success) => { /* process the result as usual */ } +)' instead.`, + 'Tarantool-Driver' +); diff --git a/lib/MsgPack.js b/lib/MsgPack.js new file mode 100644 index 0000000..caf6f6f --- /dev/null +++ b/lib/MsgPack.js @@ -0,0 +1,300 @@ +const { Packr, Unpackr, UnpackrStream, addExtension } = require('msgpackr'); +const { parse: uuidParse, stringify: uuidStringify } = require('uuid'); + +const encoder = new Packr({ + variableMapSize: true, + useRecords: false, + encodeUndefinedAsNil: true +}); + +const decoder = new Unpackr({ + mapsAsObjects: true, + int64AsType: 'auto' +}); + +/** + * Packs an integer value, ensuring it's encoded correctly + * @public + * @param {number} value - Integer value to pack + * @returns {number|bigint} Packed integer + * @throws {TypeError} If value is not an integer + */ +const packInteger = (value) => { + if (!Number.isInteger(value)) {throw new TypeError('Passed value is not an integer');} + + if ((value >>> 0 !== value) && (value >> 0 !== value)) {return BigInt(value);} + + return value; +}; + +/** + * Packs a UUID value + * @public + * @param {string} value - UUID string to pack + * @returns {packUuid} Packed UUID object + */ +function packUuid(value) { + if (!(this instanceof packUuid)) { + return new packUuid(value); + } + + this.value = uuidParse(value); +} + +addExtension({ + Class: packUuid, + type: 0x02, + pack(instance) { + return instance.value; + }, + unpack(buffer) { + return uuidStringify(buffer); + } +}); + +/** + * Checks if a number is a float + * @private + * @param {number} n - Number to check + * @returns {boolean} True if number is a float + */ +const isFloat = (n) => Number(n) === n && n % 1 !== 0; + +/** + * Packs a decimal value + * @public + * @param {number|BigInt} value - Numeric value to pack as decimal + * @returns {packDecimal} Packed decimal object + * @throws {TypeError} If value cannot be packed as decimal + */ +function packDecimal(value) { + if (!(this instanceof packDecimal)) { + return new packDecimal(value); + } + + if ( + !( + Number.isInteger(value) || + isFloat(value) || + typeof value === 'bigint' + ) + ) { + throw new TypeError( + 'Passed value cannot be packed as decimal: expected integer, floating number or BigInt instance' + ); + } + + let str = value.toString(); + + // process values like '1e-7' + if (str.includes('e')) { + // 20 should be enough + // not using dynamic value in order to lower the overhead + str = value.toFixed(20).replace(/0+$/, ''); + } + + let scale = 0; + if (str.includes('.')) { + const parts = str.split('.'); + scale = parts[1].length; + str = parts[0] + parts[1]; // remove the dot + } + + let isNegative = false; + if (str.startsWith('-')) { + isNegative = true; + str = str.substring(1); + } + + const signNibble = isNegative ? 'd' : 'c'; + let hexString = str + signNibble; + + // check if is odd and add a nibble + if (hexString.length % 2 !== 0) { + hexString = '0' + hexString; + } + + const bcdBuf = Buffer.from(hexString, 'hex'); + const scaleBuf = Buffer.from([scale]); + + this.value = Buffer.concat([scaleBuf, bcdBuf]); +} + +addExtension({ + Class: packDecimal, + type: 0x01, + pack(instance) { + return instance.value; + }, + unpack(buffer) { + const scale = buffer.readInt8(0); + + const hex = buffer.toString('hex', 1); + + const signNibble = hex[hex.length - 1].toLowerCase(); + const digits = hex.slice(0, -1); + + const isNegative = signNibble === 'd' || signNibble === 'b'; + + // get number without trailing digits like 0.10000000 + let cleanDigits = digits.replace(/^0+/, ''); + // if number is equal to zero + if (cleanDigits === '') {cleanDigits = '0';} + + if (scale === 0) { + const bigVal = BigInt(cleanDigits); + const signedBigVal = isNegative ? -bigVal : bigVal; + + if ( + signedBigVal <= BigInt(Number.MAX_SAFE_INTEGER) && + signedBigVal >= BigInt(Number.MIN_SAFE_INTEGER) + ) { + return Number(signedBigVal); + } + return signedBigVal; + } + + // if scale is not a zero + let resultStr; + if (cleanDigits.length <= scale) { + // .001 + resultStr = '0.' + cleanDigits.padStart(scale, '0'); + } else { + const dotPos = cleanDigits.length - scale; + resultStr = + cleanDigits.slice(0, dotPos) + '.' + cleanDigits.slice(dotPos); + } + + if (isNegative) {resultStr = '-' + resultStr;} + + // the result may be inaccurate if the are more than 15 digits, so return as a string? + // JS-specific + if (cleanDigits.length > 15) { + return resultStr; + } + + return Number(resultStr); + } +}); + +// Datetime extension +addExtension({ + Class: Date, + type: 0x04, + pack(instance) { + const seconds = (instance.getTime() / 1000) | 0; + const nanoseconds = instance.getMilliseconds() * 1000; + + const datetimeBuffer = Buffer.allocUnsafe(16); + datetimeBuffer.writeBigUInt64LE(BigInt(seconds)); + datetimeBuffer.writeUInt32LE(nanoseconds, 8); + datetimeBuffer.writeUInt32LE(0, 12); + /* + Node.js 'Date' doesn't provide nanoseconds, so just using milliseconds. + tzoffset is set to UTC, and tzindex is omitted. + */ + + return datetimeBuffer; + }, + unpack(buffer) { + const time = new Date(parseInt(buffer.readBigUInt64LE(0)) * 1000); + + if (buffer.length > 8) { + const milliseconds = (buffer.readUInt32LE(8) / 1000) | 0; + time.setMilliseconds(milliseconds); + } + + return time; + } +}); + +/** + * Packs an interval value + * @public + * @param {Object} value - Interval object with optional fields: year, month, week, day, hour, minute, second, nanosecond, adjust + * @returns {packInterval} Packed interval object + * @throws {TypeError} If value is not an object + */ +function packInterval(value) { + if (!(this instanceof packInterval)) { + return new packInterval(value); + } + + if (typeof value !== 'object') {throw new TypeError('Provided argument is not an object');} + + const map = new Map(); + if (value.year) {map.set(0, value.year);} + if (value.month) {map.set(1, value.month);} + if (value.week) {map.set(2, value.week);} + if (value.day) {map.set(3, value.day);} + if (value.hour) {map.set(4, value.hour);} + if (value.minute) {map.set(5, value.minute);} + if (value.second) {map.set(6, value.second);} + if (value.nanosecond) {map.set(7, value.nanosecond);} + if (value.adjust) {map.set(8, value.adjust);} + + this.value = encoder.encode(map); + // Overwrite the byte which defines the number of fields because it has a different format + // https://www.tarantool.io/en/doc/latest/reference/internals/msgpack_extensions/#the-interval-type + this.value[0] = map.size; +} + +addExtension({ + Class: packInterval, + type: 0x06, + pack(instance) { + return instance.value; + }, + unpack(buffer) { + buffer.writeUint8(0x80 + buffer[0]); // to be processed as an object + const decoded = decoder.decode(buffer); + + return { + year: decoded[0] || 0, + month: decoded[1] || 0, + week: decoded[2] || 0, + day: decoded[3] || 0, + hour: decoded[4] || 0, + minute: decoded[5] || 0, + second: decoded[6] || 0, + nanosecond: decoded[7] || 0, + adjust: decoded[8] || 0 + }; + } +}); + +module.exports = class MsgPack { + // make static methods in order to use them without creating a class + static packInteger = packInteger; + static packUuid = packUuid; + static packDecimal = packDecimal; + static packInterval = packInterval; + + packInteger = packInteger; + packUuid = packUuid; + packDecimal = packDecimal; + packInterval = packInterval; + + constructor() { + this.encode = encoder.encode; + } + + decode(v) { + return decoder.decode(v); + } + + // encode(v) { + // return encoder.encode(v); + // } + + useBuffer(buffer) { + return encoder.useBuffer(buffer); + } + + createDecoderStream() { + return new UnpackrStream({ + mapsAsObjects: true, + int64AsType: 'auto' + }); + } +}; diff --git a/lib/OfflineQueue.js b/lib/OfflineQueue.js new file mode 100644 index 0000000..748abca --- /dev/null +++ b/lib/OfflineQueue.js @@ -0,0 +1,109 @@ +const Command = require('./Command'); +const debug = require('./utils/debug').extend('offline-queue'); +const { noop } = require('lodash'); +const { TarantoolError } = require('./errors'); + +const ERR_OFFLINE_QUEUE_DISABLED = new TarantoolError( + 'Connection not established and "enableOfflineQueue" option is disabled' +); + +const cmdArr = Object.values(Command.commands); + +module.exports = class OfflineQueue { + queue = new Array(); + enabled = false; + + /** + * Creates an OfflineQueue instance + * @param {TarantoolConnection} self - Parent connection instance + */ + constructor(self) { + this.self = self; + } + + /** + * Enables the offline queue + */ + set() { + debug( + 'Setting the offline queue, commands will be passed to the queue manager' + ); + this.enabled = true; + } + + /** + * Flushes the offline queue with an error + * @private + * @param {Error} error - Error to reject with + */ + flush(error) { + debug( + 'Flushing offline queue with %i commands, rejecting pending callbacks with the following error: %O', + this.queue.length, + error + ); + while (this.queue.length > 0) { + this.queue.shift()[2](error, null); + } + } + + /** + * Disables the offline queue + */ + unset() { + debug( + 'Unsetting the offline queue, now commands should be sent directly' + ); + this.enabled = false; + } + + /** + * Adds a command to the offline queue + * @private + * @param {number} requestCode - Request code + * @param {Array} args - Command arguments + * @param {Function} cb - Callback function + */ + add(requestCode, args, cb) { + debug('Added command to the OfflineQueue, arguments: %O', arguments); + + if (!this.self.options.enableOfflineQueue) { + return cb(ERR_OFFLINE_QUEUE_DISABLED, null); + } + + if (!this.self.isConnectedState()) { + this.self.connect().catch(noop); + } + + this.queue.push([requestCode, args, cb]); + } + + /** + * Resets the offline queue + */ + reset() { + this.queue = []; + } + + /** + * Sends all queued commands to the server + */ + send() { + debug('Sending %d commands from offline queue', this.queue.length); + + while (this.queue.length > 0) { + const [rqCode, args, cb] = this.queue.shift(); + + const cmd = cmdArr.find((value) => { + return value.rqCode === rqCode; + }); + + const p = cmd.function.call(this.self, ...args); + if (p instanceof Promise) { + p.then((result) => cb(null, result)).catch((error) => + cb(error, null) + ); + } + } + } +}; diff --git a/lib/PreparedStatement.js b/lib/PreparedStatement.js new file mode 100644 index 0000000..20d4fc8 --- /dev/null +++ b/lib/PreparedStatement.js @@ -0,0 +1,12 @@ +/** + * Represents a prepared SQL statement + */ +module.exports = class PreparedStatement { + /** + * Creates a prepared statement + * @param {number} id - Statement ID from the server + */ + constructor(id) { + this.stmt_id = id; + } +}; diff --git a/lib/StandaloneConnector.js b/lib/StandaloneConnector.js new file mode 100644 index 0000000..5f13503 --- /dev/null +++ b/lib/StandaloneConnector.js @@ -0,0 +1,66 @@ +const net = require('net'); +const tls = require('tls'); +const {TarantoolError} = require('./errors'); + +const ERR_SOCKET_NOT_CREATED = new TarantoolError('Socket not created yet'); +const ERR_SOCKET_ALREADY_CREATED = new TarantoolError('Socket is already created'); + +/** + * Standalone socket connector for Tarantool + */ +module.exports = class StandaloneConnector { + socket = null; + options = null; + write = () => ERR_SOCKET_NOT_CREATED; + + /** + * Creates a connector + * @param {Object} options - Connection options + */ + constructor(options) { + this.options = options; + } + + /** + * Disconnects the socket + */ + disconnect(cb) { + if (!this.socket) {throw ERR_SOCKET_NOT_CREATED;} + this.socket.end(cb); + } + + /** + * Connects to the server + * @param {Function} callback - Callback function + */ + connect(callback) { + try { + if (this.socket) {throw ERR_SOCKET_ALREADY_CREATED;} + + let connectionModule; + if (typeof this.options.tls == 'object') { + connectionModule = tls; + Object.assign(this.options, this.options.tls); + } else { + connectionModule = net; + } + this.socket = connectionModule.connect(this.options); + this.write = this.socket.write.bind(this.socket); + this.socket.setKeepAlive(this.options.keepAlive, parseInt(this.options.keepAlive) || 0); + this.socket.setNoDelay(this.options.noDelay); + } catch (err) { + callback(err); + return; + } + + callback(null, this.socket); + } + + /** + * Checks if socket is writable + * @returns {boolean} True if socket is writable + */ + isWritable() { + return this.socket?.writable || false; + } +}; diff --git a/lib/Transaction.js b/lib/Transaction.js new file mode 100644 index 0000000..836f9de --- /dev/null +++ b/lib/Transaction.js @@ -0,0 +1,49 @@ +const Command = require('./Command'); +const {symbols: { + streamId: streamIdSym +}} = require('./const'); + +/** + * Represents a transaction context + */ +class Transaction { + /** + * Creates a transaction + * @static + * @param {TarantoolConnection} self - Parent connection instance + * @returns {Transaction} New transaction instance + */ + static createTransaction = function createTransaction() { + return new Transaction(this); + }; + + /** + * Creates a new Transaction instance + * @param {TarantoolConnection} self - Parent connection instance + */ + constructor(self) { + this.streamId = self._getRequestId(); + this._parent = self; + } +} + +Command.list.map((name) => { + const visibleName = typeof name === 'symbol' ? name.description : name; + + Transaction.prototype[visibleName] = function commandInterceptor() { + const optsPos = Command.commands[name].optsPos; + const args = [...arguments]; + // if args are not empty + if (args[optsPos]) { + args[optsPos][streamIdSym] = this.streamId; + } else { + args[optsPos] = { + [streamIdSym]: this.streamId + }; + } + + return this._parent[name].apply(this._parent, args); + }; +}); + +module.exports = Transaction; diff --git a/lib/connection.js b/lib/connection.js index e1e4fd1..e5dec49 100755 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,397 +1,546 @@ -const {EventEmitter} = require('events'); -const debug = require('debug')('tarantool-driver:main'); -const { - pick, - defaults, - assign, - noop -} = require('lodash'); -const { - parseURL, - TarantoolError, - withResolvers, - createBuffer, -} = require('./utils'); -const msgpackExt = require('./msgpack-extensions'); -const { - revertStates, - states -} = require('./const'); -const Commands = require('./commands'); -const Pipeline = require('./pipeline'); -const {findPipelineError, findPipelineErrors} = require('./pipeline-response'); -const Transaction = require('./transaction'); -const Parser = require('./parser'); -const Connector = require('./connector'); -const AutoPipeline = require('./autopipelining'); +const { EventEmitter } = require('events'); +const { withResolvers, applyMixin, parseOptions } = require('./utils'); +const debug = require('./utils/debug').extend('main'); +const { mergeWith, noop, defaults } = require('lodash'); +const MsgPack = require('./MsgPack'); +const { revertStates, states, Iterators, defaultOptions, symbols: { + bypassOfflineQueue: bypassOfflineQueueSym, + pipelined: pipelinedSym +} } = require('./const'); +const { pipeline } = require('./pipeline/Pipeline'); +const PipelineResponse = require('./pipeline/PipelineResponse'); +const { createTransaction } = require('./Transaction'); +const parser = require('./parser'); +const AutoPipeliner = require('./pipeline/AutoPipeliner'); const eventHandler = require('./event-handler'); +const Commander = require('./Commander'); +const OfflineQueue = require('./OfflineQueue'); +const { TarantoolError, ReplyError } = require('./errors'); +const { format, deprecate } = require('node:util'); +const PreparedStatement = require('./PreparedStatement'); +const {setFastTimeout} = require('./utils/FastTimer'); +const {commandsNum} = require('./Command'); -const defaultOptions = { - host: 'localhost', - port: 3301, - path: null, - username: null, - password: null, - reserveHosts: [], - beforeReserve: 2, - timeout: 0, - noDelay: true, - requestTimeout: 10000, - keepAlive: true, - tupleToObject: false, - nonWritableHostPolicy: 0, /* What to do when Tarantool server rejects write operation, - e.g. because of box.cfg.read_only set to 'true' or during fetching snapshot. - Possible values are: - - 0: reject Promise - - 1: ('changeHost') disconnect from the current host and connect to the next of 'reserveHosts'. Pending Promise will be rejected. - - 2: ('changeAndRetry') same as 'changeHost', but after connecting tries to run the command again in order to fullfil the Promise - */ - maxRetriesPerRequest: 5, // If 'nonWritableHostPolicy' specified, Promise will be rejected only after exceeding this setting - enableOfflineQueue: true, - retryStrategy: function (times) { - return Math.min(times * 50, 2000); - }, - lazyConnect: false -}; - -class TarantoolConnection extends EventEmitter { - static packUuid = msgpackExt.packUuid; - static packInteger = msgpackExt.packInteger; - static packDecimal = msgpackExt.packDecimal; - static findPipelineError = findPipelineError; - static findPipelineErrors = findPipelineErrors; +class TarantoolConnection extends Commander { + static MsgPack = MsgPack; + static PreparedStatement = PreparedStatement; + static PipelineResponse = PipelineResponse; + static errors = { + ReplyError, + TarantoolError + }; + static iterators = Iterators; connect = connect; - destroy = disconnect; + destroy = deprecate( + disconnect, + '\'.destroy()\' method is deprecated. Use \'.disconnect()\' instead.', + 'Tarantool-Driver' + ); disconnect = disconnect; - parseOptions = parseOptions; - resetOfflineQueue = resetOfflineQueue; + quit = quit; useNextReserve = useNextReserve; - sendCommand = sendCommand; setState = setState; - flushQueue = flushQueue; silentEmit = silentEmit; errorHandler = eventHandler.errorHandler; - createBuffer = createBuffer; - - constructor () { - super(); - this.reserve = []; - this.connecting = false; - this.socket = null; - this.options = {}; - this.parseOptions(arguments[0], arguments[1], arguments[2]); - this.schemaId = 0; - this.dataState = states.PREHELLO; - this.commandsQueue = []; - this.offlineQueue = []; - this.namespace = {}; - this.retryAttempts = 0; - this._id = [ 1 ]; - this.sentCommands = new Map(); - this.autoPipelineQueue = []; - this.autoPipeliningEnabled = false; - this.autoPipeliningStarted = false; - this.enableAutoPipelining = this.options.enableAutoPipelining; - this._state = [null]; - Object.assign(this, Connector); - Object.assign(this, Commands); - Object.assign(this, AutoPipeline.prototype); - Object.assign(this, Parser); - Object.assign(this, Transaction.prototype); - Object.assign(this, Pipeline); - Object.assign(this, msgpackExt); - - if (this.options.lazyConnect) { - this.setState(states.INITED); - } else { - this.connect().catch(noop); - } - } + _awaitSentCommandsDrain = _awaitSentCommandsDrain; - set enableAutoPipelining (value = false) { - if (typeof value != 'boolean') throw new Error(`Should pass a boolean to 'enableAutoPipelining'`); + // parser + processResponse = parser.processResponse; + _returnBool = parser._returnBool; - this.autoPipeliningEnabled = value; - this.autoPipeliningStarted = false; - } + // pipeline + pipeline = pipeline; - get enableAutoPipelining () { - return this.autoPipeliningEnabled; - } -} + transaction = createTransaction; -function resetOfflineQueue () { - this.offlineQueue = []; -}; + offlineQueue = new OfflineQueue(this); + autoPipeliner = new AutoPipeliner(this); + schemaId = 0; + retryAttempts = 0; + sentCommands = new Map(); + _state = [states.INITED]; + pendingPromises = {}; + mpDecoderStream = null; + connectionErrors = []; + reserveIterator = 0; + salt = ''; + connector = null; -var warnedAboutUnstability = false; -function parseOptions (){ - var i; - for (i = 0; i < arguments.length; ++i) { - var arg = arguments[i]; - if (arg === null || typeof arg === 'undefined') { - continue; - } + /** + * Creates a new TarantoolConnection instance + * + * Can be called with multiple argument variants: + * - new TarantoolConnection(port, host, opts) + * - new TarantoolConnection(host, port, opts) + * - new TarantoolConnection(port, opts) + * - new TarantoolConnection(host, opts) + * - new TarantoolConnection(connectionString) + * - new TarantoolConnection(connectionObject) + * + * @param {string|number|Object} [arg1] - Host, port, connection string, or connection object + * @param {string|number|Object} [arg2] - Port, host, or connection options + * @param {Object} [arg3] - Connection options + * + * @param {string} [opts.host='localhost'] - Server host + * @param {number} [opts.port=3301] - Server port + * @param {string} [opts.path] - Unix socket path (overrides host/port) + * @param {number} [opts.timeout=5000] - Connection timeout in milliseconds + * @param {boolean} [opts.lazyConnect=false] - Don't connect automatically on instantiation + * @param {boolean} [opts.enableOfflineQueue=true] - Enable offline queue + * @param {boolean} [opts.enableAutoPipelining=true] - Enable automatic pipelining + * @param {number} [opts.sliderBufferInitialSize=16384] - Initial size of buffer pool + * @param {boolean} [opts.tupleToObject=false] - Convert tuple arrays to objects + * @param {number} [opts.commandTimeout] - Max execution time for commands in milliseconds + * @param {boolean} [opts.prefetchSchema=true] - Fetch schema on connect + * @param {Array} [opts.reserveHosts] - Fallback hosts for failover + * @param {Object} [opts.Connector] - Custom connector implementation class + * @param {Object} [opts.MsgPack] - Custom MsgPack implementation class + */ + constructor() { + const parsed = defaults( + parseOptions(arguments[0], arguments[1], arguments[2]), + defaultOptions + ); - switch (typeof arg) { - case 'object': - defaults(this.options, arg); - break; - case 'string': - if(!isNaN(arg) && (parseFloat(arg) | 0) === parseFloat(arg)){ - this.options.port = arg; - continue; - } - defaults(this.options, parseURL(arg)); - break; - case 'number': - this.options.port = arg; - break; - default: - throw new TarantoolError('Invalid argument ' + arg); + super(parsed); + EventEmitter.call(this); + + this.msgpacker = new this.options.MsgPack(this.options); + this.packUuid = this.msgpacker.packUuid; + this.packInteger = this.msgpacker.packInteger; + this.packDecimal = this.msgpacker.packDecimal; + this.packInterval = this.msgpacker.packInterval; + this.mpDecoderStream = createUnpackrStream.call(this); + + this.offlineQueue.set(); // request will be rejected with an error if 'enableOfflineQueue' is set to false and connection is not established + if (!this.options.lazyConnect) { + this.connect().catch(noop); } } - defaults(this.options, defaultOptions); - var reserveHostsLength = this.options.reserveHosts && this.options.reserveHosts.length || 0 - if (this.options.nonWritableHostPolicy && !warnedAboutUnstability) { - warnedAboutUnstability = true; - process.emitWarning(new TarantoolError(`'nonWritableHostPolicy' is under development, use carefully`)) - } - if (this.options.nonWritableHostPolicy && !reserveHostsLength) { - throw new TarantoolError('\'nonWritableHostPolicy\' option is specified, but there are no reserve hosts. Specify them in connection options via \'reserveHosts\'') - } - if (typeof this.options.port === 'string') { - this.options.port = parseInt(this.options.port, 10); - } - if (this.options.path != null) { - delete this.options.port - delete this.options.host - } - if (reserveHostsLength > 0) { - this.reserveIterator = 1; - this.reserve.push(pick(this.options, ['port', 'host', 'username', 'password', 'path'])); - for(i = 0; i { + if (error) {return p.reject(error);} - return reserveOptions; -}; + p.resolve(success); + }; + } -function sendCommand (requestCode, reqId, buffer, callbacks, commandArguments, opts = {}){ - // create promise for async methods - var promise; - if (!callbacks) { - var {resolve, reject, ...etc} = withResolvers(); - promise = etc.promise - callbacks = [resolve, reject]; - }; - switch (this._state[0]){ - case states.INITED: - this.connect().catch(noop); - case states.CONNECT: - if(!this.socket || !this.socket.writable){ - debug('queue (no writable socket) -> %s(%s)', requestCode, reqId); - this.offlineQueue.push([ - requestCode, - callbacks, - commandArguments, - this - ]); - } else { - const tupleToObject = opts.tupleToObject ?? this.options.tupleToObject; - - // create an array which will be stored till the response is received - var setValue = [ + const opts = commandArguments[commandsNum[requestCode].optsPos] ||= {}; + // check if should pass commands through the OfflineQueue + if (this.offlineQueue.enabled && !opts[bypassOfflineQueueSym]) { + this.offlineQueue.add(requestCode, commandArguments, cb); + + return promise; + } + + switch (this._state[0]) { + case states.AUTH: + case states.CONNECT: { + if (!this.connector.isWritable()) { + cb(new TarantoolError('Socket is not writable'), null); + break; + } + // create an array which will be stored untill the response is received + const setValue = [ requestCode, - callbacks, - null, - null, - tupleToObject + cb, + null, // command arguments + null, // timeoud id + opts ]; - if ((this.options.nonWritableHostPolicy === 2) || tupleToObject) setValue[2] = Object.values(commandArguments); + if (opts.tupleToObject ?? this.options.tupleToObject) { + setValue[2] = commandArguments; + } - var requestTimeout = this.options.requestTimeout || opts.requestTimeout; - if (requestTimeout) setValue[3] = setTimeout(function () { - callbacks[1]( - new TarantoolError('Request timed out (' + requestTimeout + ' ms)') - ) - }, requestTimeout); + const commandTimeout = + opts.commandTimeout ?? this.options.commandTimeout; + if (commandTimeout) { + // setFastTimeout is more performant than native setTimeout + setValue[3] = setFastTimeout(() => { + cb( + new TarantoolError( + `Request №${reqId} timed out of ${commandTimeout} ms` + ), + null + ); + this.sentCommands.delete(reqId); // prevent memory leaks + }, commandTimeout); + } this.sentCommands.set(reqId, setValue); const shouldAutoPipeline = - opts._pipelined === true - ? false - : opts.autoPipeline ?? this.autoPipeliningEnabled; + opts[pipelinedSym] === true + ? false + : (opts.autoPipeline ?? + this.options.enableAutoPipelining); // check if should be autopipelined if (shouldAutoPipeline) { - this._addToAutoPipeliningQueue(buffer); - // in manually pipelined mode data is written via its own function - } else if (!opts._pipelined) { - debug(`sending request №${reqId} to server`) - this.socket.write(buffer); - }; + this.autoPipeliner.add(buffer); + } else if (opts[pipelinedSym]) { + return buffer; + } else { + debug( + 'sending request (rqCode: %i) №%i to server, buffer: %h, cmd args: %O', + requestCode, + reqId, + buffer, + commandArguments + ); + this.connector.write(buffer, null, () => { + // wait for the buffer to become free + // https://nodejs.org/api/net.html#socketwritedata-encoding-callback + this.bufferPool.releaseBuffer(buffer); + }); + } + break; } - break; - case states.END: - callbacks[1](new TarantoolError('Connection is closed.')); - break; - default: - debug('queue -> %s(%s)', requestCode, reqId); - if (!this.options.enableOfflineQueue) { - return callbacks[1](new TarantoolError('Connection not established yet!')); - }; - this.offlineQueue.push([ - requestCode, - callbacks, - commandArguments, - this - ]); - } + case states.END: + cb( + new TarantoolError('Connection is finished', { + cause: this.connectionErrors // if had them + }), + null + ); + break; + default: + cb( + new TarantoolError( + `Unexpected state "${ + revertStates[this._state[0]] + }": should have been sent command to offline queue instead.` + ), + null + ); + } - return promise; -}; + return promise; + } +} +applyMixin(TarantoolConnection, EventEmitter); -function setState (state, arg) { - var address; - if (this.socket && this.socket.remoteAddress && this.socket.remotePort) { - address = this.socket.remoteAddress + ':' + this.socket.remotePort; - } else { - if (this.options.path != null) { - address = this.options.path +/** + * Switches to next reserve host + * @private + */ +const useNextReserve = function () { + const connectRetryAttempts = this.options.connectRetryAttempts; + if (this.retryAttempts > connectRetryAttempts) { + throw new TarantoolError( + `Exceeded "connectRetryAttempts" (${connectRetryAttempts}) while trying to establish a connection` + ); + } + if (this.reserveIterator >= this.options.reserveHosts?.length) {this.reserveIterator = 0;} + const reserveHost = this.options.reserveHosts?.[this.reserveIterator++]; + if (!reserveHost) { + throw new TarantoolError( + 'Attempted to use the next reserve host, but there are none of them left' + ); + } + // .call(...) allows passing 'reserveHost' as string, object, integer, + // or even array (like ['localhost', 3301, {timeout: 20000}], having the same behavior as `new Tarantool('localhost', 3301, {timeout: 20000}})`) + const reserveOptions = parseOptions.call(this, reserveHost); + mergeWith(this.options, reserveOptions, (target, src) => { + if (src !== undefined) { + return src; } else { - address = this.options.host + ':' + this.options.port; + return target; } - } - debug('state[%s]: %s -> %s', address, revertStates[this._state[0]] || '[empty]', revertStates[state]); + }); + + return reserveOptions; +}; + +/** + * Sets the connection state + * @private + * @param {number} state - New state + * @param {*} arg - Additional argument + */ +const setState = function (state, arg) { + const address = + this.options.path != null + ? this.options.path + : this.options.host + ':' + this.options.port; + debug( + 'state[%s]: %s -> %s', + address, + revertStates[this._state[0]] || '[empty]', + revertStates[state] + ); this._state[0] = state; - process.nextTick(this.emit.bind(this, revertStates[state], arg)); + + process.nextTick(() => { + this.emit(revertStates[state], arg); + }); }; -function connect (){ - return new Promise(function (resolve, reject) { - var state = this._state[0]; - if (state === states.CONNECTING || state === states.CONNECT || state === states.CONNECTED || state === states.AUTH) { - reject(new TarantoolError('Tarantool is already connecting/connected')); - return; - } +/** + * Establishes a connection to the Tarantool server + * @public + * @returns {Promise} + */ +const connect = function () { + if (this.pendingPromises.connect || this.isConnectedState()) { + return Promise.reject( + new TarantoolError('Tarantool is already connecting/connected') + ); + } + + // create new promise if it doesn't exist + this.pendingPromises.connect ||= new Promise((resolve, reject) => { this.setState(states.CONNECTING); - var _this = this; - this._connect(function(err, socket){ - if(err){ - _this.flushQueue(err); + this.connector = new this.options.Connector(this.options); + const _this = this; + this.connector.connect(function (err, socket) { + if (err) { + _this.offlineQueue.flush(err); _this.silentEmit('error', err); reject(err); _this.setState(states.END); return; } - _this.socket = socket; - socket.once('connect', eventHandler.connectHandler.bind(_this)); - socket.once('error', eventHandler.errorHandler.bind(_this)) + + socket.on('error', eventHandler.errorHandler.bind(_this)); socket.once('close', eventHandler.closeHandler.bind(_this)); - socket.on('data', eventHandler.dataHandler.call(_this)); - - if (_this.options.timeout) { - socket.setTimeout(_this.options.timeout, function () { - socket.setTimeout(0); - socket.destroy(); - - var error = new TarantoolError('connect ETIMEDOUT'); - error.errorno = 'ETIMEDOUT'; - error.code = 'ETIMEDOUT'; - error.syscall = 'connect'; - _this.errorHandler(error); - }); - socket.once('connect', function () { - socket.setTimeout(0); - }); + socket.once('data', eventHandler.dataHandler_prehello.bind(_this)); + + // this is only needed until the connection is succesfully established + socket.setTimeout(_this.options.timeout, function () { + socket.destroy(); + + const error = new TarantoolError( + 'connect ETIMEDOUT' + ); + error.errorno = 'ETIMEDOUT'; + error.code = 'ETIMEDOUT'; + error.syscall = 'connect'; + _this.errorHandler(error); + }); + socket.once('connect', function () { + // now we can clear the setTimeout which was set above + socket.setTimeout(0); + }); + + // prevent from EventEmitter memory leak + const listeners = _this.listeners('connect'); + if (!listeners.length || !listeners.some((func) => func._u)) { + const connectionConnectHandler = function () { + _this.removeListener('close', connectionCloseHandler); + + if (this.options.prefetchSchema) { + return this.fetchSchema() + .then(() => { + this.offlineQueue.unset(); + this.offlineQueue.send(); + }) + .then(resolve) + .catch((error) => + reject( + new TarantoolError( + 'Failed to fetch space schema', + { + cause: error + } + ) + ) + ); + } else { + this.offlineQueue.unset(); + this.offlineQueue.send(); + } + + resolve(); + }; + connectionConnectHandler._u = true; // make this function unique without relying on the name of function + const connectionCloseHandler = function () { + _this.removeListener('connect', connectionConnectHandler); + reject( + new TarantoolError('Connection is closed.', { + cause: _this.connectionErrors + }) + ); + }; + _this.once('connect', connectionConnectHandler); + _this.once('close', connectionCloseHandler); } - var connectionConnectHandler = function () { - _this.removeListener('close', connectionCloseHandler); - resolve(); - }; - var connectionCloseHandler = function () { - _this.removeListener('connect', connectionConnectHandler); - reject(new Error('Connection is closed.')); - }; - _this.once('connect', connectionConnectHandler); - _this.once('close', connectionCloseHandler); }); - }.bind(this)); + }); + + return this.pendingPromises.connect + .then(() => this.connectionErrors = []) + // clear created promise + .finally(() => { + this.pendingPromises.connect = null; + }); }; -function flushQueue (error) { - while (this.offlineQueue.length > 0) { - this.offlineQueue.shift()[3][1](error); +/** + * Emits an event silently (doesn't throw on no listeners) + * @private + * @param {string} eventName - Event name + * @returns {boolean} True if event was emitted + */ +const silentEmit = function (eventName) { + const listenersLen = this.listeners(eventName).length; + let error; + if (eventName === 'error') { + error = arguments[1]; + + if ( + error instanceof Error && + (error.message === 'Connection manually closed' || + error.syscall === 'read') + ) { + return; + } + + // if no 'error' listeners + if (!listenersLen) { + return process.emitWarning( + new TarantoolError(format('Unhandled error event: %O', error)) // error object may not be filled correctly if passing error via {cause: error}, so we are using util's format() + ); + } } - while (this.commandsQueue.length > 0) { - this.commandsQueue.shift()[3][1](error); + + if (listenersLen > 0) { + return this.emit.apply(this, arguments); } + + return false; }; -function silentEmit (eventName) { - var error; - if (eventName === 'error') { - error = arguments[1]; +/** + * Closes the connection immediately. + * Some sent commands (if present) will be rejected with an error. + * @public + * @returns {undefined} + */ +const disconnect = function () { + debug('received a manual \'.disconnect()\' call'); + this.setState(states.END); + return eventHandler.closeHandler.call(this, undefined, false); +}; - if (this.status === 'end') { - return; - } +/** + * Closes the connection gracefully and waits for pending commands to fulfill + * @public + * @returns {Promise} + */ +const quit = function () { + debug('received a manual \'.quit()\' call'); + this.setState(states.END); + return eventHandler.closeHandler.call(this, undefined, true); +}; - if (this.manuallyClosing) { - if ( - error instanceof Error && - ( - error.message === 'Connection manually closed' || - error.syscall === 'connect' || - error.syscall === 'read' - ) - ) { - return; - } - } - } - if (this.listeners(eventName).length > 0) { - return this.emit.apply(this, arguments); - } - if (error && error instanceof Error) { - console.error('[tarantool-driver] Unhandled error event:', error.stack); - } - return false; +/** + * Creates a decoder stream for msgpack messages + * @private + * @returns {Stream} Decoder stream + */ +const createUnpackrStream = function () { + let decodedHeaders = null; + let decodingStep = 0; + + const unpackrStream = this.msgpacker.createDecoderStream(); + + unpackrStream.on('error', (error) => { + this.errorHandler( + new TarantoolError('MsgPack decoder error', { + cause: error + }) + ); + }); + + unpackrStream.on('data', (data) => { + const type = typeof data; + switch (type) { + case 'number': + decodingStep = 0; + break; + case 'object': + switch (decodingStep) { + case 0: + decodedHeaders = data; + decodingStep = 1; + break; + case 1: + this.processResponse(decodedHeaders, data); + decodingStep = 2; // in order to catch the unexpected data in the 'default' block below + break; + default: + this.errorHandler( + new TarantoolError( + 'Unknown step detected while decoding response data, maybe network loss occured?' + ) + ); + } + break; + default: + this.errorHandler( + new TarantoolError( + 'Type of decoded value does not satisfy requirements: ' + + type + ) + ); + } + }); + + return unpackrStream; }; -function disconnect (reconnect){ - if (!reconnect) { - this.manuallyClosing = true; - } - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = null; - } - if (this._state[0] === states.INITED) { - eventHandler.closeHandler(this)(); - } else { - this._disconnect(); - } +/** + * Waits until all sent commands are drained (responses received) + * @public + * @returns {Promise} + */ +const _awaitSentCommandsDrain = async function () { + if (this.sentCommands.size === 0) {return Promise.resolve();} + + let timeoudId, intervalId; + return Promise.race([ + new Promise((_, reject) => { + timeoudId = setTimeout(reject, this.options.timeout); + timeoudId.unref(); + }), + new Promise((resolve) => { + intervalId = setInterval(() => { + if (this.sentCommands.size === 0) {return resolve();} + }, 500); + intervalId.unref(); + }) + ]).finally(() => { + // free the event loop + clearTimeout(timeoudId); + clearInterval(intervalId); + }); }; -module.exports = TarantoolConnection; \ No newline at end of file +module.exports = TarantoolConnection; diff --git a/lib/const.js b/lib/const.js index 32f9146..9501451 100755 --- a/lib/const.js +++ b/lib/const.js @@ -1,129 +1,165 @@ -// i steal it from go -var RequestCode = { - rqConnect: 0x00, //fake for connect - rqSelect: 0x01, - rqInsert: 0x02, - rqReplace: 0x03, - rqUpdate: 0x04, - rqDelete: 0x05, - rqCall: 0x06, - rqAuth: 0x07, - rqEval: 0x08, - rqUpsert: 0x09, - rqRollback: 0x10, - rqCallNew: 0x0a, - rqExecute: 0x0b, - rqPrepare: 0x0d, - rqBegin: 0x0e, - rqCommit: 0x0f, - rqDestroy: 0x100, //fake for destroy socket cmd - rqPing: 0x40, - rqId: 0x49 -}; +const MsgPack = require('./MsgPack'); +const StandaloneConnector = require('./StandaloneConnector'); -var KeysCode = { - code: 0x00, - sync: 0x01, - schema_version: 0x05, - space_id: 0x10, - index_id: 0x11, - limit: 0x12, - offset: 0x13, - iterator: 0x14, - key: 0x20, - tuple: 0x21, - function_name: 0x22, - username: 0x23, - expression: 0x27, - def_tuple: 0x28, - data: 0x30, - iproto_error_24: 0x31, - metadata: 0x32, - bind_metadata: 0x33, - sql_text: 0x40, - sql_bind: 0x41, - sql_info: 0x42, - stmt_id: 0x43, - iproto_error: 0x52, - iproto_version: 0x54, - iproto_features: 0x55, - iproto_timeout: 0x56, - iproto_txn_isolation: 0x59, - iproto_stream_id: 0x0a, - iproto_auth_type: 0x5b +module.exports.RequestCode = { + rqSelect: 0x01, + rqInsert: 0x02, + rqReplace: 0x03, + rqUpdate: 0x04, + rqDelete: 0x05, + rqCall: 0x06, + rqAuth: 0x07, + rqEval: 0x08, + rqUpsert: 0x09, + rqRollback: 0x10, + rqCallNew: 0x0a, + rqExecute: 0x0b, + rqPrepare: 0x0d, + rqBegin: 0x0e, + rqCommit: 0x0f, + rqPing: 0x40, + rqId: 0x49 }; -// https://github.com/fl00r/go-tarantool-1.6/issues/2 -var IteratorsType = { - eq: 0, - req: 1, - all: 2, - lt: 3, - le: 4, - ge: 5, - gt: 6, - bitsAllSet: 7, - bitsAnySet: 8, - bitsAllNotSet: 9 +module.exports.KeysCode = { + code: 0x00, + sync: 0x01, + schema_version: 0x05, + space_id: 0x10, + index_id: 0x11, + limit: 0x12, + offset: 0x13, + iterator: 0x14, + key: 0x20, + tuple: 0x21, + function_name: 0x22, + username: 0x23, + expression: 0x27, + def_tuple: 0x28, + data: 0x30, + iproto_error_24: 0x31, + metadata: 0x32, + bind_metadata: 0x33, + sql_text: 0x40, + sql_bind: 0x41, + sql_info: 0x42, + stmt_id: 0x43, + iproto_error: 0x52, + iproto_version: 0x54, + iproto_features: 0x55, + iproto_timeout: 0x56, + iproto_txn_isolation: 0x59, + iproto_stream_id: 0x0a, + iproto_auth_type: 0x5b }; -var OkCode = 0; -var NetErrCode = 0xfffffff1; // fake code to wrap network problems into response -var TimeoutErrCode = 0xfffffff2; // fake code to wrap timeout error into repsonse +module.exports.Iterators = { + EQ: 'eq', + REQ: 'req', + ALL: 'all', + LT: 'lt', + LE: 'le', + GE: 'ge', + GT: 'gt', + BITS_ALL_SET: 'bitsAllSet', + BITS_ANY_SET: 'bitsAnySet', + BITS_ALL_NOT_SET: 'bitsAllNotSet', + OVERLAPS: 'overlaps', + NEIGHBOR: 'neighbor' +}; -var Space = { - schema: 272, - space: 281, - index: 289, - func: 296, - user: 304, - priv: 312, - cluster: 320 +// https://www.tarantool.io/ru/doc/latest/reference/internals/iproto/keys/#iproto-iterator +module.exports.IteratorsType = { + eq: 0, + req: 1, + all: 2, + lt: 3, + le: 4, + ge: 5, + gt: 6, + bitsAllSet: 7, + bitsAnySet: 8, + bitsAllNotSet: 9, + overlaps: 10, + neighbor: 11 }; -var IndexSpace = { - primary: 0, - name: 2, - indexPrimary: 0, - indexName: 2 +module.exports.Space = { + schema: 272, + space: 281, + index: 289, + func: 296, + user: 304, + priv: 312, + cluster: 320 }; -const revertStates = { - 0: 'connecting', - 1: 'connected', - 2: 'awaiting', - 4: 'inited', - 8: 'prehello', - 16: 'awaiting_length', - 32: 'end', - 64: 'reconnecting', - 128: 'auth', - 256: 'connect', - 512: 'changing_host' +module.exports.IndexSpace = { + id: 0, + name: 2, + format: 6 }; -const states = { +module.exports.states = { CONNECTING: 0, - CONNECTED: 1, - AWAITING: 2, + // CONNECTED: 1, + // AWAITING: 2, INITED: 4, - PREHELLO: 8, - AWAITING_LENGTH: 16, + // PREHELLO: 8, + // AWAITING_LENGTH: 16, END: 32, RECONNECTING: 64, AUTH: 128, - CONNECT: 256, - CHANGING_HOST: 512 + CONNECT: 256 }; -module.exports = { - states, - revertStates, - RequestCode, - KeysCode, - IteratorsType, - OkCode, - passEnter: Buffer.from('a9636861702d73686131', 'hex') /* from msgpack.encode('chap-sha1') */, - Space, - IndexSpace, -}; \ No newline at end of file +module.exports.revertStates = Object.fromEntries( + Object.entries(module.exports.states).map((arr) => [ + arr[1], + arr[0].toLowerCase() + ]) +); + +module.exports.passEnter = Buffer.from('a9636861702d73686131', 'hex'); /* from msgpack.encode('chap-sha1') */ + +module.exports.defaultOptions = { + host: 'localhost', + port: 3301, + path: null, // UNIX-path + username: null, + password: null, + reserveHosts: [], // array of strings(e.g. UNIX-path or connection string)/objects/numbers(ports) + beforeReserve: 2, // Number of attempts to reconnect before connecting to next host from the 'reserveHosts' + timeout: 10000, // do never set this to near-zero values to prevent unexpected behavior of connect/disconnect methods + noDelay: true, // 'net' module option + commandTimeout: null, // how many milliseconds to wait for command execution before throwing an error to callback/promise. Recommended values are 500+ + keepAlive: true, // 'net' module option + tupleToObject: false, // convert array-of-arrays (default) response to array-of-objects + enableOfflineQueue: true, + retryStrategy: function (times) { + return Math.min(times * 50, 2000); + }, + lazyConnect: false, // set to true if you wish to connect later manually + enableAutoPipelining: false, // set to true if you want to increase performance (200%+) with a trade-off in the form of a bit increased query execution time. + sliderBufferInitialSize: Buffer.poolSize * 10, // increase for better performance on high-load, decrease on weak systems + prefetchSchema: true, // load space schema on connect + connectRetryAttempts: 10, // how many attempts to make trying to connect (including reserve hosts) before throwing error on '.connect()' promise + MsgPack, // Custom MsgPack class can be provided as option + Connector: StandaloneConnector // Custom Connector class can be provided as option +}; + +module.exports.nullishOpts = Object.fromEntries( + Object.entries(module.exports.defaultOptions).map((arr) => [ + arr[0], + undefined + ]) +); + +module.exports.symbols = { + streamId: Symbol('streamId'), + bypassOfflineQueue: Symbol('bypassOfflineQueue'), + pipelined: Symbol('pipelined'), + begin: Symbol('begin'), + rollback: Symbol('rollback'), + commit: Symbol('commit') +}; diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..4682193 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,25 @@ +module.exports.TarantoolError = class TarantoolError extends Error { + name = 'TarantoolError'; +}; + +// https://www.tarantool.io/ru/doc/latest/reference/internals/msgpack_extensions/#the-error-type +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/errcodes/ +// For a complete list of errors, refer to the Tarantool error code header file (https://github.com/tarantool/tarantool/blob/master/src/box/errcode.h) +module.exports.ReplyError = class ReplyError extends module.exports.TarantoolError { + name = 'ReplyError'; + type = ''; // e.g. “ClientError”, “SocketError”, etc + file = ''; + line = 0; + errno = 0; + fields = null; + + constructor(msg, obj) { + super(msg, obj); + + this.type = obj.type; + this.file = obj.file; + this.line = obj.line; + this.errno = obj.errno; + this.fields = obj.fields || {}; + } +}; diff --git a/lib/event-handler.js b/lib/event-handler.js index f13e9e8..68ce6e2 100755 --- a/lib/event-handler.js +++ b/lib/event-handler.js @@ -1,172 +1,108 @@ -var debug = require('debug')('tarantool-driver:handler'); -var { TarantoolError } = require("./utils"); -var { UnpackrStream } = require("msgpackr"); -const { states } = require("./const"); +const debug = require('./utils/debug').extend('event-handler'); +const { states, symbols: { + bypassOfflineQueue: bypassOfflineQueueSym +} } = require('./const'); +const { pick } = require('lodash'); +const { TarantoolError } = require('./errors'); +const { + closeConnection, + tryToReconnect, + rejectSentCommands +} = require('./utils'); -exports.connectHandler = function () { - this.retryAttempts = 0; - switch(this._state[0]){ - case states.CONNECTING: - this.dataState = states.PREHELLO; - break; - case states.CONNECTED: - if(this.options.password){ - this.setState(states.AUTH); - this._auth(this.options.username, this.options.password) - .then(() => { - this.setState(states.CONNECT, {host: this.options.host, port: this.options.port}); - debug('authenticated [%s]', this.options.username); - sendOfflineQueue.call(this); - }, (err) => { - this.flushQueue(err); - this.errorHandler(err); - this.disconnect(true); - }); - } else { - this.setState(states.CONNECT, {host: this.options.host, port: this.options.port}); - sendOfflineQueue.call(this); - } - break; - } -}; - -function sendOfflineQueue(){ - if (this.offlineQueue.length) { - debug('send %d commands in offline queue', this.offlineQueue.length); - var offlineQueue = this.offlineQueue; - this.resetOfflineQueue(); - while (offlineQueue.length > 0) { - var command = offlineQueue.shift(); - var rqCode = command[0]; - var cb = command[1]; - var args = command[2]; - var rqFunc = this.rqCommands[rqCode]; - if (!rqFunc) return cb[1]( - new Error(`Unknown request code [${rqCode}] to resend a request, maybe a bug?`) - ); +/** + * Handles the initial handshake data from server + * @private + */ +exports.dataHandler_prehello = function (data) { + this.salt = data.toString('utf8').split('\n')[1].replaceAll(' ', ''); - rqFunc.apply(command[3], args) - .then(cb[0]) - .catch(cb[1]) - } - } -} + this.connector.socket.on('data', this.mpDecoderStream.write.bind(this.mpDecoderStream)); -const unpackrOpts = { - mapsAsObjects: true, - int64AsType: "auto", + if (this.options.password) { + this.setState(states.AUTH); + this._auth(this.options.username, this.options.password, { + [bypassOfflineQueueSym]: true + }) + .then(() => { + debug('authenticated [%s]', this.options.username); + this.retryAttempts = 0; + this.setState( + states.CONNECT, + pick(this.options, ['port', 'host', 'path']) + ); + }) + .catch((err) => { + debug('failed to authenticate: %O', err); + this.errorHandler(err); + return exports.closeHandler.call(this, err); + }); + } else { + this.retryAttempts = 0; + this.setState(states.CONNECT, pick(this.options, ['port', 'host', 'path'])); + } }; -function createUnpackrStream () { - var _this = this; - var decodedHeaders = {}; - var decodingStep = 0; - - var unpackrStream = new UnpackrStream(unpackrOpts); - - unpackrStream.on("error", function (error) { - _this.errorHandler( - new TarantoolError("Msgpack unpacker errored", { - cause: error, - }) - ); - }); - - unpackrStream.on("data", function (data) { - var type = typeof data; - switch (type) { - case "number": - decodingStep = 0; - break; - case "object": - switch (decodingStep) { - case 0: - decodedHeaders = data; - decodingStep = 1; - break; - case 1: - _this.processResponse(decodedHeaders, data); - decodingStep = 2; - break; - default: - _this.errorHandler( - new TarantoolError( - "Unknown step detected while decoding response data, maybe network loss occured with some bytes?" - ) - ); - } - break; - default: - _this.errorHandler( - new TarantoolError( - "Type of decoded value does not satisfy requirements: " + type - ) - ); - } - }); - - return unpackrStream; -} +/** + * Handles errors from socket + * @private + * @param {Error} error - Error object + */ +const errorHandler = function (error) { + debug('handled error: %s', error); + this.silentEmit('error', error); -exports.dataHandler = function () { - const unpackrStream = createUnpackrStream.call(this); - return (data) => { - switch (this.dataState) { - case states.PREHELLO: - this.salt = data.toString("utf8").split('\n')[1].replaceAll(' ', ''); - this.dataState = states.CONNECTED; - this.setState(states.CONNECTED); - exports.connectHandler.call(this); - break; - case states.CONNECTED: - unpackrStream.write(data) - break; - } - }; -}; - -function errorHandler (error){ - debug('error: %s', error); - this.silentEmit('error', error); + // check if error relates to the pending connection process + if (this.pendingPromises.connect) { + this.connectionErrors.push(error); + } }; exports.errorHandler = errorHandler; -exports.closeHandler = function () { - function close () { - this.setState(states.END); - this.flushQueue(new TarantoolError('Connection is closed.')); - } +const ERR_DRAINING_TIMED_OUT = new TarantoolError( + 'Timed out awaiting for sent command for being fulfilled before disconnect' +); +const ERR_MANUALLY_CLOSED = new TarantoolError('manually closed'); + +/** + * Handles socket close event + * @private + * @param {boolean|Error} [error] - Error object or boolean indicating transmission error + * @param {boolean} [graceful] - Whether to wait for pending commands + */ +exports.closeHandler = async function (error, graceful) { + // check if had a transmission error + // https://nodejs.org/api/net.html#event-close_1 + if (error === true) {error = new TarantoolError('Socket transmission error');} + // consider this is a duplicate after .quit() or .disconnect() + // which may be inited by the socket 'close' listener + if (this._state[0] & states.END && error != undefined) {return;} - return function(){ - process.nextTick(this.emit.bind(this, 'close')); - if (this.manuallyClosing) { - this.manuallyClosing = false; - debug('skip reconnecting since the connection is manually closed.'); - return close(); - } - if (typeof this.options.retryStrategy !== 'function') { - debug('skip reconnecting because `retryStrategy` is not a function'); - return close(); - } - var retryDelay = this.options.retryStrategy(++this.retryAttempts); + const close = closeConnection.bind(this); - if (typeof retryDelay !== 'number') { - debug('skip reconnecting because `retryStrategy` doesn\'t return a number'); - return close(); - } - this.setState(states.RECONNECTING, retryDelay); - if (this.options.reserveHosts) { - if (this.retryAttempts-1 == this.options.beforeReserve){ - this.useNextReserve(); - this.connect().catch(function(){}); - return; - } - } - debug('reconnect in %sms', retryDelay); + // check if this was a manual disconnection + if (this._state[0] & states.END) { + debug( + 'skip reconnecting since the connection is manually closed (or has been closed before)' + ); - this.reconnectTimeout = setTimeout(function () { - this.reconnectTimeout = null; - this.connect().catch(function(){}); - }, retryDelay); - }; -}; \ No newline at end of file + if (graceful) { + return ( + this._awaitSentCommandsDrain() + // if some commands are still not fulfilled + .catch(() => { + debug( + 'Timed out awaiting for sent commands responses, rejecting them' + ); + rejectSentCommands.call(this, ERR_DRAINING_TIMED_OUT); + }) + .finally(() => close(ERR_MANUALLY_CLOSED)) + ); + } else { + return close(ERR_MANUALLY_CLOSED); + } + } + + // consider this close event was not planned or manually invoked + return tryToReconnect.call(this); +}; diff --git a/lib/parser.js b/lib/parser.js index 2eccfdd..fd9589a 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,150 +1,135 @@ -var { KeysCode, RequestCode } = require('./const'); -var { TarantoolError } = require('./utils'); -var debug = require('debug')('tarantool-driver:parser'); +const { KeysCode, RequestCode } = require('./const'); +const { TarantoolError } = require('./errors'); +const { ReplyError } = require('./errors'); +const debug = require('./utils/debug').extend('parser'); +const PreparedStatement = require('./PreparedStatement'); -exports.processResponse = function(headers, data){ - var schemaId = headers[KeysCode.schema_version] - var reqId = headers[KeysCode.sync] - var code = headers[KeysCode.code] - debug(`processing response for request №${reqId}; code: ${code}, data: `, data, ', headers: ', headers) +exports.processResponse = function (headers, data) { + const schemaId = headers[KeysCode.schema_version]; + const reqId = headers[KeysCode.sync]; + const code = headers[KeysCode.code]; + debug('processing response for request № %i; code: %i, data: %O; headers: %O', reqId, code, data, headers); - if (this.schemaId) { + this.schemaId ||= schemaId; if (this.schemaId != schemaId) { - this.schemaId = schemaId; - this.namespace = {}; + this.schemaId = schemaId; + this.offlineQueue.set(); + this.fetchSchema().then(() => { + this.offlineQueue.unset(); + this.offlineQueue.send(); + }); } - } else { - this.schemaId = schemaId; - } - var task = this.sentCommands.get(reqId); - this.sentCommands.delete(reqId); - var dfd = task && task[1]; - var timeoutId = task && task[3]; - - if (timeoutId) clearTimeout(timeoutId); - - if (!dfd) { - return this.errorHandler( - new TarantoolError( - `Request with id ${reqId} was not found, maybe a duplicate` - ) - ); - } - if (code === 0) { - dfd[0](this._returnBool(task, data)); - } else { - var tarantoolErrorObject = data[KeysCode.iproto_error] && data[KeysCode.iproto_error][0x00][0] - var errCode = tarantoolErrorObject && tarantoolErrorObject[0x05] - var errDecription = (tarantoolErrorObject && tarantoolErrorObject[0x03]) || data[KeysCode.iproto_error_24] - - switch (errCode) { - case 7: /* ER_READONLY */ - case 116: /* ER_LOADING */ - switch (this.options.nonWritableHostPolicy) { - case 2: - // should remake 'attemptsCount' - // var attemptsCount = task[2].attempt; - // if (attemptsCount) { - // task[2].attempt++; - // } else { - // task[2].attempt = 1; - // } + const task = this.sentCommands.get(reqId); + if (!task) { + return this.errorHandler( + new TarantoolError( + `Received request with id №${reqId}, but the corresponding callback was not found. Maybe a duplicate or protocol error?` + ) + ); + } + this.sentCommands.delete(reqId); + const dfd = task[1]; + task[3]?.clear(); // timeoutId - // if (this.options.maxRetriesPerRequest <= attemptsCount) { - // return dfd[1](new TarantoolError(errDecription)); - // } + if (code === 0) { + dfd(null, this._returnBool(task, data)); + } else { + let error; + if (data[KeysCode.iproto_error]) { + const { + 0: type, + 1: file, + 2: line, + 3: message, + 5: errno, + 6: fields + } = data[KeysCode.iproto_error][0][0]; - this.offlineQueue.push([ - task[0], task[1], task[2] - ]); - return changeHost.call(this, errDecription); - case 1: - changeHost.call(this, errDecription); - default: - dfd[1](new TarantoolError(errDecription)); + error = new ReplyError(message, { + type, + file, + line, + errno, + fields + }); + } else { + error = new ReplyError(data[KeysCode.iproto_error_24]); } - break; - default: - if (reqId) return dfd[1](new TarantoolError(errDecription)); + + if (reqId) {return dfd(error, null);} this.errorHandler( - new TarantoolError( - "Processed response with an unsuccessful response code: " + errDecription - ) + new TarantoolError( + 'Unprocessed response with an unsuccessful response code', + { + cause: error + } + ) ); } - } }; -exports._returnBool = function _returnBool (task, data){ - var cmd = task[0]; - switch (cmd){ - case RequestCode.rqAuth: - case RequestCode.rqPing: - return true; - case RequestCode.rqExecute: - if (data[KeysCode.metadata]) { - var res = []; - var meta = data[KeysCode.metadata]; - var rows = data[KeysCode.data]; - for (var i = 0; i < rows.length; i++) { - var formattedRow = {}; - for (var j = 0; j < meta.length; j++ ) { - formattedRow[meta[j][0x00]] = rows[i][j]; - } - res.push(formattedRow); - } - return res; - } else { - return 'Affected row count: ' + (data[KeysCode.sql_info][0x0] || 0); - } - case RequestCode.rqPrepare: - return data[KeysCode.stmt_id]; - case RequestCode.rqId: - return { - version: data[KeysCode.iproto_version], - features: data[KeysCode.iproto_features], - auth_type: data[KeysCode.iproto_auth_type] - }; - case RequestCode.rqSelect: - case RequestCode.rqInsert: - case RequestCode.rqReplace: - case RequestCode.rqUpdate: - case RequestCode.rqDelete: - if (task[4]) { - // should load the new space shema in case of change - if (!this.namespace[task[2][0]]) { - const err = new TarantoolError('Failed to convert tuple to object'); - err.tuples = data[KeysCode.data]; - throw err; - } - return convertTupleToObject(data[KeysCode.data], this.namespace[task[2][0]].tupleKeys) - }; - default: - return data[KeysCode.data]; - } -} - -function changeHost (errDecription) { - this.setState(512, errDecription) // event 'changing_host' - this.useNextReserve() - this.disconnect(true) -} +exports._returnBool = function _returnBool(task, data) { + const cmd = task[0]; + switch (cmd) { + case RequestCode.rqAuth: + case RequestCode.rqPing: + return true; + case RequestCode.rqExecute: + if (data[KeysCode.metadata]) { + const res = []; + const meta = data[KeysCode.metadata]; + const rows = data[KeysCode.data]; + for (let i = 0; i < rows.length; i++) { + const formattedRow = {}; + for (let j = 0; j < meta.length; j++) { + formattedRow[meta[j][0x00]] = rows[i][j]; + } + res.push(formattedRow); + } + return res; + } else { + return ( + 'Affected row count: ' + (data[KeysCode.sql_info][0x0] || 0) + ); + } + case RequestCode.rqPrepare: + return new PreparedStatement(data[KeysCode.stmt_id]); + case RequestCode.rqId: + return { + version: data[KeysCode.iproto_version], + features: data[KeysCode.iproto_features], + auth_type: data[KeysCode.iproto_auth_type] + }; + case RequestCode.rqSelect: + case RequestCode.rqInsert: + case RequestCode.rqReplace: + case RequestCode.rqUpdate: + case RequestCode.rqDelete: + if (task[4].tupleToObject ?? this.options.tupleToObject) { + return convertTupleToObject( + data[KeysCode.data], + this.namespace[task[2][0]].tupleKeys + ); + } + // eslint-disable-next-line no-fallthrough + default: + return data[KeysCode.data]; + } +}; -function convertTupleToObject (rows, tupleKeys) { - return rows.map(function (row) { - return createObject(tupleKeys, row); - }) +function convertTupleToObject(rows, tupleKeys) { + return rows.map(function (row) { + return createObject(tupleKeys, row); + }); } -function createObject (keys, values) { - var num = 0; - var obj = {}; - for (var key of keys) { - obj[key] = values[num] - num++ - } +function createObject(keys, values) { + const obj = {}; + values.map((value, index) => { + obj[keys[index]] = value; + }); - return obj; -} \ No newline at end of file + return obj; +} diff --git a/lib/pipeline/AutoPipeliner.js b/lib/pipeline/AutoPipeliner.js new file mode 100644 index 0000000..dccab7b --- /dev/null +++ b/lib/pipeline/AutoPipeliner.js @@ -0,0 +1,30 @@ +module.exports = class AutoPipeliner { + queue = []; + autoPipeliningStarted = false; + + constructor (parent) { + this.parent = parent; + } + + add (buffer) { + this.queue.push(buffer); + // check if auto pipelining is already started in order to don't execute 'setImmediate' every time + if (!this.autoPipeliningStarted) { + process.nextTick(() => this.process()) + this.autoPipeliningStarted = true; + } + } + + process () { + const concatenatedBuffer = Buffer.concat(this.queue); + this.reset(); + this.parent.connector.write(concatenatedBuffer, null, () => { + this.parent.bufferPool.releaseBuffer(concatenatedBuffer); + }); + this.autoPipeliningStarted = false; + }; + + reset () { + this.queue = []; + } +} \ No newline at end of file diff --git a/lib/pipeline/Pipeline.js b/lib/pipeline/Pipeline.js new file mode 100644 index 0000000..1b4424f --- /dev/null +++ b/lib/pipeline/Pipeline.js @@ -0,0 +1,109 @@ +const Command = require('../Command'); +const PipelineResponse = require('./PipelineResponse'); +const { noop } = require('lodash'); +const {symbols: { + pipelined: pipelinedSym +}} = require('../const'); + +class Pipeline { + pipelinedCommands = []; + + /** + * Creates a new Pipeline instance + * @param {TarantoolConnection} self - Parent connection instance + */ + constructor(self) { + this._parent = self; + } + + /** + * Executes all pipelined commands in a single batch + * @public + * @returns {Promise} Response object containing results for each command + */ + exec() { + // if not connected + if (!this._parent.isConnectedState()) this._parent.connect().catch(noop); + // wait until connection is fully established + if (this._parent.pendingPromises?.connect) { + return this._parent.pendingPromises.connect + .then(() => this.exec()); + }; + + const promises = []; + const buffers = []; + for (const [command, args] of this.pipelinedCommands) { + promises.push( + new Promise((resolve) => { + try { + const func = command.function; + const originalCb = args[command.argsLen]; + args[command.argsLen] = function cbInterceptor (error, result) { + if (error) { + resolve([error, null]); + } else { + resolve([null, result]); + } + + if (originalCb) originalCb(error, result); + } + const buffer = func.apply(this._parent, args); + buffers.push(buffer); + } catch (error) { + resolve([error, null]); + } + }) + ); + } + + this.flushPipelined(); + const concatenatedBuffer = Buffer.concat(buffers); + this._parent.connector.write(concatenatedBuffer, null, () => { + this._parent.bufferPool.releaseBuffer(concatenatedBuffer); + }); + + return Promise.all(promises) + .then(function (result) { + return new PipelineResponse(result); + }); + } + + /** + * Clears the pipelined commands queue + * @public + */ + flushPipelined() { + this.pipelinedCommands = []; + } +} + +Command.list.map((name) => { + Pipeline.prototype[name] = function commandInterceptor() { + const optsPos = Command.commands[name].optsPos; + const args = [...arguments]; + if (args[optsPos]) { + args[optsPos][pipelinedSym] = true; + } else { + args[optsPos] = { + [pipelinedSym]: true + }; + } + + this.pipelinedCommands.push([ + Command.commands[name], + args + ]); + return this; + }; +}); + +/** + * Creates a new Pipeline instance + * Allows queuing commands for batch execution + * May be created only once and reused multiple times + * @public + * @returns {Pipeline} Pipeline instance + */ +module.exports.pipeline = function () { + return new Pipeline(this); +}; \ No newline at end of file diff --git a/lib/pipeline/PipelineResponse.js b/lib/pipeline/PipelineResponse.js new file mode 100644 index 0000000..f68092d --- /dev/null +++ b/lib/pipeline/PipelineResponse.js @@ -0,0 +1,36 @@ +const { TarantoolError } = require("../errors"); + +function findPipelineError (array) { + const error = (array || this).find(element => element[0]); + return error ? error[0] : null; +}; + +function findPipelineErrors (array) { + const a = []; + for (const subarray of (array || this)) { + const errored_element = subarray[0] + if (errored_element) a.push(errored_element) + } + + return a; +}; + +class PipelineResponse extends Array { + findPipelineError = findPipelineError; + findPipelineErrors = findPipelineErrors; + static findPipelineError = findPipelineError; + static findPipelineErrors = findPipelineErrors; + + constructor (arr) { + super(...arr); + }; + + get pipelineError () { + return this.findPipelineError() + }; + + get pipelineErrors () { + return this.findPipelineErrors() + } +} +module.exports = PipelineResponse; \ No newline at end of file diff --git a/lib/utils/FastTimer.js b/lib/utils/FastTimer.js new file mode 100644 index 0000000..2b43103 --- /dev/null +++ b/lib/utils/FastTimer.js @@ -0,0 +1,427 @@ +'use strict' + +// Copied from 'undici' + +/** + * This module offers an optimized timer implementation designed for scenarios + * where high precision is not critical. + * + * The timer achieves faster performance by using a low-resolution approach, + * with an accuracy target of within 500ms. This makes it particularly useful + * for timers with delays of 1 second or more, where exact timing is less + * crucial. + * + * It's important to note that Node.js timers are inherently imprecise, as + * delays can occur due to the event loop being blocked by other operations. + * Consequently, timers may trigger later than their scheduled time. + */ + +/** + * The fastNow variable contains the internal fast timer clock value. + * + * @type {number} + */ +let fastNow = 0 + +/** + * RESOLUTION_MS represents the target resolution time in milliseconds. + * + * @type {number} + * @default 1000 + */ +const RESOLUTION_MS = 1e3 + +/** + * TICK_MS defines the desired interval in milliseconds between each tick. + * The target value is set to half the resolution time, minus 1 ms, to account + * for potential event loop overhead. + * + * @type {number} + * @default 499 + */ +const TICK_MS = (RESOLUTION_MS >> 1) - 1 + +/** + * fastNowTimeout is a Node.js timer used to manage and process + * the FastTimers stored in the `fastTimers` array. + * + * @type {NodeJS.Timeout} + */ +let fastNowTimeout + +/** + * The kFastTimer symbol is used to identify FastTimer instances. + * + * @type {Symbol} + */ +const kFastTimer = Symbol('kFastTimer') + +/** + * The fastTimers array contains all active FastTimers. + * + * @type {FastTimer[]} + */ +const fastTimers = [] + +/** + * These constants represent the various states of a FastTimer. + */ + +/** + * The `NOT_IN_LIST` constant indicates that the FastTimer is not included + * in the `fastTimers` array. Timers with this status will not be processed + * during the next tick by the `onTick` function. + * + * A FastTimer can be re-added to the `fastTimers` array by invoking the + * `refresh` method on the FastTimer instance. + * + * @type {-2} + */ +const NOT_IN_LIST = -2 + +/** + * The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled + * for removal from the `fastTimers` array. A FastTimer in this state will + * be removed in the next tick by the `onTick` function and will no longer + * be processed. + * + * This status is also set when the `clear` method is called on the FastTimer instance. + * + * @type {-1} + */ +const TO_BE_CLEARED = -1 + +/** + * The `PENDING` constant signifies that the FastTimer is awaiting processing + * in the next tick by the `onTick` function. Timers with this status will have + * their `_idleStart` value set and their status updated to `ACTIVE` in the next tick. + * + * @type {0} + */ +const PENDING = 0 + +/** + * The `ACTIVE` constant indicates that the FastTimer is active and waiting + * for its timer to expire. During the next tick, the `onTick` function will + * check if the timer has expired, and if so, it will execute the associated callback. + * + * @type {1} + */ +const ACTIVE = 1 + +/** + * The onTick function processes the fastTimers array. + * + * @returns {void} + */ +function onTick () { + /** + * Increment the fastNow value by the TICK_MS value, despite the actual time + * that has passed since the last tick. This approach ensures independence + * from the system clock and delays caused by a blocked event loop. + * + * @type {number} + */ + fastNow += TICK_MS + + /** + * The `idx` variable is used to iterate over the `fastTimers` array. + * Expired timers are removed by replacing them with the last element in the array. + * Consequently, `idx` is only incremented when the current element is not removed. + * + * @type {number} + */ + let idx = 0 + + /** + * The len variable will contain the length of the fastTimers array + * and will be decremented when a FastTimer should be removed from the + * fastTimers array. + * + * @type {number} + */ + let len = fastTimers.length + + while (idx < len) { + /** + * @type {FastTimer} + */ + const timer = fastTimers[idx] + + // If the timer is in the ACTIVE state and the timer has expired, it will + // be processed in the next tick. + if (timer._state === PENDING) { + // Set the _idleStart value to the fastNow value minus the TICK_MS value + // to account for the time the timer was in the PENDING state. + timer._idleStart = fastNow - TICK_MS + timer._state = ACTIVE + } else if ( + timer._state === ACTIVE && + fastNow >= timer._idleStart + timer._idleTimeout + ) { + timer._state = TO_BE_CLEARED + timer._idleStart = -1 + timer._onTimeout(timer._timerArg) + } + + if (timer._state === TO_BE_CLEARED) { + timer._state = NOT_IN_LIST + + // Move the last element to the current index and decrement len if it is + // not the only element in the array. + if (--len !== 0) { + fastTimers[idx] = fastTimers[len] + } + } else { + ++idx + } + } + + // Set the length of the fastTimers array to the new length and thus + // removing the excess FastTimers elements from the array. + fastTimers.length = len + + // If there are still active FastTimers in the array, refresh the Timer. + // If there are no active FastTimers, the timer will be refreshed again + // when a new FastTimer is instantiated. + if (fastTimers.length !== 0) { + refreshTimeout() + } +} + +function refreshTimeout () { + // If the fastNowTimeout is already set and the Timer has the refresh()- + // method available, call it to refresh the timer. + // Some timer objects returned by setTimeout may not have a .refresh() + // method (e.g. mocked timers in tests). + if (fastNowTimeout?.refresh) { + fastNowTimeout.refresh() + // fastNowTimeout is not instantiated yet or refresh is not availabe, + // create a new Timer. + } else { + clearTimeout(fastNowTimeout) + fastNowTimeout = setTimeout(onTick, TICK_MS) + // If the Timer has an unref method, call it to allow the process to exit, + // if there are no other active handles. When using fake timers or mocked + // environments (like Jest), .unref() may not be defined, + fastNowTimeout?.unref() + } +} + +/** + * The `FastTimer` class is a data structure designed to store and manage + * timer information. + */ +class FastTimer { + [kFastTimer] = true + + /** + * The state of the timer, which can be one of the following: + * - NOT_IN_LIST (-2) + * - TO_BE_CLEARED (-1) + * - PENDING (0) + * - ACTIVE (1) + * + * @type {-2|-1|0|1} + * @private + */ + _state = NOT_IN_LIST + + /** + * The number of milliseconds to wait before calling the callback. + * + * @type {number} + * @private + */ + _idleTimeout = -1 + + /** + * The time in milliseconds when the timer was started. This value is used to + * calculate when the timer should expire. + * + * @type {number} + * @default -1 + * @private + */ + _idleStart = -1 + + /** + * The function to be executed when the timer expires. + * @type {Function} + * @private + */ + _onTimeout + + /** + * The argument to be passed to the callback when the timer expires. + * + * @type {*} + * @private + */ + _timerArg + + /** + * @constructor + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should wait + * before the specified function or code is executed. + * @param {*} arg + */ + constructor (callback, delay, arg) { + this._onTimeout = callback + this._idleTimeout = delay + this._timerArg = arg + + this.refresh() + } + + /** + * Sets the timer's start time to the current time, and reschedules the timer + * to call its callback at the previously specified duration adjusted to the + * current time. + * Using this on a timer that has already called its callback will reactivate + * the timer. + * + * @returns {void} + */ + refresh () { + // In the special case that the timer is not in the list of active timers, + // add it back to the array to be processed in the next tick by the onTick + // function. + if (this._state === NOT_IN_LIST) { + fastTimers.push(this) + } + + // If the timer is the only active timer, refresh the fastNowTimeout for + // better resolution. + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + } + + // Setting the state to PENDING will cause the timer to be reset in the + // next tick by the onTick function. + this._state = PENDING + } + + /** + * The `clear` method cancels the timer, preventing it from executing. + * + * @returns {void} + * @private + */ + clear () { + // Set the state to TO_BE_CLEARED to mark the timer for removal in the next + // tick by the onTick function. + this._state = TO_BE_CLEARED + + // Reset the _idleStart value to -1 to indicate that the timer is no longer + // active. + this._idleStart = -1 + } +} + +/** + * This module exports a setTimeout and clearTimeout function that can be + * used as a drop-in replacement for the native functions. + */ +module.exports = { + /** + * The setTimeout() method sets a timer which executes a function once the + * timer expires. + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should + * wait before the specified function or code is executed. + * @param {*} [arg] An optional argument to be passed to the callback function + * when the timer expires. + * @returns {NodeJS.Timeout|FastTimer} + */ + setTimeout (callback, delay, arg) { + // If the delay is less than or equal to the RESOLUTION_MS value return a + // native Node.js Timer instance. + return delay <= RESOLUTION_MS + ? setTimeout(callback, delay, arg) + : new FastTimer(callback, delay, arg) + }, + /** + * The clearTimeout method cancels an instantiated Timer previously created + * by calling setTimeout. + * + * @param {NodeJS.Timeout|FastTimer} timeout + */ + clearTimeout (timeout) { + // If the timeout is a FastTimer, call its own clear method. + if (timeout[kFastTimer]) { + /** + * @type {FastTimer} + */ + timeout.clear() + // Otherwise it is an instance of a native NodeJS.Timeout, so call the + // Node.js native clearTimeout function. + } else { + clearTimeout(timeout) + } + }, + /** + * The setFastTimeout() method sets a fastTimer which executes a function once + * the timer expires. + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should + * wait before the specified function or code is executed. + * @param {*} [arg] An optional argument to be passed to the callback function + * when the timer expires. + * @returns {FastTimer} + */ + setFastTimeout (callback, delay, arg) { + return new FastTimer(callback, delay, arg) + }, + /** + * The clearTimeout method cancels an instantiated FastTimer previously + * created by calling setFastTimeout. + * + * @param {FastTimer} timeout + */ + clearFastTimeout (timeout) { + timeout.clear() + }, + /** + * The now method returns the value of the internal fast timer clock. + * + * @returns {number} + */ + now () { + return fastNow + }, + /** + * Trigger the onTick function to process the fastTimers array. + * Exported for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + * @param {number} [delay=0] The delay in milliseconds to add to the now value. + */ + tick (delay = 0) { + fastNow += delay - RESOLUTION_MS + 1 + onTick() + onTick() + }, + /** + * Reset FastTimers. + * Exported for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + reset () { + fastNow = 0 + fastTimers.length = 0 + clearTimeout(fastNowTimeout) + fastNowTimeout = null + }, + /** + * Exporting for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + kFastTimer +} \ No newline at end of file diff --git a/lib/utils/SliderBuffer.js b/lib/utils/SliderBuffer.js new file mode 100644 index 0000000..14f6670 --- /dev/null +++ b/lib/utils/SliderBuffer.js @@ -0,0 +1,44 @@ +// It is faster to reuse the existing buffer than allocating a new one. + +module.exports = class SliderBuffer { + offset = 0; + isCleanupScheduled = false; + initialSize = 0; + + constructor (initialSize) { + if (typeof initialSize != 'number') throw new Error(`'initialSize' should be a number`); + if (initialSize < 2) throw new Error(`'initialSize' is too low`); + + this.initialSize = initialSize; + this.buffer = Buffer.allocUnsafe(initialSize); + } + + getBuffer (size) { + if (size > this.buffer.length - this.offset) { + this.buffer = Buffer.allocUnsafe(this.buffer.length * 2); + this.offset = 0; + this.scheduleCleanup() + } + + return this.buffer.subarray(this.offset, this.offset += size); + } + + scheduleCleanup() { + if (!this.isCleanupScheduled) { + this.isCleanupScheduled = true; + setImmediate(() => { + this.cleanupCache(); + this.isCleanupScheduled = false; + }); + } + } + + cleanupCache() { + this.buffer = Buffer.allocUnsafe(this.initialSize); + this.offset = 0; + } + + releaseBuffer(buffer) { + if (buffer.length > this.buffer.length) this.buffer = buffer; + } +} \ No newline at end of file diff --git a/lib/utils/debug.js b/lib/utils/debug.js new file mode 100644 index 0000000..2b5b8a4 --- /dev/null +++ b/lib/utils/debug.js @@ -0,0 +1,2 @@ +const debug = require('debug'); +module.exports = debug('tarantool-driver'); \ No newline at end of file diff --git a/lib/utils/index.js b/lib/utils/index.js new file mode 100644 index 0000000..eeb1d5e --- /dev/null +++ b/lib/utils/index.js @@ -0,0 +1,189 @@ +const { TarantoolError } = require('../errors'); +const { defaults, noop } = require('lodash'); +const { states, nullishOpts } = require('../const'); +const { format } = require('node:util'); +const debug = require('./debug').extend('utils'); +require('debug').formatters.h = bufferToHex; + +function parseURL(str) { + var result = {}; + if (str.startsWith('/')) { + result.path = str; + return result; + } + var parsed = str.split(':'); + switch (parsed.length) { + case 1: + result.host = parsed[0]; + break; + case 2: + result.host = parsed[0]; + result.port = parsed[1]; + break; + default: + result.username = parsed[0]; + result.password = parsed[1].split('@')[0]; + result.host = parsed[1].split('@')[1]; + result.port = parsed[2]; + } + return result; +} + +function withResolversPoly() { + let resolve = Promise.resolve; + let reject = Promise.reject; + const promise = new Promise(function (_resolve, _reject) { + resolve = _resolve; + reject = _reject; + }); + + return { + promise, + resolve, + reject + }; +} + +exports.withResolvers = Promise.withResolvers.bind(Promise) ?? withResolversPoly; + +exports.applyMixin = function applyMixin(derivedConstructor, mixinConstructor) { + Object.getOwnPropertyNames(mixinConstructor.prototype).forEach((name) => { + Object.defineProperty( + derivedConstructor.prototype, + name, + Object.getOwnPropertyDescriptor(mixinConstructor.prototype, name) + ); + }); +}; + +function bufferToHex (v) { + return v.toString('hex'); +}; +module.exports.bufferToHex = bufferToHex; + +module.exports.parseOptions = function parseOptions() { + const options = {}; + for (let i = 0; i < arguments.length; ++i) { + var arg = arguments[i]; + if (arg == null) { + continue; + } + + switch (typeof arg) { + case 'object': + defaults(options, arg); + break; + case 'string': + if (!isNaN(arg) && (parseFloat(arg) | 0) === parseFloat(arg)) { + options.port = arg; + continue; + } + defaults(options, parseURL(arg)); + break; + case 'number': + options.port = arg; + break; + default: + throw new TarantoolError('Invalid argument: ' + arg); + } + } + + if (typeof options.port === 'string') { + options.port = parseInt(options.port, 10); + } + defaults(options, nullishOpts); + if (options.path) { + options.port = null; + options.host = null; + } + if (options.port) options.path = null; + + return options; +}; + +module.exports.tryToReconnect = function tryToReconnect() { + this.offlineQueue.set(); + const close = module.exports.closeConnection.bind(this); + + if (typeof this.options.retryStrategy !== 'function') { + debug('skip reconnecting because `retryStrategy` is not a function'); + return close(new TarantoolError(`'retryStrategy' is not a function`)); + } + const retryDelay = this.options.retryStrategy(++this.retryAttempts); + + if (typeof retryDelay !== 'number') { + const err = format( + '\'retryStrategy\' doesn\'t return a number. Received value: ', + retryDelay + ); + debug(err); + return close(new TarantoolError(err)); + } + + this.setState(states.RECONNECTING, retryDelay); + this.pendingPromises.connect = null; + if (this.connector?.socket) this.connector?.disconnect(); // close the existing connection + + const useReserve = this.retryAttempts - 1 >= this.options.beforeReserve; + if (this.options.reserveHosts?.length && useReserve) { + try { + const nextReserveHost = this.useNextReserve(); + debug( + 'reconnecting to the next reserve host in %sms: %O', + retryDelay, + nextReserveHost + ); + } catch (e) { + debug('Got error while choosing next reserve host: %O', e); + return close(e) + } + } else if (useReserve) { + return close( + new TarantoolError( + `All ${this.options.beforeReserve} connection attempts were failed and no 'reserveHosts' are specified`, { + cause: this.connectionErrors + } + ) + ); + } else { + debug('reconnecting to the same host in %sms', retryDelay); + } + + setTimeout(() => { + // don't reconnect if connection was closed manually before new reconnection attempt + if (this._state[0] & states.END) return; + + this.connect().catch(noop); + }, retryDelay); +}; + +module.exports.closeConnection = function closeConnection(error) { + this.setState(states.END); + this.offlineQueue.flush( + new TarantoolError('Connection is closed.', { + cause: error + }) + ); + rejectSentCommands.call(this, ERR_UNGRACEFUL_FLUSH); + process.nextTick(() => { + this.emit('close'); + }); + if (this.connector?.socket) this.connector?.disconnect(() => { + // maybe add some logic? + }); +}; + +const ERR_UNGRACEFUL_FLUSH = new TarantoolError('Flushed the commands queue not gracefully'); +/** + * Rejects all pending commands with an error + * @private + */ +const rejectSentCommands = function (err) { + for (const [key, value] of this.sentCommands.entries()) { + debug('Rejecting unresolved request with reqId № %i', key) + + value[1](err); + } + this.sentCommands.clear(); +} +module.exports.rejectSentCommands = rejectSentCommands; \ No newline at end of file diff --git a/package.json b/package.json index a007db6..e6efd83 100755 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "lint": "eslint ./lib ./test", - "test": "istanbul cover --report lcov _mocha", + "test": "echo 'Make sure you started the \"assets/box.lua\" instance first!' && node test/commander && node test/msgpack_extensions && node test/app && node test/autopipeline && node test/pipeline && node test/transaction", "coveralls": "cat ./coverage/lcov.info | coveralls", "benchmark-read": "node benchmark/read", "benchmark-write": "node benchmark/write" @@ -27,30 +27,29 @@ "in-memory storage", "in memory database", "in-memory database", - "database" + "database", + "client", + "connector" ], "author": "KlonD90", + "contributors": [ + "goodwise (https://github.com/goodwise)" + ], "license": "MIT", "bugs": { "url": "https://github.com/tarantool/node-tarantool-driver/issues" }, "homepage": "https://github.com/tarantool/node-tarantool-driver", "dependencies": { - "debug": "^2.6.8", - "int64-buffer": "^1.0.1", - "lodash": "^4.17.4", - "uuid": "^9.0.0", - "msgpackr": "^1.11.2" + "debug": "^4.4.3", + "lodash": "^4.17.23", + "uuid": "^13.0.0", + "msgpackr": "^1.11.8" }, "devDependencies": { - "benchmark": "^2.1.4", - "chai": "^4.1.0", - "coveralls": "^2.13.3", - "eslint": "^2.5.3", - "istanbul": "^0.4.5", - "mocha": "^2.2.4", - "nanotimer": "^0.3.14", - "nyc": "^15.1.0", - "sinon": "^3.0.0" + "tinybench": "^6.0.0", + "coveralls": "^3.1.1", + "eslint": "^10.0.0", + "@eslint/js": "^10.0.1" } } diff --git a/test/app.js b/test/app.js index e5acc5a..891bbd6 100755 --- a/test/app.js +++ b/test/app.js @@ -1,746 +1,607 @@ /** - * Created by klond on 05.04.15. + * Test suite for TarantoolConnection */ -/*eslint-env mocha */ -/* global Promise */ -var exec = require('child_process').exec; -var expect = require('chai').expect; -var sinon = require('sinon'); -var stub = sinon.stub.bind(sinon); - -var assert = require('assert'); -var TarantoolConnection = require('../lib/connection'); -var mlite = require('msgpack-lite'); -var conn; - -describe('constructor', function () { - it('should parse options correctly', function () { - stub(TarantoolConnection.prototype, 'connect').returns(Promise.resolve()); - var option; - try { - option = getOption(33013); - expect(option).to.have.property('port', 33013); - expect(option).to.have.property('host', 'localhost'); +const { test, describe, before, after } = require('node:test'); +const assert = require('node:assert'); +const TarantoolConnection = require('../lib/connection'); +const { states } = require('../lib/const'); +const { noop } = require('lodash'); +const { + setTimeout: promisedSetTimeout, +} = require('node:timers/promises'); +let conn; - option = getOption('33013'); - expect(option).to.have.property('port', 33013); +// Global error handlers for better debugging +process.on('unhandledRejection', (error, promise) => { + console.error('❌ Unhandled Rejection:', error); +}); - option = getOption(33013, '192.168.0.1'); - expect(option).to.have.property('port', 33013); - expect(option).to.have.property('host', '192.168.0.1'); +process.on('uncaughtException', (error) => { + console.error('❌ Uncaught Exception:', error); + process.exit(1); +}); - option = getOption(33013, '192.168.0.1', { - password: '123', - username: 'userloser' - }); - expect(option).to.have.property('port', 33013); - expect(option).to.have.property('host', '192.168.0.1'); - expect(option).to.have.property('password', '123'); - expect(option).to.have.property('username', 'userloser'); - - option = getOption('mail.ru:33013'); - expect(option).to.have.property('port', 33013); - expect(option).to.have.property('host', 'mail.ru'); - - option = getOption('notguest:sesame@mail.ru:33013'); - expect(option).to.have.property('port', 3301); - expect(option).to.have.property('host', 'mail.ru'); - expect(option).to.have.property('username', 'notguest'); - expect(option).to.have.property('password', 'sesame'); - - option = getOption('/tmp/tarantool-test.sock'); - expect(option).to.have.property('path', '/tmp/tarantool-test.sock'); - - option = getOption({ - port: 33013, - host: '192.168.0.1' - }); - expect(option).to.have.property('port', 33013); - expect(option).to.have.property('host', '192.168.0.1'); +// mainly for 'constructor' tests +const optLazyConnect = { + lazyConnect: true +}; +// load fast and save on network bandwith +const optDontPrefetchSchema = { + prefetchSchema: false +}; - option = getOption({ - port: 33013, - host: '192.168.0.1', - reserveHosts: ['notguest:sesame@mail.ru:33013', 'mail.ru:33013'] - }); - expect(option).to.have.property('port', 33013); - expect(option).to.have.property('host', '192.168.0.1'); - expect(option).to.have.property('reserveHosts'); - expect(option.reserveHosts).to.deep.equal(['notguest:sesame@mail.ru:33013', 'mail.ru:33013']); - - option = new TarantoolConnection({ - port: 33013, - host: '192.168.0.1', - reserveHosts: ['notguest:sesame@mail.ru:33013', 'mail.ru:33013'] - }); - expect(option.reserve).to.deep.include( - { - port: 33013, - host: '192.168.0.1', - path: null, - username: null, - password: null - }, - { - port: 3301, - host: 'mail.ru' - }, - { - port: 3301, - host: 'mail.ru', - username: 'notguest', - password: 'sesame' - } - ); - - option = getOption({ - port: 33013, - host: '192.168.0.1' - }); - expect(option).to.have.property('port', 33013); - expect(option).to.have.property('host', '192.168.0.1'); +const truncateSpace = async (conn, spaceName) => { + return conn.sql(`DELETE FROM "${spaceName}" INDEXED BY "tree_idx" WHERE true`); +}; - option = getOption({ - port: '33013' - }); - expect(option).to.have.property('port', 33013); +describe('constructor', () => { + test('should parse options correctly', () => { + try { + let option; - option = getOption(33013, { - host: '192.168.0.1' - }); - expect(option).to.have.property('port', 33013); - expect(option).to.have.property('host', '192.168.0.1'); + option = getOption(33013); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, 'localhost'); - option = getOption('33013', { - host: '192.168.0.1' - }); - expect(option).to.have.property('port', 33013); - } catch (err) { - TarantoolConnection.prototype.connect.restore(); - throw err; - } - TarantoolConnection.prototype.connect.restore(); - - function getOption() { - conn = TarantoolConnection.apply(null, arguments); - return conn.options; - } - }); + option = getOption('33013'); + assert.strictEqual(option.port, 33013); - it('should throw when arguments is invalid', function () { - expect(function () { - new TarantoolConnection(function () {}); - }).to.throw(Error); - }); + option = getOption(33013, '192.168.0.1'); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, '192.168.0.1'); + + option = getOption(33013, '192.168.0.1', { + password: '123', + username: 'userloser' + }); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, '192.168.0.1'); + assert.strictEqual(option.password, '123'); + assert.strictEqual(option.username, 'userloser'); + + option = getOption('mail.ru:33013'); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, 'mail.ru'); + + option = getOption('notguest:sesame@mail.ru:33013'); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, 'mail.ru'); + assert.strictEqual(option.username, 'notguest'); + assert.strictEqual(option.password, 'sesame'); + + option = getOption('/tmp/tarantool-test.sock'); + assert.strictEqual(option.path, '/tmp/tarantool-test.sock'); + + option = getOption({ + port: 33013, + host: '192.168.0.1' + }); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, '192.168.0.1'); + + option = getOption({ + port: 33013, + host: '192.168.0.1', + reserveHosts: ['notguest:sesame@mail.ru:33013', 'mail.ru:33013'] + }); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, '192.168.0.1'); + assert.ok(option.reserveHosts); + assert.deepStrictEqual(option.reserveHosts, [ + 'notguest:sesame@mail.ru:33013', + 'mail.ru:33013' + ]); + + // option = new TarantoolConnection({ + // port: 33013, + // host: '192.168.0.1', + // reserveHosts: [ + // 'notguest:sesame@mail.ru:33013', + // 'mail.ru:33013' + // ], + // ...optLazyConnect, + // ...optDontPrefetchSchema + // }); + + option = getOption({ + port: 33013, + host: '192.168.0.1' + }); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, '192.168.0.1'); + + option = getOption({ + port: '33013' + }); + assert.strictEqual(option.port, 33013); + + option = getOption(33013, { + host: '192.168.0.1' + }); + assert.strictEqual(option.port, 33013); + assert.strictEqual(option.host, '192.168.0.1'); + + option = getOption('33013', { + host: '192.168.0.1' + }); + assert.strictEqual(option.port, 33013); + } catch (e) { + console.error('Failed to parse options: ', e); + throw e; + } + + function getOption(a, b, c) { + // don't emit process warning while connecting + if (typeof a == 'object') a.lazyConnect = true; + if (typeof b == 'object') b.lazyConnect = true; + if (typeof c == 'object') c.lazyConnect = true; + if (a === undefined) a = optLazyConnect; + if (b === undefined) b = optLazyConnect; + if (c === undefined) c = optLazyConnect; + + conn = new TarantoolConnection(a, b, c); + return conn.options; + } + }); + + test('should throw when arguments are invalid', () => { + assert.throws(() => { + new TarantoolConnection(function () {}); + }, Error); + }); }); -describe('reconnecting', function () { - this.timeout(8000); - it('should pass the correct retry times', function (done) { - var t = 0; - new TarantoolConnection({ - port: 1, - retryStrategy: function (times) { - expect(times).to.eql(++t); - if (times === 3) { - done(); - return; - } - return 0; - } - }); - }); +describe('reconnecting', { timeout: 8000 }, () => { + test('should pass the correct retry times', async () => { + let t = 0; + let finished = false; + let lastError = null; - it('should skip reconnecting when retryStrategy doesn\'t return a number', function (done) { - conn = new TarantoolConnection({ - port: 1, - retryStrategy: function () { - process.nextTick(function () { - expect(conn.state).to.eql(32); // states.END == 32 - done(); - }); - return null; - } - }); - }); + conn = new TarantoolConnection({ + port: 1, + retryStrategy: (times) => { + try { + assert.strictEqual(times, ++t); + if (times === 3) { + finished = true; + return; + } + return 0; + } catch (err) { + lastError = err; + console.error('❌ retryStrategy assertion failed:', { + expected: t, + actual: times, + message: err.message, + stack: err.stack + }); + } + } + }); - it('should not try to reconnect when disconnected manually', function (done) { - conn = new TarantoolConnection(33013, { lazyConnect: true }); - conn.eval('return func_foo()') - .then(function () { - conn.disconnect(); - return conn.eval('return func_foo()'); - }) - .catch(function (err) { - expect(err.message).to.match(/Connection is closed/); - done(); - }); - }); - it('should try to reconnect and then connect eventially', function (done){ - function timer(){ - return conn.ping() - .then(function(res){ - assert.equal(res, true); - done(); - }) - .catch(function(err){ - done(err); - }); - } - conn = new TarantoolConnection(33013, { lazyConnect: true }); - conn.eval('return func_foo()') - .then(function () { - exec('docker kill tarantool', function(error, stdout, stderr){ - if(error){ - done(error); - } - conn.eval('return func_foo()') - .catch(function(err){ - expect(err.message).to.match(/connect ECONNREFUSED/); - }); - exec('docker start tarantool', function(e, stdo, stde){ - if(error){ - done(error); - } - setTimeout(timer, 1000); - }); - }); - }); - }); + conn.on('error', noop); // don't emit the process warning + + await new Promise((resolve) => setTimeout(resolve, 200)); + + conn.disconnect(); + + if (lastError) { + console.error( + 'Test failed with error from retryStrategy:', + lastError + ); + throw lastError; + } + + assert.ok( + finished, + `Expected retryStrategy to complete (finished=${finished}, t=${t})` + ); + }); + + test("should skip reconnecting when retryStrategy doesn't return a number", async () => { + let finished = false; + let lastError = null; + + conn = new TarantoolConnection({ + port: 1, + ...optDontPrefetchSchema, + retryStrategy: () => { + process.nextTick(() => { + try { + assert.strictEqual(conn._state[0], states.END); + finished = true; + } catch (err) { + lastError = err; + console.error('❌ Assertion failed in nextTick:', { + actual: conn._state[0], + expected: states.END, + message: err.message, + stack: err.stack + }); + } + }); + return null; + } + }); + + conn.on('error', noop); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + if (lastError) { + console.error('Test failed:', lastError); + throw lastError; + } + + assert.ok( + finished, + `Expected state check to complete (finished=${finished}, state=${conn._state[0]})` + ); + + conn.disconnect(); + }); + + test('should not try to reconnect when disconnected manually', async () => { + try { + conn = new TarantoolConnection(3301, { + lazyConnect: false, + ...optDontPrefetchSchema + }); + await conn.eval('return func_arg(...)', ['test']); + conn.disconnect(); + + await assert.rejects( + async () => conn.eval('return func_arg(...)', ['test']), + { message: 'Connection is finished' } + ); + } catch (err) { + console.error('❌ Test failed with error:', err); + throw err; + } + }); + + test('should try to reconnect and then connect eventually', async () => { + conn = new TarantoolConnection(3301, { + ...optLazyConnect, + ...optDontPrefetchSchema + }); + await conn.connect(); + let res = await conn.ping(); + assert.strictEqual(res, true); + + // disrupt the connection + // imitating network error + conn.connector.socket.destroy(); + + await assert.rejects(() => conn.eval('return func_arg()'), { + message: 'Socket is not writable' + }); + + // socket 'close' event may be emitted not immediately + await promisedSetTimeout(100); + + res = await conn.ping(); + assert.strictEqual(res, true); + conn.disconnect(); + }); }); -describe('multihost', function () { - this.timeout(10000); - // after(function() { - // exec('docker start tarantool'); - // }); - var t; - it('should try to connect to reserve hosts cyclically', function(done){ - conn = new TarantoolConnection(33013, { - reserveHosts: ['test:test@127.0.0.1:33014', '127.0.0.1:33015'], - beforeReserve: 1, - retryStrategy: function (times) { - return Math.min(times * 500, 2000); - } - }); - t = 0; - conn.on('connect', function(){ - switch (t){ - case 1: - conn.eval('return box.cfg') - .then(function(res){ - t++; - expect(res[0].listen.toString()).to.eql('33014'); - exec('docker kill reserve', function(error, stdout, stderr){ - if(error){ - done(error); - } - }); - exec('docker start tarantool'); - }) - .catch(function(e){ - done(e); - }); - break; - case 2: - conn.eval('return box.cfg') - .then(function(res){ - t++; - expect(res[0].listen.toString()).to.eql('33015'); - exec('docker kill reserve_2', function(error, stdout, stderr){ - if(error){ - done(error); - } - }); - }) - .catch(function(e){ - done(e); - }); - break; - case 3: - conn.eval('return box.cfg') - .then(function(res){ - t++; - expect(res[0].listen.toString()).to.eql('33013'); - done(); - }) - .catch(function(e){ - done(e); - }); - break; - } - }); - conn.ping() - .then(function(){ - t++; - exec('docker kill tarantool', function(error, stdout, stderr){ - if(error){ - done(error); - } - }); - }) - .catch(function(e){ - done(e); - }); - }); +describe('multihost', { timeout: 10000 }, () => { + // Consider servers on port 33010 and UNIX-socket are unavailable + // Connector will try to find the alive one (which is on port 3301). + // This is a good example on how to pass 'reserveHosts' items in multiple ways + // and demonstrate that we can also change connection options per each host. + test('should try to connect to reserve hosts cyclically', async () => { + conn = new TarantoolConnection(33010 /* pass only port */, { + reserveHosts: [ + '/tmp/inactiveInstanceExample.sock', // may pass only UNIX socket path as a string + { + // pass object with a custom 'timeout' and credentials + host: 'localhost', + port: 3301, + timeout: 12345, + username: 'test', + password: 'notStrongPass :(' + } + ], + beforeReserve: 1, + ...optDontPrefetchSchema, + retryStrategy: (times) => { + return Math.min(times * 500, 2000); + } + }); + + conn.on('error', noop); + + let t = 0; + const connectionPromise = new Promise((resolve) => { + conn.on('reconnecting', () => { + const opts = conn.options; + try { + switch (t) { + case 0: + assert.equal(opts.port, 33010); + assert.equal(opts.host, 'localhost'); + assert.equal(opts.path, null); + assert.equal(opts.username, null); + assert.equal(opts.password, null); + break; + case 1: + assert.equal(opts.port, null); + assert.equal(opts.host, null); + assert.equal( + opts.path, + '/tmp/inactiveInstanceExample.sock' + ); + assert.equal(opts.username, null); + assert.equal(opts.password, null); + break; + case 2: + assert.equal(opts.port, 3301); + assert.equal(opts.host, 'localhost'); + assert.equal(opts.path, null); + assert.equal(opts.username, 'test'); + assert.equal(opts.password, 'notStrongPass :('); + assert.equal(opts.timeout, 12345); + resolve(); + break; + } + } catch (e) { + reject(e); + } + t++; + }); + }); + + await conn.ping(); + return connectionPromise + .finally(() => conn.disconnect()); + }); }); -describe('lazy connect', function(){ - beforeEach(function(){ - conn = new TarantoolConnection({port: 33013, lazyConnect: true, username: 'test', password: 'test'}); +describe('lazy connect', () => { + before(() => { + conn = new TarantoolConnection({port: 3301, lazyConnect: true, username: 'test', password: 'notStrongPass :('}); }); - it('lazy connect', function(done){ - conn.connect() - .then(function(){ - done(); - }, function(e){ - done(e); - }); + test('lazy connect', async () => { + assert.strictEqual(conn._state[0], states.INITED); + await conn.connect(); + assert.strictEqual(conn._state[0], states.CONNECT); }); - it('should be authenticated', function(done){ - conn.connect().then(function(){ - return conn.eval('return box.session.user()'); - }) - .then(function(res){ - assert.equal(res[0], 'test'); - done(); - }) - .catch(function(e){done(e);}); - }); - it('should disconnect when inited', function(done){ + test('should be authenticated', async () => { + const res = await conn.eval('return box.session.user()'); + assert.strictEqual(res[0], 'test'); + }); + test('should disconnect when inited', () => { + conn.disconnect(); + assert.strictEqual(conn._state[0], states.END); + }); + test('should connect after ".disconnect()" call', async () => { + await promisedSetTimeout(100) // wait for the previous '.disconnect()' call to fullfill + await conn.connect(); conn.disconnect(); - expect(conn.state).to.eql(32); // states.END == 32 - done(); - }); - it('should disconnect', function(done){ - conn.connect() - .then(function(res){ - conn.disconnect(); - assert.equal(conn.socket.writable, false); - done(); - }) - .catch(function(e){done(e);}); + assert.strictEqual(conn._state[0], states.END); + const isWritable = conn.connector.isWritable(); + assert.strictEqual(isWritable, false); }); }); -describe('instant connection', function(){ - beforeEach(function(){ - conn = new TarantoolConnection({port: 33013, username: 'test', password: 'test'}); - }); - it('connect', function(done){ - conn.eval('return func_arg(...)', 'connected!') - .then(function(res){ - try{ - assert.equal(res, 'connected!'); - } catch(e){console.error(e);} - done(); - }, function(e){ - done(e); - }); + +describe('instant connection', () => { + before(() => { + conn = new TarantoolConnection({port: 3301, username: 'test', password: 'notStrongPass :('}); + }); + test('connect', async () => { + const res = await conn.eval('return func_arg(...)', ['connected!']); + assert.strictEqual(res[0], 'connected!'); + }); + test('should reject when connected', async () => { + await assert.rejects( + () => conn.connect(), + { message: /Tarantool is already connecting\/connected/ } + ); }); - it('should reject when connected', function (done) { - conn.connect().catch(function (err) { - expect(err.message).to.match(/Tarantool is already connecting\/connected/); - done(); - }); - }); - it('should be authenticated', function(done){ - conn.eval('return box.session.user()') - .then(function(res){ - assert.equal(res[0], 'test'); - done(); - }) - .catch(function(e){done(e);}); - }); - it('should reject when auth failed', function (done) { - conn = new TarantoolConnection({port: 33013, username: 'userloser', password: 'test'}); - conn.eval('return func_foo()') - .catch(function (err) { - expect(err.message).to.include("not found"); - conn.disconnect(); - done(); - }); + test('should be authenticated', async () => { + const res = await conn.eval('return box.session.user()'); + assert.strictEqual(res[0], 'test'); + conn.disconnect() }); - it('should reject command when connection is closed', function (done) { - conn = new TarantoolConnection(); + test('should reject when auth failed', async () => { + conn = new TarantoolConnection({port: 3301, username: 'userloser', password: 'test'}); + conn.on('error', noop) + + await conn.pendingPromises.connect.catch(noop); + + await assert.rejects( + () => conn.eval('return func_arg()'), + { message: /Connection is closed/ } + ); conn.disconnect(); - conn.eval('return func_foo()') - .catch(function (err) { - expect(err.message).to.match(/Connection is closed/); - done(); - }); + }); + test('should reject command when connection is closed', async () => { + conn = new TarantoolConnection(3301); + + await conn.pendingPromises.connect.catch(noop); + + conn.disconnect(); + + await assert.rejects( + () => conn.eval('return func_arg()'), + { message: /Connection is finished/ } + ); }); }); -describe('timeout', function(){ - it('should close the connection when timeout', function (done) { - conn = new TarantoolConnection(33013, '192.0.0.0', { +describe('timeout', { timeout: 10000 }, () => { + test('should close the connection when timeout', async () => { + conn = new TarantoolConnection(3301, '192.0.0.0', { timeout: 1, retryStrategy: null }); - var pending = 2; - conn.on('error', function (err) { - expect(err.message).to.eql('connect ETIMEDOUT'); - if (!--pending) { - done(); - } - }); - conn.ping() - .catch(function (err) { - expect(err.message).to.match(/Connection is closed/); - if (!--pending) { - done(); - } + + const errorPromise = new Promise((resolve) => { + conn.on('error', (err) => { + resolve( + assert.strictEqual(err.message, 'connect ETIMEDOUT') + ) }); - }); - it('should clear the timeout when connected', function (done) { - conn = new TarantoolConnection(33013, { timeout: 10000 }); - setImmediate(function () { - stub(conn.socket, 'setTimeout') - .callsFake(function (timeout) { - expect(timeout).to.eql(0); - conn.socket.setTimeout.restore(); - done(); - }); }); + + try { + await conn.ping(); + } catch (err) { + assert.match(err.message, /Connection is closed/); + } + + await errorPromise; }); }); +describe('requests', () => { + let insertTuple = [1, [1, 2]]; + before(async () => { + try { + conn = new TarantoolConnection({port: 3301, username: 'test', password: 'notStrongPass :(', ...optLazyConnect}); -describe('requests', function(){ - var insertTuple = [50, 10, 'my key', 30]; - before(function(done){ - try{ - conn = new TarantoolConnection({port: 33013, username: 'test', password: 'test'}); - - Promise.all([conn.delete(514, 0, [1]),conn.delete(514, 0, [2]), - conn.delete(514, 0, [3]),conn.delete(514, 0, [4]), - conn.delete(512, 0, [999])]) - .then(function(){ - return conn.call('clearaddmore'); - }) - .then(function(){ - done(); - }) - .catch(function(e){ - done(e); - }); - } - catch(e){ - console.log(e); + await truncateSpace(conn, 'bench_memtx'); + console.info('✓ Deleted test data'); + } catch(e) { + console.error('❌ Setup failed in requests.before hook:', e); + throw e; } }); - it('replace', function(done){ - conn.replace(512, insertTuple) - .then(function(a){ - assert.equal(a.length, 1); - for (var i = 0; i { + const a = await conn.insert('bench_memtx', insertTuple); + assert.deepStrictEqual(a[0], insertTuple); + }); + test('replace', async () => { + const a = await conn.replace('bench_memtx', insertTuple); + assert.deepStrictEqual(a[0], insertTuple); + }); + test('simple select', async () => { + const a = await conn.select(512, 0, 1, 0, 'eq', [1]); + assert.deepStrictEqual(a[0], insertTuple); + }); + test('simple select with callback', async () => { + return new Promise((resolve, reject) => { + conn.select(512, 0, 1, 0, 'eq', [1], null, (error, result) => { + if (error) return reject(error); + + try { + assert.deepStrictEqual(result[0], insertTuple); + resolve(); + } catch (e) { + reject(e) + } + }) + }) + }); + test('composite select', async () => { + const a = await conn.select(512, 2 /* rtree idx */, null /* connector should use the default offset and limit values if omitted */, undefined, 'eq', [[1, 2]]) + assert.deepStrictEqual(a[0], insertTuple); + }); + test('dup error', async () => { + await assert.rejects( + () => conn.insert(512, insertTuple), + Error + ); }); - it('update', function(done){ - conn.update(512, 0, [50], [['+',3,10]]) - .then(function(a){ - assert.equal(a.length, 1); - assert.equal(a[0][3], insertTuple[3]+10); - done(); - }).catch(function(e){ done(e); }); - }); - it('a lot of insert', function(done){ - var promises = []; - for (var i = 0; i <= 5000; i++) { - promises.push(conn.insert(515, ['key' + i, i])); + test('update', async () => { + insertTuple[1] = [2, 3]; + const a = await conn.update(512, 0, [1], [['=', 1, insertTuple[1]]]); + assert.deepStrictEqual(a[0], insertTuple); + }); + test('update with field as string', async () => { + insertTuple[1] = [3, 4]; + const a = await conn.update(512, 0, [1], [['=', 'line', insertTuple[1]]]); + assert.deepStrictEqual(a[0], insertTuple); + }); + // https://www.tarantool.io/en/doc/latest/reference/reference_lua/json_paths/ + test('update inside of an array', async () => { + insertTuple[1][0]++; + const a = await conn.update(512, 0, [1], [['+', 'line[1]', 1]]); + assert.deepStrictEqual(a[0], insertTuple); + }); + test('delete', async () => { + const a = await conn.delete(512, 0, [1]); + assert.deepStrictEqual(a[0], insertTuple); + }); + test('a lot of insert', async () => { + const promises = []; + for (let i = 1; i <= 5000; i++) { + promises.push(conn.insert(512, [i, [i, i+1]])); } - Promise.all(promises) - .then(function(pr){ - done(); - }) - .catch(function(e){ - done(e); - }); + return Promise.all(promises); }); - it('check errors', function(done){ - conn.insert(512, ['key', 'key', 'key']) - .then(function(){ - done(new Error('Right when need error')); - }) - .catch(function(e){ - done(); - }); + test('check errors', async () => { + await assert.rejects( + () => conn.insert(512, ['key', 'key', 'key']) + ); }); - it('call print', function(done){ - conn.call('myprint', ['test']) - .then(function(){ - done(); - }) - .catch(function(e){ - console.log(e); - done(e); - }); + test('call print', async () => { + const a = await conn.call('func_arg', ['test']); + assert.strictEqual(a[0][0], 'test'); }); - it('call batch', function(done){ - conn.call('batch', [[1], [2], [3]]) - .then(function(){ - done(); - }) - .catch(function(e){ - console.log(e); - done(e); - }); + test('call sum', async () => { + const a = await conn.call('sum', [1, 2]).catch(console.error); + assert.strictEqual(a[0][0], 3); }); - it('call get', function(done){ - conn.insert(514, [4]) - .then(function() { - return conn.call('myget', 4); - }) - .then(function(value){ - done(); - }) - .catch(function(e){ - console.log(e); - done(e); - }); + test('get metadata space by name', async () => { + const v = await conn._getSpaceId('bench_memtx'); + assert.strictEqual(v, 512); }); - it('get metadata space by name', function(done){ - conn._getSpaceId('batched') - .then(function(v){ - assert.equal(v, 514); - done(); - }) - .catch(function(e){ - done(e); - }); + test('get metadata index by name', async () => { + const v = await conn._getIndexId(512, 'tree_idx'); + assert.strictEqual(v, 1); }); - it('get metadata index by name', function(done){ - conn._getIndexId(514, 'primary') - .then(function(v){ - assert.equal(v, 0); - done(); - }) - .catch(function(e){ - done(e); - }); + test('insert with space name', async () => { + insertTuple = [0, [0, 1]]; + await conn.insert('bench_memtx', insertTuple); }); - it('insert with space name', function(done){ - conn.insert('test', [999, 999, 'fear']) - .then(function(v){ - done(); - }) - .catch(done); - }); - it('select with space name and index name', function(done){ - conn.select('test', 'primary', 0, 0, 'all', [999]) - .then(function(){ - done(); - }) - .catch(done); - }); - it('select with space name and index number', function(done){ - conn.select('test', 0, 0, 0, 'eq', [999]) - .then(function(){ - done(); - }) - .catch(done); - }); - it('select with space number and index name', function(done){ - conn.select(512, 'primary', 0, 0, 'eq', [999]) - .then(function(){ - done(); - }) - .catch(done); - }); - it('delete with name', function(done){ - conn.delete('test', 'primary', [999]) - .then(function(){ - done(); - }) - .catch(done); - }); - it('update with name', function(done){ - conn.update('test', 'primary', [999], ['+', 1, 10]) - .then(function(){ - done(); - }) - .catch(done); - }); - it('evaluate expression', function(done){ - conn.eval('return 2+2') - .then(function(res){ - assert.equal(res, 4); - done(); - }) - .catch(function(e){ - done(e); - }); + test('select with space name and index name', async () => { + const a = await conn.select('bench_memtx', 'hash_idx', 0, 0, 'eq', [0]); + assert.deepStrictEqual(a[0], insertTuple) }); - it('evaluate expression with args', function(done){ - conn.eval('return func_sum(...)', 11, 22) - .then(function(res){ - assert.equal(res, 33); - done(); - }) - .catch(function(e){ - done(e); - }); + test('select with space name and index number', async () => { + const a = await conn.select('bench_memtx', 1, 0, 0, 'eq', [0]); + assert.deepStrictEqual(a[0], insertTuple) }); - it('ping', function(done){ - conn.ping() - .then(function(res){ - assert.equal(res, true); - done(); - }) - .catch(function(e){ - done(e); - }); + test('select with space number and index name', async () => { + const a = await conn.select(512, 'hash_idx', 0, 0, 'eq', [0]); + assert.deepStrictEqual(a[0], insertTuple) }); -}); + test('upsert', async () => { + // should update the tuple because it exists + insertTuple[1] = [1, 1] + await conn.upsert('bench_memtx', [['=', 1, insertTuple[1]]], insertTuple); + const a = await conn.select('bench_memtx', 1, 0, 0, 'eq', [0]); + assert.deepStrictEqual(a[0], insertTuple); - -describe('upsert', function(){ - before(function(done){ - try{ - conn = new TarantoolConnection({port: 33013,lazyConnect: true}); - conn.connect().then(function(){ - return conn._auth('test', 'test'); - }, function(e){ done(e); }) - .then(function(){ - return Promise.all([ - conn.delete('upstest', 'primary', 1), - conn.delete('upstest', 'primary', 2) - ]); - }) - .then(function(){ - done(); - }) - .catch(function(e){ - done(e); - }); - } - catch(e){ - console.log(e); - } - }); - it('insert', function(done){ - conn.upsert('upstest', [['+', 3, 3]], [1, 2, 3]) - .then(function() { - return conn.select('upstest', 'primary', 1, 0, 'eq', 1); - }) - .then(function(tuples){ - assert.equal(tuples.length, 1); - assert.deepEqual(tuples[0], [1, 2, 3]); - done(); - }) - - .catch(function(e){ - done(e); - }); + // should insert a new tuple because it doesn't exist + const newArr = [5001, [1, 1]]; + await conn.upsert('bench_memtx', [['=', 1, [0, 0]]], newArr); + const b = await conn.select('bench_memtx', 1, 0, 0, 'eq', [5001]); + assert.deepStrictEqual(b[0], newArr); }); - it('update', function(done){ - conn.upsert('upstest', [['+', 2, 2]], [2, 4, 3]) - .then(function(){ - return conn.upsert('upstest', [['+', 2, 2]], [2, 4, 3]); - }) - .then(function() { - return conn.select('upstest', 'primary', 1, 0, 'eq', 2) ; - }) - .then(function(tuples){ - assert.equal(tuples.length, 1); - assert.deepEqual(tuples[0], [2, 4, 5]); - done(); - }) - - .catch(function(e){ - done(e); - }); + test('delete with name', async () => { + const a = await conn.delete('bench_memtx', 'hash_idx', [0]); + assert.deepStrictEqual(a[0], insertTuple); }); -}); -describe('connection test with custom msgpack implementation', function(){ - var customConn; - beforeEach(function(){ - customConn = TarantoolConnection( - { - port: 33013, - msgpack: { - encode: function(obj){ - return mlite.encode(obj); - }, - decode: function(buf){ - return mlite.decode(buf); - } - }, - lazyConnect: true, - username: 'test', - password: 'test' - } - ); + test('evaluate expression', async () => { + const res = await conn.eval('return 2+2'); + assert.strictEqual(res[0], 4); }); - it('connect', function(done){ - customConn.connect().then(function(){ - done(); - }, function(e){ throw 'not connected'; }); + test('evaluate expression with args', async () => { + const res = await conn.eval('return sum(...)', [11, 22]); + assert.strictEqual(res[0], 33); }); - it('should be authenticated', function(done){ - conn.eval('return box.session.user()') - .then(function(res){ - assert.equal(res[0], 'test'); - done(); - }) - .catch(function(e){done(e);}); + test('ping', async () => { + const res = await conn.ping(); + assert.strictEqual(res, true); }); -}); -describe('slider buffer', function(){ -}) \ No newline at end of file + + after(async () => { + return conn.quit() + }) +}); \ No newline at end of file diff --git a/test/autopipeline.js b/test/autopipeline.js new file mode 100644 index 0000000..7e81f0b --- /dev/null +++ b/test/autopipeline.js @@ -0,0 +1,127 @@ +/** + * Test suite for Auto-Pipelining feature (enableAutoPipelining option) + */ + +const { test, describe, before, after, afterEach } = require('node:test'); +const assert = require('node:assert'); +const TarantoolConnection = require('../lib/connection'); + +let conn; + +const truncateSpace = async (conn, spaceName) => { + return conn.sql(`DELETE FROM "${spaceName}" INDEXED BY "tree_idx" WHERE true`); +}; + +describe('Auto-Pipelining (enableAutoPipelining option)', { timeout: 10000 }, () => { + before(async () => { + conn = new TarantoolConnection(3301, { + enableAutoPipelining: true, + lazyConnect: true + }); + await conn.connect(); + }); + + after(async () => { + if (conn) { + await conn.quit(); + } + }); + + afterEach(() => { + return truncateSpace(conn, 'bench_memtx'); + }) + + test('should have autoPipeliner available on connection', () => { + assert.ok(conn.autoPipeliner, 'Connection should have autoPipeliner'); + assert.ok(typeof conn.autoPipeliner.add === 'function', 'autoPipeliner should have add method'); + assert.ok(Array.isArray(conn.autoPipeliner.queue) || + conn.autoPipeliner.queue instanceof Array, + 'autoPipeliner should have queue'); + }); + + test('should accumulate commands during same event loop tick', async () => { + try { + assert.equal(conn.autoPipeliner.queue.length, 0); + + const firstInsert = [771, [771, 772]]; + const secondInsert = [772, [772, 773]]; + + // Issue multiple commands without await (in same event loop) + const promise1 = conn.insert('bench_memtx', firstInsert); + const promise2 = conn.insert('bench_memtx', secondInsert); + + // At this point, commands should be queued + assert.equal(conn.autoPipeliner.queue.length, 2); + + // autopipeline mode flushes queue on next tick + await new Promise((resolve) => { + process.nextTick(() => { + resolve(assert.equal(conn.autoPipeliner.queue.length, 0)); + }); + }); + + // Wait for all commands to complete + const [res1, res2] = await Promise.all([promise1, promise2]); + + assert.deepStrictEqual(res1[0], firstInsert); + assert.deepStrictEqual(res2[0], secondInsert); + } catch (err) { + console.error('❌ Auto-pipelining accumulation test failed:', err); + throw err; + } + }); + + test('should properly resolve all commands after batch is sent', async () => { + try { + const results = []; + const expectedCount = 15; + + // Issue multiple commands + const promises = []; + for (let i = 0; i < expectedCount; i++) { + promises.push( + conn.insert('bench_memtx', [i, [i, i+1]]) + .then(res => { + results.push(res); + return res; + }) + ); + } + + // Wait for all + await Promise.all(promises); + + // Each should have unique values + for (let i = 0; i < expectedCount; i++) { + assert.deepStrictEqual(results[i][0], [i, [i, i+1]]); + } + } catch (err) { + console.error('❌ Auto-pipelining resolution test failed:', err); + throw err; + } + }); + + test('should disable auto-pipelining with autoPipeline: false in options', async () => { + try { + assert.equal(conn.autoPipeliner.queue.length, 0); + const tuple = [740, [140, 141]]; + // This command should be sent immediately, not batched + const result = conn.insert( + 'bench_memtx', + tuple, + { autoPipeline: false } // Disable for this particular command + ); + + // still should be empty + assert.equal(conn.autoPipeliner.queue.length, 0); + + const res = await result; + + assert.ok(res, 'Insert with autoPipeline: false should succeed'); + assert.deepStrictEqual(res[0], tuple); + } catch (err) { + console.error('❌ Auto-pipelining disable test failed:', err); + throw err; + } + }); +}); diff --git a/test/commander.js b/test/commander.js new file mode 100644 index 0000000..a6f0182 --- /dev/null +++ b/test/commander.js @@ -0,0 +1,48 @@ +const test = require('node:test'); +const assert = require('assert'); +const Commander = require('../lib/Commander.js'); +const { TarantoolError } = require('../lib/errors.js'); + +function createCommanderMock() { + const msgpacker = { + encode: (v) => Buffer.from(JSON.stringify(v)), + }; + return Object.assign(new Commander(), { + options: {}, + isConnectedState: () => true, + msgpacker, + _id: [0], + streamId: 1, + namespace: {}, + offlineQueue: { enabled: false }, + pendingPromises: {}, + sendCommand: (...args) => args, + salt: Buffer.from('12345678901234567890').toString('base64'), + }); +} + +test('Commander: _createBuffer allocates buffer', () => { + const c = createCommanderMock(); + const buf = c._createBuffer(10); + assert(buf instanceof Buffer); + assert.strictEqual(buf.length, 10); +}); + +test('Commander: _getRequestId increments', () => { + const c = createCommanderMock(); + const id1 = c._getRequestId(); + const id2 = c._getRequestId(); + assert.strictEqual(id2, id1 + 1); +}); + +test('Commander: returns error on _getSpaceId if schema not fetched', () => { + const c = createCommanderMock(); + c.schemaFetched = false; + assert.ok(c._getSpaceId('test') instanceof TarantoolError) +}); + +test('Commander: returns error on _getIndexId if schema not fetched', () => { + const c = createCommanderMock(); + c.schemaFetched = false; + assert.ok(c._getIndexId('space', 'idx') instanceof TarantoolError) +}); \ No newline at end of file diff --git a/test/msgpack_extensions.js b/test/msgpack_extensions.js new file mode 100644 index 0000000..7d74305 --- /dev/null +++ b/test/msgpack_extensions.js @@ -0,0 +1,173 @@ +const { test, before, after } = require('node:test'); +const assert = require('node:assert'); +const Driver = require('../lib/connection.js'); + +// Connection to Tarantool instance +const tarantool = new Driver({port: 3301, host: 'localhost'}, { + lazyConnect: true +}); + +// Connect to Tarantool before running all tests +before(async () => { + try { + await tarantool.connect(); + console.info('Connected to Tarantool'); + } catch (error) { + console.error('Failed to connect to Tarantool:', error.message); + throw error; + } +}); + +// Disconnect after all tests are completed +after(async () => { + try { + await tarantool.disconnect(); + console.info('Disconnected from Tarantool'); + } catch (error) { + console.error('Failed to disconnect from Tarantool:', error.message); + } +}); + +// Test pack methods via SQL queries +test('MessagePack extensions - Testing packUuid()', async () => { + // Valid UUID format + const validUuid = '123e4567-e89b-12d3-a456-426614174000'; + + try { + const result = await tarantool.sql( + `SELECT TYPEOF(:value) as "type", :value as "value"`, + [{ ':value': tarantool.packUuid(validUuid) }] + ); + assert.strictEqual(result[0].type, 'uuid', 'Should return uuid type'); + // UUID is returned as formatted string + assert.strictEqual(result[0].value, validUuid, 'Should return same UUID value'); + } catch (error) { + console.error('packUuid test error:', error); + assert.fail(`packUuid test failed: ${error.message}`); + } +}); + +test('MessagePack extensions - Testing packDecimal()', async () => { + const testValues = [123, 123.45, -99.99, 0.001, BigInt(100)]; + + for (const value of testValues) { + try { + const result = await tarantool.sql( + `SELECT TYPEOF(:value) as "type", :value as "value"`, + [{ ':value': tarantool.packDecimal(value) }] + ); + assert.strictEqual( + result[0].type, + 'decimal', + `Should return decimal type for value ${value}` + ); + // packDecimal(BigInt(100)) returns 100 as a number - this is acceptable + const returnedValue = result[0].value; + const expectedValue = typeof value === 'bigint' ? Number(value) : value; + // For floating point comparisons, allow small precision errors + const tolerance = Math.abs(expectedValue) * 0.0001; + assert( + Math.abs(returnedValue - expectedValue) <= tolerance, + `Value mismatch for ${value}: expected ${expectedValue}, got ${returnedValue}` + ); + } catch (error) { + console.error(`packDecimal test failed for value ${value}:`, error); + assert.fail(`packDecimal test failed for value ${value}: ${error.message}`); + } + } +}); + +test('MessagePack extensions - Testing packInterval()', async () => { + const intervalObject = { + day: 5, + hour: 12, + minute: 30 + }; + + try { + const result = await tarantool.sql( + `SELECT TYPEOF(:value) as "type", :value as "value"`, + [{ ':value': tarantool.packInterval(intervalObject) }] + ); + assert.strictEqual(result[0].type, 'interval', 'Should return interval type'); + + // Returned interval object may contain undefined/null for unspecified fields + const returnedInterval = result[0].value; + assert.strictEqual(returnedInterval.day, intervalObject.day, 'Day value should match'); + assert.strictEqual(returnedInterval.hour, intervalObject.hour, 'Hour value should match'); + assert.strictEqual(returnedInterval.minute, intervalObject.minute, 'Minute value should match'); + // Other fields might be undefined or null - that's acceptable + } catch (error) { + console.error('packInterval test error:', error); + assert.fail(`packInterval test failed: ${error.message}`); + } +}); + +test('MessagePack extensions - Testing packInteger()', async () => { + const testValues = [0, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; + + for (const value of testValues) { + try { + const result = await tarantool.sql( + `SELECT TYPEOF(:value) as "type", :value as "value"`, + [{ ':value': tarantool.packInteger(value) }] + ); + // packInteger should preserve integer type + const typeResult = result[0].type; + assert( + typeResult === 'integer' || typeResult === 'unsigned', + `Should return integer type for value ${value}, got ${typeResult}` + ); + // Verify the value is returned correctly + assert.strictEqual(result[0].value, value, `Value should match for ${value}`); + } catch (error) { + console.error(`packInteger test failed for value ${value}:`, error); + assert.fail(`packInteger test failed for value ${value}: ${error.message}`); + } + } +}); + +test('MessagePack extensions - Testing packDecimal() type casting', async () => { + // Test that packDecimal correctly preserves decimal representation + const testValue = 123.456; + const decimalValue = tarantool.packDecimal(testValue); + + try { + const result = await tarantool.sql( + `SELECT TYPEOF(:value) as "type", :value as "value"`, + [{ ':value': decimalValue }] + ); + assert.strictEqual(result[0].type, 'decimal', 'Should be decimal type'); + // Compare with tolerance for floating point precision + const tolerance = Math.abs(testValue) * 0.0001; + assert( + Math.abs(result[0].value - testValue) <= tolerance, + `Decimal value should match original: expected ${testValue}, got ${result[0].value}` + ); + } catch (error) { + console.error('packDecimal casting test error:', error); + assert.fail(`packDecimal casting test failed: ${error.message}`); + } +}); + +test('MessagePack extensions - Testing Date (datetime) type', async () => { + // Validate that Date objects are correctly packed as datetime + const testDate = new Date(); + + try { + const result = await tarantool.sql( + `SELECT TYPEOF(:value) as "type", :value as "value"`, + [{ ':value': testDate }] + ); + assert.strictEqual(result[0].type, 'datetime', 'Should return datetime type'); + // Verify returned value is a Date object and matches the original timestamp + const returnedDate = result[0].value; + assert.ok(returnedDate instanceof Date, 'Should return Date object'); + // Compare timestamps with a small tolerance for nanosecond precision loss + const timeDiff = Math.abs(returnedDate.getTime() - testDate.getTime()); + assert(timeDiff <= 1, `Date timestamp should match: expected ${testDate.getTime()}, got ${returnedDate.getTime()}`); + } catch (error) { + console.error('Date/datetime test error:', error); + assert.fail(`Date/datetime test failed: ${error.message}`); + } +}); \ No newline at end of file diff --git a/test/pipeline.js b/test/pipeline.js new file mode 100644 index 0000000..3d55c78 --- /dev/null +++ b/test/pipeline.js @@ -0,0 +1,265 @@ +/** + * Test suite for Pipelining Methods and PipelineResponse + */ + +const { test, describe, before, after, afterEach } = require('node:test'); +const assert = require('node:assert'); +const TarantoolConnection = require('../lib/connection'); + +let conn; + +const truncateSpace = async (conn, spaceName) => { + return conn.sql(`DELETE FROM "${spaceName}" INDEXED BY "tree_idx" WHERE true`); +}; + +describe('Pipelining Methods', { timeout: 5000 }, () => { + before(async () => { + conn = new TarantoolConnection(3301, { + lazyConnect: true + }); + await conn.connect(); + await truncateSpace(conn, 'bench_memtx'); + }); + + after(async () => { + if (conn) { + await conn.quit(); + } + }); + + afterEach(() => { + return truncateSpace(conn, 'bench_memtx'); + }) + + test('should queue commands in pipeline', () => { + const pipeline = conn.pipeline(); + assert.ok(pipeline, 'Pipeline should be created'); + assert.ok(typeof pipeline.insert === 'function', 'Pipeline should have insert method'); + assert.ok(typeof pipeline.select === 'function', 'Pipeline should have select method'); + assert.ok(typeof pipeline.exec === 'function', 'Pipeline should have exec method'); + }); + + test('should execute pipelined commands and return PipelineResponse', async () => { + try { + const firstTuple = [881, [881, 882]]; + const secondTuple = [882, [882, 883]]; + const pipelineResponse = await conn.pipeline() + .insert('bench_memtx', firstTuple) + .insert('bench_memtx', secondTuple) + .select('bench_memtx', 'hash_idx', 1, 0, 'eq', [881]) + .exec(); + + assert.ok(pipelineResponse, 'Pipeline response should exist'); + assert.ok(pipelineResponse instanceof TarantoolConnection.PipelineResponse, 'Response should be an instance of PipelineResponse'); + assert.strictEqual(pipelineResponse.length, 3, 'Should have 3 results'); + + // Check first result (insert) + const [err1, res1] = pipelineResponse[0]; + assert.strictEqual(err1, null, 'First insert should not have error'); + assert.deepStrictEqual(res1[0], firstTuple); + + // Check second result (insert) + const [err2, res2] = pipelineResponse[1]; + assert.strictEqual(err2, null, 'Second insert should not have error'); + assert.deepStrictEqual(res2[0], secondTuple); + + // Check third result (select) + const [err3, res3] = pipelineResponse[2]; + assert.strictEqual(err3, null, 'Select should not have error'); + assert.ok(Array.isArray(res3), 'Select result should be array'); + } catch (err) { + console.error('❌ Pipeline execution test failed:', err); + throw err; + } + }); + + test('reuse pipeline instance', async () => { + try { + const pipelined = conn.pipeline(); + + let firstTuple = [881, [881, 882]]; + let secondTuple = [882, [882, 883]]; + let pipelineResponse = await pipelined + .insert('bench_memtx', firstTuple) + .insert('bench_memtx', secondTuple) + .select('bench_memtx', 'hash_idx', 1, 0, 'eq', [881]) + .exec(); + + // Check first result (insert) + const [err1, res1] = pipelineResponse[0]; + assert.strictEqual(err1, null, 'First insert should not have error'); + assert.deepStrictEqual(res1[0], firstTuple); + + // Check second result (insert) + const [err2, res2] = pipelineResponse[1]; + assert.strictEqual(err2, null, 'Second insert should not have error'); + assert.deepStrictEqual(res2[0], secondTuple); + + // Check third result (select) + const [err3, res3] = pipelineResponse[2]; + assert.strictEqual(err3, null, 'Select should not have error'); + assert.ok(Array.isArray(res3), 'Select result should be array'); + + // Reuse the "pipelined" instance + const thirdTuple = [883, [883, 884]]; + pipelineResponse = await pipelined + .insert('bench_memtx', thirdTuple) + .exec(); + + const [err4, res4] = pipelineResponse[0]; + assert.strictEqual(err4, null, 'Thrird insert should not have error'); + assert.deepStrictEqual(res4[0], thirdTuple); + assert.strictEqual(pipelineResponse.length, 1, 'Should have only 1 result'); + } catch (err) { + console.error('❌ Pipeline execution test failed:', err); + throw err; + } + }); + + test('should support callback style in pipeline', async () => { + try { + let callbackCalled = false; + let callbackError = null; + let callbackResult = null; + + const pipelineResponse = await conn.pipeline() + .insert('bench_memtx', [871, [871, 872]], null, (err, result) => { + callbackCalled = true; + callbackError = err; + callbackResult = result; + }) + .insert('bench_memtx', [872, [872, 873]]) + .exec(); + + assert.ok(pipelineResponse, 'Pipeline response should exist'); + assert.strictEqual(pipelineResponse.length, 2, 'Should have 2 results'); + + // Check individual callback was called + assert.ok(callbackCalled, 'Callback should be called'); + assert.strictEqual(callbackError, null, 'Callback error should be null'); + assert.ok(callbackResult, 'Callback result should exist'); + } catch (err) { + console.error('❌ Pipeline callback test failed:', err); + throw err; + } + }); + + test('should provide "findPipelineError" and "findPipelineErrors" methods', async () => { + const pipelineResponse = await conn.pipeline() + .insert('bench_memtx', [861, [861, 862]]) + .select('bench_memtx', 'hash_idx', 1, 0, 'eq', [861]) + .exec(); + + assert.ok(typeof pipelineResponse.findPipelineError === 'function', + 'PipelineResponse should have findPipelineError method'); + assert.ok(typeof pipelineResponse.findPipelineErrors === 'function', + 'PipelineResponse should have findPipelineErrors method'); + }); + + test('Testing "findPipelineError" and "findPipelineErrors" methods', async () => { + try { + // All ops should succeed below + let pipelineResponse = await conn.pipeline() + .insert('bench_memtx', [861, [861, 862]]) + .select('bench_memtx', 'hash_idx', 1, 0, 'eq', [861]) + .exec(); + + assert.ok(typeof pipelineResponse.findPipelineError === 'function', + 'PipelineResponse should have findPipelineError method'); + assert.ok(typeof pipelineResponse.findPipelineErrors === 'function', + 'PipelineResponse should have findPipelineErrors method'); + + // All operations succeeded, so should return null + let firstError = pipelineResponse.findPipelineError(); + let allErrors = pipelineResponse.findPipelineErrors(); + assert.ok(firstError === null, + 'findPipelineError should return null'); + assert.strictEqual(allErrors.length, 0) + + // Insert below should fail because of invalid space with id 12345 + pipelineResponse = await conn.pipeline() + .insert(12345, [861, [861, 862]]) + .exec(); + + firstError = pipelineResponse.findPipelineError(); + assert.strictEqual(firstError.errno, 36); + allErrors = pipelineResponse.findPipelineErrors(); + assert.strictEqual(allErrors.length, 1); + + } catch (err) { + console.error('❌ Pipeline "findPipelineError*" test failed:', err); + throw err; + } + }); + + test('should accumulate multiple buffers in large pipelines', async () => { + try { + const pipeline = conn.pipeline(); + assert.strictEqual(pipeline.pipelinedCommands.length, 0) + + // Add multiple operations to potentially create multiple buffers + for (let i = 0; i < 20; i++) { + pipeline.insert('bench_memtx', [i, [i, i + 1]]); + } + + assert.strictEqual(pipeline.pipelinedCommands.length, 20, 'Should have 20 queued commands') + + const pipelineResponse = await pipeline.exec(); + + assert.strictEqual(pipelineResponse.length, 20, 'Should have 20 results'); + + // Verify all succeeded + const errors = pipelineResponse.findPipelineErrors(); + assert.strictEqual(errors.length, 0, 'No errors should be in bulk pipeline'); + } catch (err) { + console.error('❌ Pipeline bulk operations test failed:', err); + throw err; + } + }); + + test('should be flushing queued commands', async () => { + try { + const pipeline = conn.pipeline(); + assert.strictEqual(pipeline.pipelinedCommands.length, 0, 'Should be empty after init'); + + pipeline.insert('bench_memtx', [861, [861, 862]]); + assert.strictEqual(pipeline.pipelinedCommands.length, 1, 'Should NOT be empty after "insert" command'); + + assert.ok(typeof pipeline.flushPipelined === 'function', + 'Should have flushPipelined on pipeline object'); + + pipeline.flushPipelined(); + assert.strictEqual(pipeline.pipelinedCommands.length, 0, 'Should be empty after flushing'); + } catch (err) { + console.error('❌ Empty pipeline test failed:', err); + throw err; + } + }); + + test('should support empty pipeline', async () => { + try { + const pipelineResponse = await conn.pipeline().exec(); + + assert.ok(pipelineResponse instanceof TarantoolConnection.PipelineResponse, 'Should return PipelineResponse instance'); + assert.strictEqual(pipelineResponse.length, 0, 'Empty pipeline should return empty result'); + } catch (err) { + console.error('❌ Empty pipeline test failed:', err); + throw err; + } + }); + + test('should access PipelineResponse via static methods', ({ skip }) => { + try { + const PipelineResponse = TarantoolConnection.PipelineResponse; + + // Static methods should be available on PipelineResponse class + assert.ok(typeof PipelineResponse.findPipelineError === 'function', + 'Should have static findPipelineError on Connection'); + assert.ok(typeof PipelineResponse.findPipelineErrors === 'function', + 'Should have static findPipelineErrors on Connection'); + } catch (err) { + console.error('❌ Static methods test failed:', err); + throw err; + } + }); +}); diff --git a/test/transaction.js b/test/transaction.js new file mode 100644 index 0000000..32a153d --- /dev/null +++ b/test/transaction.js @@ -0,0 +1,176 @@ +/** + * Test suite for Transaction Methods + */ + +const { test, describe, before, after, afterEach } = require('node:test'); +const assert = require('node:assert'); +const { + setTimeout: promisedSetTimeout, +} = require('node:timers/promises'); +const TarantoolConnection = require('../lib/connection'); + +let conn; + +const truncateSpace = async (conn, spaceName) => { + return conn.sql(`DELETE FROM "${spaceName}" INDEXED BY "tree_idx" WHERE true`); +}; + +describe('Transaction Methods', { timeout: 10000 }, () => { + before(async () => { + conn = new TarantoolConnection(3301, { + lazyConnect: true + }); + await conn.connect(); + }); + + after(async () => { + if (conn) { + await conn.quit(); + } + }); + + afterEach(() => { + return truncateSpace(conn, 'bench_memtx'); + }) + + test('should create transaction context', () => { + const txn = conn.transaction(); + assert.ok(txn, 'Transaction should be created'); + assert.ok(typeof txn.begin === 'function', 'Should have begin method'); + assert.ok(typeof txn.commit === 'function', 'Should have commit method'); + assert.ok(typeof txn.rollback === 'function', 'Should have rollback method'); + }); + + test('should execute commands within transaction and commit', async () => { + try { + const txn = conn.transaction(); + + // Begin transaction + await txn.begin(120, 0); + + // Insert data within transaction + const tuple = [1, [1, 2]]; + const inserted = await txn.insert('bench_memtx', tuple); + assert.ok(inserted, 'Insert within transaction should succeed'); + assert.deepStrictEqual(inserted[0], tuple); + + // Commit transaction + await txn.commit(); + + // Verify data was committed + const results = await conn.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [1]); + assert.strictEqual(results.length, 1, 'Data should be visible after commit'); + assert.deepStrictEqual(results[0], tuple); + } catch (err) { + console.error('❌ Transaction commit test failed:', err); + throw err; + } + }); + + test('should rollback transaction changes', async () => { + try { + const txn = conn.transaction(); + + // Begin transaction + await txn.begin(120, 0); + + // Insert data within transaction + const inserted = await txn.insert('bench_memtx', [1, [1, 2]]); + assert.ok(inserted, 'Insert should succeed'); + + // Rollback transaction + await txn.rollback(); + + // Verify data was NOT committed + const results = await conn.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [1]); + const found = results.some(r => r[0] === 1); + assert.ok(!found, 'Rolled back data should not be persisted'); + } catch (err) { + console.error('❌ Transaction rollback test failed:', err); + throw err; + } + }); + + test('check transaction params', async () => { + try { + const txn = conn.transaction(); + const txn_isolation = 2; + + // Begin transaction with a very low timeout + await txn.begin(1, txn_isolation); + + const currentIsolation = await txn.eval('return box.internal.txn_isolation()'); + assert.strictEqual(currentIsolation[0], txn_isolation); + + const is_in_txn = await txn.eval('return box.is_in_txn()'); + assert.ok(is_in_txn[0] === true); + + // Currently there is no internal function to get transaction timeout :( + await promisedSetTimeout(2000) + + let err; + await txn.commit().catch(e => err = e); + assert.strictEqual(err?.errno, 231, 'Transaction should timeout'); + } catch (err) { + console.error('❌ Transaction timeout test failed:', err); + throw err; + } + }); + + test('should support callback style for transaction methods', async () => { + try { + const txn = conn.transaction(); + + // Test begin with callback + await new Promise((resolve, reject) => { + txn.begin(120, 0, {}, (err, result) => { + if (err) reject(err); + else resolve(result); + }); + }); + + // Insert within transaction + const inserted = await txn.insert('bench_memtx', [1, [1, 2]]); + assert.ok(inserted, 'Insert should succeed'); + + // Test commit with callback + await new Promise((resolve, reject) => { + txn.commit({}, (err, result) => { + if (err) reject(err); + else resolve(result); + }); + }); + + // Verify + const results = await conn.select('bench_memtx', 'hash_idx', 1, 0, 'eq', [1]); + assert.ok(results.some(r => r[0] === 1), 'Data should be committed'); + } catch (err) { + console.error('❌ Transaction callback test failed:', err); + throw err; + } + }); + + test('should handle multiple commands in same transaction', async () => { + try { + const txn = conn.transaction(); + + await txn.begin(120, 0); + + // Multiple inserts + const res1 = await txn.insert('bench_memtx', [1, [1, 2]]); + const res2 = await txn.insert('bench_memtx', [2, [2, 3]]); + const res3 = await txn.insert('bench_memtx', [3, [3, 4]]); + + assert.ok(res1 && res2 && res3, 'All inserts should succeed'); + + await txn.commit(); + + // Verify all were committed + const results = await conn.select('bench_memtx', 'hash_idx', 3, 0, 'eq', [1]); + assert.ok(results.length > 0, 'First record should exist'); + } catch (err) { + console.error('❌ Transaction multi-command test failed:', err); + throw err; + } + }); +}); From 7eae3396b40655fd1e3aa19363558b015fd79521 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:17:46 +0100 Subject: [PATCH 09/12] Removed old files --- .eslintrc | 38 - .gitignore | 9 - .travis.yml | 23 - benchmark/box.lua | 58 - lib/autopipelining.js | 19 - lib/buffer-processor.js | 90 -- lib/commands.js | 871 -------------- lib/connector.js | 34 - lib/msgpack-extensions.js | 136 --- lib/pipeline-response.js | 34 - lib/pipeline.js | 101 -- lib/sliderBuffer.js | 78 -- lib/transaction.js | 17 - lib/utils.js | 109 -- package-lock.json | 2248 ------------------------------------- test.sh | 16 - test/box.lua | 124 -- test/box1.lua | 7 - test/box2.lua | 2 - 19 files changed, 4014 deletions(-) delete mode 100644 .eslintrc delete mode 100644 .gitignore delete mode 100644 .travis.yml delete mode 100755 benchmark/box.lua delete mode 100644 lib/autopipelining.js delete mode 100644 lib/buffer-processor.js delete mode 100755 lib/commands.js delete mode 100755 lib/connector.js delete mode 100644 lib/msgpack-extensions.js delete mode 100644 lib/pipeline-response.js delete mode 100644 lib/pipeline.js delete mode 100644 lib/sliderBuffer.js delete mode 100644 lib/transaction.js delete mode 100644 lib/utils.js delete mode 100755 package-lock.json delete mode 100755 test.sh delete mode 100755 test/box.lua delete mode 100644 test/box1.lua delete mode 100644 test/box2.lua diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 0e7ea4a..0000000 --- a/.eslintrc +++ /dev/null @@ -1,38 +0,0 @@ -{ - "env": { - "node": true - }, - "parserOptions": { - "ecmaVersion": 6, - "ecmaFeatures": {} - }, - "rules": { - "block-scoped-var": 2, - "no-cond-assign": 2, - "no-control-regex": 2, - "no-debugger": 2, - "no-dupe-args": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-ex-assign": 2, - "no-extra-semi": 2, - "no-func-assign": 2, - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-negated-in-lhs": 2, - "no-obj-calls": 2, - "no-redeclare": 2, - "no-regex-spaces": 2, - "no-sparse-arrays": 2, - "no-unexpected-multiline": 2, - "no-unreachable": 2, - "no-delete-var": 2, - "no-shadow": 2, - "no-undef": 2, - "radix": 2, - "semi": 2, - "use-isnan": 2, - "valid-jsdoc": 2, - "valid-typeof": 2 - } -} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 33c5728..0000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -coverage -node_modules/ -node_modules/* -.idea/ -.idea/* -*.snap -*.xlog - -npm-debug.log diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8d91f0d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -sudo: required -services: - - docker - -language: node_js -node_js: - - "5" - - "4" - - "6" - - "7" - - "node" - - "iojs" -cache: false - -before_script: - - ./test.sh - -script: - - sleep 4 - - npm test - -after_success: -- npm run coveralls \ No newline at end of file diff --git a/benchmark/box.lua b/benchmark/box.lua deleted file mode 100755 index 1fcead2..0000000 --- a/benchmark/box.lua +++ /dev/null @@ -1,58 +0,0 @@ -box.cfg{ - listen=3301, - memtx_use_mvcc_engine=true -} - -if not box.schema.user.exists('test') then - box.schema.user.create('test') -end - -user = box.user -if not user then - box.schema.user.grant('test', 'execute', 'universe') -end - -box.once('grant_user_right', function() - box.schema.user.grant('guest', 'read,write,execute', 'universe') -end) - -c = box.space.counter -if not c then - c = box.schema.space.create('counter', {engine = 'memtx'}) - c:format({ - {name = 'primary', type = 'string'}, - {name = 'num', type = 'unsigned'}, - {name = 'text', type = 'string'} - }) - pr = c:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'string'}}) - c:insert({'test', 1337, 'Some text.'}) -end - -c = box.space.counter_vinyl -if not c then - c = box.schema.space.create('counter_vinyl', {engine = 'vinyl'}) - c:format({ - {name = 'primary', type = 'string'}, - {name = 'num', type = 'unsigned'}, - {name = 'text', type = 'string'} - }) - pr = c:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'string'}}) - c:insert({'test', 1337, 'Some text.'}) -end - -s = box.space.bench -if not s then - s = box.schema.space.create('bench') - p = s:create_index('primary', {type = 'hash', parts = {1, 'unsigned'}}) -end - -s = box.space.bench_vinyl -if not s then - s = box.schema.space.create('bench_vinyl', {engine = 'vinyl'}) - p = s:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) -end - -function clear() - box.session.su('admin') - box.space.bench:truncate{} -end \ No newline at end of file diff --git a/lib/autopipelining.js b/lib/autopipelining.js deleted file mode 100644 index 4e410c4..0000000 --- a/lib/autopipelining.js +++ /dev/null @@ -1,19 +0,0 @@ -function AutoPipeline() {} - -AutoPipeline.prototype._processAutoPipeliningQueue = function () { - var concatenatedBuffer = Buffer.concat(this.autoPipelineQueue); - this.autoPipelineQueue = []; - this.socket.write(concatenatedBuffer); - this.autoPipeliningStarted = false; -}; - -AutoPipeline.prototype._addToAutoPipeliningQueue = function (buffer) { - this.autoPipelineQueue.push(buffer); - // check if auto pipelining is already started in order to don't execute 'setImmediate' each time - if (!this.autoPipeliningStarted) { - setImmediate(this._processAutoPipeliningQueue.bind(this)) - this.autoPipeliningStarted = true; - } -}; - -module.exports = AutoPipeline; \ No newline at end of file diff --git a/lib/buffer-processor.js b/lib/buffer-processor.js deleted file mode 100644 index 50847a1..0000000 --- a/lib/buffer-processor.js +++ /dev/null @@ -1,90 +0,0 @@ -var { - createBuffer, - TarantoolError, -} = require('./utils'); -var msgpackr = require('msgpackr'); -var packr = new msgpackr.Packr(); - -function _getScaleByNumber (num) { - if (num <= 255) { - return 8; - } else if (num <= 65535) { - return 16; - } else if (num <= 4294967295) { - return 32; - } else if (num <= Number.MAX_SAFE_INTEGER) { - return 64; - } else { - throw new TarantoolError('Exeeded max supported number for scale'); - } -}; - -function _getBufferLenghtByNumber (num) { - if (num <= 255) return 2; - if (num <= 65535) return 3; - if (num <= 4294967295) return 5; - if (num <= Number.MAX_SAFE_INTEGER) return 9; - - throw new TarantoolError('Exeeded max supported buffer length'); -}; - -function _getFbyteByNumber (num) { - if (num <= 255) return 0xcc; - if (num <= 65535) return 0xcd; - if (num <= 4294967295) return 0xce; - if (num <= Number.MAX_SAFE_INTEGER) return 0xcf; - - throw new TarantoolError('Exeeded max supported buffer length'); -}; - -function _getWriteMethodByNumber (num) { - if (num <= 255) return 'writeUInt8'; - if (num <= 65535) return 'writeUInt16BE'; - if (num <= 4294967295) return 'writeUInt32BE'; - if (num <= Number.MAX_SAFE_INTEGER) return 'writeBigUInt64BE'; - - throw new TarantoolError('Exeeded max supported buffer length'); -} - -// reuse buffers and pre-write the first byte -var buffer2bytes = createBuffer(2); -buffer2bytes[0] = 0xcc; -var buffer3bytes = createBuffer(3); -buffer3bytes[0] = 0xcd; -var buffer5bytes = createBuffer(5); -buffer5bytes[0] = 0xce; -var buffer9bytes = createBuffer(9); -buffer9bytes[0] = 0xcf; -function _encodeMsgpackNumber (num, scale = 0) { - if (scale === 0) scale = _getScaleByNumber(num); - - switch (scale) { - case 8: - buffer2bytes.writeUInt8(num, 1); - return buffer2bytes; - case 16: - buffer3bytes.writeUInt16BE(num, 1); - return buffer3bytes; - case 32: - buffer5bytes.writeUInt32BE(num, 1); - return buffer5bytes; - case 64: - buffer9bytes.writeBigUInt64BE(num, 1); - return buffer9bytes; - default: - throw new TarantoolError('Unsupported scale provided: ' + scale); - }; -}; - -var bufferCache = new Map(); - -module.exports.getCachedMsgpackBuffer = function (value) { - var search = bufferCache.get(value); - if (search) { - return search; - } else { - var encoded = packr.encode(value); - bufferCache.set(value, encoded); - return encoded; - }; -}; \ No newline at end of file diff --git a/lib/commands.js b/lib/commands.js deleted file mode 100755 index 4ffbba6..0000000 --- a/lib/commands.js +++ /dev/null @@ -1,871 +0,0 @@ -/* global Promise */ -var { createHash } = require('crypto'); -var tarantoolConstants = require('./const'); -var { - TarantoolError, - withResolvers -} = require('./utils'); -var { Packr } = require('msgpackr'); -var packr = new Packr({ - variableMapSize: true, - useRecords: false, - encodeUndefinedAsNil: true -}); - -var bufferCache = new Map(); -function getCachedMsgpackBuffer (value) { - var search = bufferCache.get(value); - if (search) { - return search; - } else { - var encoded = packr.encode(value); - bufferCache.set(value, encoded); - return encoded; - }; -}; - -var maxSmi = 1<<30 -exports._getRequestId = function _getRequestId (){ - var _id = this._id - if (_id[0] > maxSmi) _id[0] = 1; - return _id[0]++; -}; - -exports._getMetadata = function _getMetadata (spaceName, indexName){ - var _this = this; - var spName = this.namespace[spaceName] // reduce overhead of lookup - if (spName) - { - spaceName = spName.id; - } - if (typeof(spName) != 'undefined' && typeof(spName.indexes[indexName])!='undefined') - { - indexName = spName.indexes[indexName]; - } - if (typeof(spaceName)=='string' && typeof(indexName)=='string') - { - return this._getSpaceId(spaceName) - .then(function(spaceId){ - return Promise.all([spaceId, _this._getIndexId(spaceId, indexName)]); - }); - } - var promises = []; - if (typeof(spaceName) == 'string') - promises.push(this._getSpaceId(spaceName)); - else - promises.push(spaceName); - if (typeof(indexName) == 'string') - promises.push(this._getIndexId(spaceName, indexName)); - else - promises.push(indexName); - return Promise.all(promises); -}; - -exports._getIndexId = function _getIndexId (spaceId, indexName){ - var _this = this; - return this.select(tarantoolConstants.Space.index, tarantoolConstants.IndexSpace.indexName, 1, 0, - 'eq', [spaceId, indexName], { - tupleToObject: false, - autoPipeline: false - }) - .then(function(value) { - if (value && value[0] && value[0].length>1) { - var indexId = value[0][1]; - var space = _this.namespace[spaceId]; - if (space) { - _this.namespace[space.name].indexes[indexName] = indexId; - _this.namespace[space.id].indexes[indexName] = indexId; - } - return indexId; - } - else - throw new TarantoolError('Cannot read a space name indexes or index is not defined'); - }); -}; - -exports._getSpaceId = function _getSpaceId (name){ - var _this = this; - return this.select(tarantoolConstants.Space.space, tarantoolConstants.IndexSpace.name, 1, 0, - 'eq', [name], { - tupleToObject: false, - autoPipeline: false - }) - .then(function(value){ - if (value && value.length && value[0]) - { - var spaceId = value[0][0]; - var tupleKeys = value[0][6]; - tupleKeys.map(function (value, index) { - tupleKeys[index] = value.name - }) - - _this.namespace[name] = _this.namespace[spaceId] = { - id: spaceId, - name: name, - indexes: {}, - tupleKeys - }; - return spaceId; - } - else - { - throw new TarantoolError('Cannot read a space name or space is not defined'); - } - }); -}; - -exports.writePacketHeaders = function writePacketHeaders (buffer, length) { - const reqId = this._getRequestId(); - - buffer[0] = 0xce; - buffer.writeUInt32BE(length, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - - return reqId; -} - -exports.select = function select (spaceId, indexId, limit, offset = 0, iterator = 'eq', key, opts = {}) { - if (!Array.isArray(key)) key = [key]; - - var _this = this; - - if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) - spaceId = _this.namespace[spaceId].id; - if (typeof(indexId)=='string' && _this.namespace[spaceId] && _this.namespace[spaceId].indexes[indexId]) - indexId = _this.namespace[spaceId].indexes[indexId]; - if (typeof(spaceId)=='string' || typeof(indexId)=='string') - { - return _this._getMetadata(spaceId, indexId) - .then(function(info){ - return _this.select(info[0], info[1], limit, offset, iterator, key, opts); - }) - } - - if (iterator == 'all') key = []; - - var bufKey = packr.encode(key); - const len = 37+bufKey.length; - var buffer = this.createBuffer(len+5); - - const reqId = this.writePacketHeaders(buffer, len); - buffer[7] = tarantoolConstants.RequestCode.rqSelect; - buffer[20] = 0x86; - buffer[21] = tarantoolConstants.KeysCode.space_id; - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 26); - buffer[27] = tarantoolConstants.KeysCode.limit; - buffer[28] = 0xce; - buffer.writeUInt32BE(limit, 29); - buffer[33] = tarantoolConstants.KeysCode.offset; - buffer[34] = 0xce; - buffer.writeUInt32BE(offset, 35); - buffer[39] = tarantoolConstants.KeysCode.iterator; - buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 40); - buffer[41] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 42); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqSelect, - reqId, - buffer, - null, - arguments, - opts - ); -} - -exports.ping = function ping (opts = {}){ - var reqId = this._getRequestId(); - var len = 9; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqPing; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqPing, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.begin = function begin (transTimeoutSec = 60.01 /* prevent JS from converting 60.0 to 60 */, isolationLevel = 0, opts = {}) { - if (!this.streamId) { - return Promise.reject( - new TarantoolError('Cannot get streamId, maybe called method outside of transaction?') - ); - } - - var reqId = this._getRequestId(); - // check if not decimal - if (Number.isInteger(transTimeoutSec)) transTimeoutSec += 0.001; - var transTimeoutBuf = packr.encode(transTimeoutSec) - - var len = 19+transTimeoutBuf.length; - var buffer = this.createBuffer(5+len); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqBegin; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id; - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x82; - buffer[21] = tarantoolConstants.KeysCode.iproto_txn_isolation; - buffer.writeUInt8(isolationLevel, 22); - buffer[23] = tarantoolConstants.KeysCode.iproto_timeout; - transTimeoutBuf.copy(buffer, 24) - - return this.sendCommand( - tarantoolConstants.RequestCode.rqBegin, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.commit = function commit (opts = {}) { - if (!this.streamId) { - return Promise.reject( - new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') - ); - } - var reqId = this._getRequestId(); - - var len = 14; - var buffer = this.createBuffer(5+len); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqCommit; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id; - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqCommit, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.rollback = function rollback (opts = {}) { - if (!this.streamId) { - return Promise.reject( - new TarantoolError('Cannot find streamId, maybe called method outside of transaction?') - ); - } - var reqId = this._getRequestId(); - - var len = 14; - var buffer = this.createBuffer(5+len); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqRollback; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id; - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId, 16); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqRollback, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.selectCb = function selectCb (spaceId, indexId, limit, offset, iterator, key, success, error, opts = {}){ - if (!Array.isArray(key)) key = [key]; - - var _this = this; - - if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) - spaceId = _this.namespace[spaceId].id; - if (typeof(indexId)=='string' && _this.namespace[spaceId] && _this.namespace[spaceId].indexes[indexId]) - indexId = _this.namespace[spaceId].indexes[indexId]; - if (typeof(spaceId)=='string' || typeof(indexId)=='string') - { - return _this._getMetadata(spaceId, indexId) - .then(function(info){ - return _this.selectCb(info[0], info[1], limit, offset, iterator, key, success, error, opts); - }) - .catch(error); - } - - var reqId = this._getRequestId(); - if (iterator == 'all') - key = []; - var bufKey = packr.encode(key); - - var bufferLength = 42+bufKey.length; - var buffer = this.createBuffer(bufferLength); - - buffer[0] = 0xce; - buffer.writeUInt32BE(37+bufKey.length, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqSelect; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x86; - buffer[21] = tarantoolConstants.KeysCode.space_id; - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 26); - buffer[27] = tarantoolConstants.KeysCode.limit; - buffer[28] = 0xce; - buffer.writeUInt32BE(limit, 29); - buffer[33] = tarantoolConstants.KeysCode.offset; - buffer[34] = 0xce; - buffer.writeUInt32BE(offset || 0, 35); - buffer[39] = tarantoolConstants.KeysCode.iterator; - buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 40); - buffer[41] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 42); - - this.sendCommand( - tarantoolConstants.RequestCode.rqSelect, - reqId, - buffer, - [success, error], - arguments, - opts - ); -}; - -exports.delete = function _delete (spaceId, indexId, key, opts = {}){ - var _this = this; - if (Number.isInteger(key)) key = [key]; - if (!Array.isArray(key)) return Promise.reject(new TarantoolError('need array')); - if (typeof(spaceId)=='string' || typeof(indexId)=='string') { - return this._getMetadata(spaceId, indexId) - .then(function(info){ - return _this.delete(info[0], info[1], key, opts); - }) - } - var reqId = this._getRequestId(); - var bufKey = packr.encode(key); - - var len = 23+bufKey.length; - var buffer = this.createBuffer(5+len); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqDelete; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x83; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 26); - buffer[27] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 28); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqDelete, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.update = function update (spaceId, indexId, key, ops, opts = {}){ - if (Number.isInteger(key)) key = [key]; - if (!(Array.isArray(ops) && Array.isArray(key))) return Promise.reject(new TarantoolError('need array')); - - var _this = this; - - if (typeof(spaceId)=='string' || typeof(indexId)=='string') { - return this._getMetadata(spaceId, indexId) - .then(function(info){ - return _this.update(info[0], info[1], key, ops, opts); - }) - } - var reqId = this._getRequestId(); - var bufKey = packr.encode(key); - var bufOps = packr.encode(ops); - - var len = 24+bufKey.length+bufOps.length; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqUpdate; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x84; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.index_id; - buffer.writeUInt8(indexId, 26); - buffer[27] = tarantoolConstants.KeysCode.key; - bufKey.copy(buffer, 28); - buffer[28+bufKey.length] = tarantoolConstants.KeysCode.tuple; - bufOps.copy(buffer, 29+bufKey.length); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqUpdate, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.upsert = function upsert (spaceId, ops, tuple, opts = {}){ - var _this = this; - if (!Array.isArray(ops)) return Promise.reject(new TarantoolError('need ops array')); - if (typeof(spaceId)=='string') { - return this._getMetadata(spaceId, 0) - .then(function(info){ - return _this.upsert(info[0], ops, tuple, opts); - }) - } - var reqId = this._getRequestId(); - var bufTuple = packr.encode(tuple); - var bufOps = packr.encode(ops); - - var len = 22+bufTuple.length+bufOps.length; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqUpsert; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x83; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 26); - buffer[26+bufTuple.length] = tarantoolConstants.KeysCode.def_tuple; - bufOps.copy(buffer, 27+bufTuple.length); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqUpsert, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.eval = function eval (expression, opts = {}){ - var tuple = Array.prototype.slice.call(arguments, 1); - var reqId = this._getRequestId(); - var bufExp = getCachedMsgpackBuffer(expression); - var bufTuple = packr.encode(tuple ? tuple : []); - var len = 18+bufExp.length + bufTuple.length; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqEval; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.expression, 21); - bufExp.copy(buffer, 22); - buffer[22+bufExp.length] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 23+bufExp.length); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqEval, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.call = function call (functionName, opts = {}){ - var tuple = arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : []; - var reqId = this._getRequestId(); - var bufName = getCachedMsgpackBuffer(functionName); - var bufTuple = packr.encode(tuple ? tuple : []); - var len = 18+bufName.length + bufTuple.length; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqCall; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.function_name, 21); - bufName.copy(buffer, 22); - buffer[22+bufName.length] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 23+bufName.length); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqCall, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.sql = function sql (query, bindParams = [], opts = {}){ - var reqId = this._getRequestId(); - var bufParams = packr.encode(bindParams); - var isPreparedStatement = (typeof query === 'number') // in case of the statement ID being passed to 'query' param - var bufQuery = isPreparedStatement ? getCachedMsgpackBuffer(query) : packr.encode(query); // cache only prepared queries, considering them to be frequently used - - var len = 18+bufQuery.length + bufParams.length; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqExecute; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(isPreparedStatement ? tarantoolConstants.KeysCode.stmt_id : tarantoolConstants.KeysCode.sql_text, 21); - bufQuery.copy(buffer, 22); - buffer[22+bufQuery.length] = tarantoolConstants.KeysCode.sql_bind; - bufParams.copy(buffer, 23+bufQuery.length); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqExecute, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.sql = function sql (query, bindParams = [], opts = {}){ - var _this = this; - var _arguments = arguments; - var {promise, resolve, reject} = withResolvers(); - - var reqId = _this._getRequestId(); - var bufParams = packr.encode(bindParams); - var isPreparedStatement = (typeof query === 'number') // in case of the statement ID being passed to 'query' param - var bufQuery = isPreparedStatement ? getCachedMsgpackBuffer(query) : packr.encode(query); // cache only prepared queries, considering them frequently used - - var len = 18+bufQuery.length + bufParams.length; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqExecute; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(_this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(isPreparedStatement ? tarantoolConstants.KeysCode.stmt_id : tarantoolConstants.KeysCode.sql_text, 21); - bufQuery.copy(buffer, 22); - buffer[22+bufQuery.length] = tarantoolConstants.KeysCode.sql_bind; - bufParams.copy(buffer, 23+bufQuery.length); - - _this.sendCommand( - tarantoolConstants.RequestCode.rqExecute, - reqId, - buffer, - [resolve, reject], - _arguments, - opts - ); - - return promise; -}; - -exports.prepare = function prepare (query, opts = {}){ - var reqId = this._getRequestId(); - var bufQuery = packr.encode(query); - - var len = 13+bufQuery.length; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqPrepare; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.sql_text, 15); - bufQuery.copy(buffer, 16); - - return this.sendCommand( - tarantoolConstants.RequestCode.rqPrepare, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports.id = function id (version = 3, features = [1], auth_type = 'chap-sha1', opts = {}){ - var reqId = this._getRequestId(); - - var headersMap = new Map(); - headersMap.set(tarantoolConstants.KeysCode.code, tarantoolConstants.RequestCode.rqId) - headersMap.set(tarantoolConstants.KeysCode.sync, reqId) - var headersBuffer = packr.encode(headersMap) - - var bodyMap = new Map(); - bodyMap.set(tarantoolConstants.KeysCode.iproto_version, version) - bodyMap.set(tarantoolConstants.KeysCode.iproto_features, features) - bodyMap.set(tarantoolConstants.KeysCode.iproto_auth_type, auth_type) - var bodyBuffer = packr.encode(bodyMap) - - var dataLengthBuffer = packr.encode(headersBuffer.length + bodyBuffer.length); - var concatenatedBuffers = Buffer.concat([dataLengthBuffer, headersBuffer, bodyBuffer]) - - return this.sendCommand( - tarantoolConstants.RequestCode.rqId, - reqId, - concatenatedBuffers, - null, - arguments, - opts - ); -}; - -exports.insert = function insert (spaceId, tuple, opts = {}){ - return this._replaceInsert(tarantoolConstants.RequestCode.rqInsert, spaceId, tuple, opts); -}; - -exports.replace = function replace (spaceId, tuple, opts = {}){ - return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, spaceId, tuple, opts); -}; - -exports._replaceInsert = function _replaceInsert (cmd, spaceId, tuple, opts = {}){ - if (!Array.isArray(tuple)) return Promise.reject(new TarantoolError('need array')); - - var _this = this; - - if (typeof(spaceId)=='string') - { - return this._getMetadata(spaceId, 0) - .then(function(info){ - return _this._replaceInsert(cmd, info[0], tuple, opts); - }) - } - - var reqId = this._getRequestId(); - var bufTuple = packr.encode(tuple); - var len = 21+bufTuple.length; - var buffer = this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x83; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = cmd; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = tarantoolConstants.KeysCode.iproto_stream_id - buffer[15] = 0xce; - buffer.writeUInt32BE(this.streamId || 0, 16); - buffer[20] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 21); - buffer[22] = 0xcd; - buffer.writeUInt16BE(spaceId, 23); - buffer[25] = tarantoolConstants.KeysCode.tuple; - bufTuple.copy(buffer, 26); - - return this.sendCommand( - cmd, - reqId, - buffer, - null, - arguments, - opts - ); -}; - -exports._auth = function _auth (username, password){ - var _this = this; - return new Promise(function (resolve, reject) { - var reqId = _this._getRequestId(); - - var user = packr.encode(username); - var scrambled = scramble(password, _this.salt); - var len = 44+user.length; - var buffer = _this.createBuffer(len+5); - - buffer[0] = 0xce; - buffer.writeUInt32BE(len, 1); - buffer[5] = 0x82; - buffer[6] = tarantoolConstants.KeysCode.code; - buffer[7] = tarantoolConstants.RequestCode.rqAuth; - buffer[8] = tarantoolConstants.KeysCode.sync; - buffer[9] = 0xce; - buffer.writeUInt32BE(reqId, 10); - buffer[14] = 0x82; - buffer.writeUInt8(tarantoolConstants.KeysCode.username, 15); - user.copy(buffer, 16); - buffer[16+user.length] = tarantoolConstants.KeysCode.tuple; - buffer[17+user.length] = 0x92; - tarantoolConstants.passEnter.copy(buffer, 18+user.length); - buffer[28+user.length] = 0xb4; - scrambled.copy(buffer, 29+user.length); - - _this.sentCommands.set(reqId, [ - tarantoolConstants.RequestCode.rqAuth, - [resolve, reject] - ]) - _this.socket.write(buffer); - }); -}; - -exports.rqCommands = { - 0x01: exports.select, - 0x02: exports.insert, - 0x03: exports.replace, - 0x04: exports.update, - 0x05: exports.delete, - 0x06: exports.call, - 0x07: exports._auth, - 0x08: exports.eval, - 0x09: exports.upsert, - 0x10: exports.rollback, - 0x0b: exports.sql, - 0x0d: exports.prepare, - 0x0e: exports.begin, - 0x0f: exports.commit, - 0x40: exports.ping, - 0x49: exports.id, -}; - -function shatransform(t){ - return createHash('sha1').update(t).digest(); -} - -function xor(a, b) { - if (!Buffer.isBuffer(a)) a = Buffer.from(a); - if (!Buffer.isBuffer(b)) b = Buffer.from(b); - var res = []; - var i; - if (a.length > b.length) { - for (i = 0; i < b.length; i++) { - res.push(a[i] ^ b[i]); - } - } else { - for (i = 0; i < a.length; i++) { - res.push(a[i] ^ b[i]); - } - } - return Buffer.from(res); -} - -function scramble(password, salt){ - var encSalt = Buffer.from(salt, 'base64'); - var step1 = shatransform(password); - var step2 = shatransform(step1); - var step3 = shatransform( - Buffer.concat( - [ - encSalt.subarray(0, 20), - step2 - ] - ) - ); - return xor(step1, step3); -} \ No newline at end of file diff --git a/lib/connector.js b/lib/connector.js deleted file mode 100755 index 1f05cbc..0000000 --- a/lib/connector.js +++ /dev/null @@ -1,34 +0,0 @@ -var net = require('net'); -var tls = require('tls'); -var { TarantoolError } = require('./utils'); - -exports._disconnect = function () { - this.connecting = false; - if (this.socket) { - this.socket.end(); - } -}; - -exports._connect = function (callback) { - this.connecting = true; - - var _this = this; - if (!_this.connecting) { - callback(new TarantoolError('Connection is closed.')); - return; - } - try { - var connectionModule - if (typeof _this.options.tls == 'object') { - connectionModule = tls - _this.options = Object.assign(_this.options, _this.options.tls) - } else { - connectionModule = net - } - _this.socket = connectionModule.connect(_this.options); - } catch (err) { - callback(err); - return; - } - callback(null, _this.socket); -}; \ No newline at end of file diff --git a/lib/msgpack-extensions.js b/lib/msgpack-extensions.js deleted file mode 100644 index 22d2592..0000000 --- a/lib/msgpack-extensions.js +++ /dev/null @@ -1,136 +0,0 @@ -var { addExtension } = require('msgpackr'); -var { - TarantoolError -} = require('./utils'); -var { - parse: uuidParse, - stringify: uuidStringify -} = require('uuid'); -var { - Uint64BE, - Int64BE -} = require("int64-buffer"); - -var packAs = {}; - -// Pack big integers correctly (fix for https://github.com/tarantool/node-tarantool-driver/issues/48) -packAs.Integer = function (value) { - if (!Number.isInteger(value)) throw new TarantoolError("Passed value doesn't seems to be an integer") - - if (value > 2147483647) return Uint64BE(value) - if (value < -2147483648) return Int64BE(value) - - return value -} - -// UUID extension -packAs.Uuid = function TarantoolUuidExt (value) { - if (!(this instanceof TarantoolUuidExt)) { - return new TarantoolUuidExt(value) - } - - this.value = value -} - -addExtension({ - Class: packAs.Uuid, - type: 0x02, - pack(instance) { - return uuidParse(instance.value); - }, - unpack(buffer) { - return uuidStringify(buffer) - } -}); - -// Decimal extension -function isFloat(n){ - return Number(n) === n && n % 1 !== 0; -} - -packAs.Decimal = function TarantoolDecimalExt (value) { - if (!(this instanceof TarantoolDecimalExt)) { - return new TarantoolDecimalExt(value) - } - - if (!(Number.isInteger(value) || isFloat(value))) { - throw new TarantoolError('Passed value cannot be packed as decimal: expected integer or floating number') - } - - this.value = value -} - -function isOdd (number) { - return number % 2 !== 0; -}; - -addExtension({ - Class: packAs.Decimal, - type: 0x01, - pack(instance) { - var strNum = instance.value.toString() - var rawNum = strNum.replace('-', '') - var rawNumSplitted1 = rawNum.split('.')[1] - var decimalBuffer = Buffer.allocUnsafe(1); - decimalBuffer.writeInt8(rawNumSplitted1 && rawNum.split('.')[1].length || 0) - var bufHexed = decimalBuffer.toString('hex') - + rawNum.replace('.', '') - + (strNum.startsWith('-') ? 'b' : 'a') - - if (isOdd(bufHexed.length)) { - bufHexed = bufHexed.slice(0, 2) + '0' + bufHexed.slice(2) - } - - return Buffer.from(bufHexed, 'hex') - }, - unpack(buffer) { - var scale = buffer.readIntBE(0, 1) - var hex = buffer.toString('hex') - - var sign = ['b', 'd'].includes(hex.slice(-1)) ? '-' : '+' - var slicedValue = hex.slice(2).slice(0, -1) - - // readDoubleBE - if (scale > 0) { - var nScale = scale * -1 - slicedValue = slicedValue.slice(0, nScale) + '.' + slicedValue.slice(nScale) - } - - return parseFloat(sign + slicedValue) - } -}); - -// Datetime extension -addExtension({ - Class: Date, - type: 0x04, - pack(instance) { - var seconds = instance.getTime() / 1000 | 0 - var nanoseconds = instance.getMilliseconds() * 1000 - - var datetimeBuffer = Buffer.allocUnsafe(16); - datetimeBuffer.writeBigUInt64LE(BigInt(seconds)) - datetimeBuffer.writeUInt32LE(nanoseconds, 8) - datetimeBuffer.writeUInt32LE(0, 12) - /* - Node.Js 'Date' doesn't provide nanoseconds, so just using milliseconds. - tzoffset is set to UTC, and tzindex is omitted. - */ - - return datetimeBuffer; - }, - unpack(buffer) { - var time = new Date(parseInt(buffer.readBigUInt64LE(0)) * 1000) - - if (buffer.length > 8) { - var milliseconds = (buffer.readUInt32LE(8) / 1000 | 0) - time.setMilliseconds(milliseconds) - } - - return time; - } -}); - -for (var packerName of Object.keys(packAs)) { - exports['pack' + packerName] = packAs[packerName] -} \ No newline at end of file diff --git a/lib/pipeline-response.js b/lib/pipeline-response.js deleted file mode 100644 index 71b2b0b..0000000 --- a/lib/pipeline-response.js +++ /dev/null @@ -1,34 +0,0 @@ -function findPipelineError (array = []) { - var error = (array || this).find(element => element[0]) - return error[0] ?? null; -}; - -function findPipelineErrors (array = []) { - var aoe = []; - for (var subarray of (array || this)) { - var errored_element = subarray[0] - if (errored_element) aoe.push(errored_element) - } - - return aoe; -}; - -class PipelineResponse extends Array { - findPipelineError = findPipelineError; - findPipelineErrors = findPipelineErrors; - static findPipelineError = findPipelineError; - static findPipelineErrors = findPipelineErrors; - - constructor (arr) { - super(...arr) - }; - - get pipelineError () { - return this.findPipelineError() - }; - - get pipelineErrors () { - return this.findPipelineErrors() - } -} -module.exports = PipelineResponse; \ No newline at end of file diff --git a/lib/pipeline.js b/lib/pipeline.js deleted file mode 100644 index 0c62702..0000000 --- a/lib/pipeline.js +++ /dev/null @@ -1,101 +0,0 @@ -var commandsPrototype = require('./commands'); -var PipelineResponse = require('./pipeline-response'); - -function commandInterceptorFactory (method) { - return function commandInterceptor () { - this.pipelinedCommands.push([method, arguments]); - return this - } -} - -var commandsPrototypeKeys = Object.keys(commandsPrototype); -var setOfCommandInterceptors = {}; -for (var commandKey of commandsPrototypeKeys) { - setOfCommandInterceptors[commandKey] = commandInterceptorFactory(commandKey) -} - -function sendCommandInterceptor (requestCode, reqId, buffer, callbacks, commandArguments, opts) { - // If the Select request was made to search for a system space/index name, then bypass this - if (opts.autoPipeline === false) { - return this._parent.sendCommand(requestCode, reqId, buffer, callbacks, commandArguments, opts) - } - - this._buffersConcatenedCounter++ - this.buffers.push(buffer) - - opts._pipelined = true; - // firstly add the command to 'sentCommands' Map - var send = this._parent.sendCommand(requestCode, reqId, buffer, callbacks, commandArguments, opts) - // then write the buffer - // in order to avoid the possible race conditions (?) - this._trySendConcatenedBuffer() - - return send; -} - -function exec () { - var _this = this; - var promises = []; - for (var interceptedCommand of this.pipelinedCommands) { - var args = interceptedCommand[1] - var method = interceptedCommand[0] - promises.push( - new Promise(function (resolve) { - _this._originalMethods[method].apply(_this._originalMethods, args) - .then(function (result) { - resolve([null, result]) - }) - .catch(function (error) { - _this._buffersConcatenedCounter++ // fake, because rejected promise doesn't have its request buffer - _this._trySendConcatenedBuffer() - resolve([error, null]) - }) - }) - ); - } - - return new Promise(function (resolve) { - Promise.all(promises) - .then(function (result) { - resolve( - new PipelineResponse(result) - ) - }) - }) -} - -class Pipeline { - constructor (self) { - var _this = this; - this._parent = self; - this.buffers = []; - this.pipelinedCommands = []; - this._buffersConcatenedCounter = 0; - this._originalMethods = Object.assign({}, - commandsPrototype, - self, - { - _id: self._id, - sendCommand: sendCommandInterceptor.bind(_this) - } - ) - this.exec = exec; - Object.assign(this, setOfCommandInterceptors); - }; - - flushPipelined () { - this.pipelinedCommands = []; - this.buffers = []; - }; - - _trySendConcatenedBuffer () { - if (this._buffersConcatenedCounter == this.pipelinedCommands.length) { - this._parent.socket.write(Buffer.concat(this.buffers)) - this.flushPipelined() - } - }; -} - -module.exports.pipeline = function () { - return new Pipeline(this); -}; \ No newline at end of file diff --git a/lib/sliderBuffer.js b/lib/sliderBuffer.js deleted file mode 100644 index 422b82b..0000000 --- a/lib/sliderBuffer.js +++ /dev/null @@ -1,78 +0,0 @@ -var { createBuffer } = require('./utils'); - -function SliderBuffer(){ - this.bufferOffset = 0; - this.bufferLength = 0; - this.buffer = createBuffer(1024*10); -} - -SliderBuffer.prototype.add = function(data, from, size){ - var multiplierBuffer = 2; - var newBuffer; - var destLen; - var newLen; - if (typeof from !== 'undefined' && typeof size !== 'undefined') - { - if (this.bufferOffset + this.bufferLength + size < this.buffer.length) - { - data.copy(this.buffer, this.bufferOffset + this.bufferLength, from, from+size); - this.bufferLength+=size; - } - else - { - destLen = size + this.bufferLength; - if (this.buffer.length > destLen) - { - newBuffer = createBuffer(this.buffer.length * multiplierBuffer); - this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset+this.bufferLength); - data.copy(newBuffer, this.bufferLength, from, from+size); - this.buffer = newBuffer; - } - else - { - newLen = this.buffer.length*multiplierBuffer; - while(newLen < destLen) - newLen *= multiplierBuffer; - newBuffer = createBuffer(newLen); - this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset+this.bufferLength); - data.copy(newBuffer, this.bufferLength, from, from+size); - this.buffer = newBuffer; - } - this.bufferOffset = 0; - this.bufferLength = destLen; - } - } - else - { - if (this.bufferOffset + this.bufferLength + data.length < this.buffer.length) - { - data.copy(this.buffer, this.bufferOffset + this.bufferLength); - this.bufferLength+=data.length; - } - else - { - destLen = data.length + this.bufferLength; - if (this.buffer.length > destLen) - { - newBuffer = createBuffer(this.buffer.length * multiplierBuffer); - this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset+this.bufferLength); - data.copy(newBuffer, this.bufferLength); - this.buffer = newBuffer; - } - else - { - newLen = this.buffer.length*multiplierBuffer; - while(newLen < destLen) - newLen *= multiplierBuffer; - newBuffer = createBuffer(newLen); - this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset+this.bufferLength); - data.copy(newBuffer, this.bufferLength); - this.buffer = newBuffer; - } - this.bufferOffset = 0; - this.bufferLength = destLen; - } - } -}; - -module.exports = SliderBuffer; \ No newline at end of file diff --git a/lib/transaction.js b/lib/transaction.js deleted file mode 100644 index 5facc85..0000000 --- a/lib/transaction.js +++ /dev/null @@ -1,17 +0,0 @@ -class Transaction { - constructor (self) { - this.streamId = self._getRequestId(); - Object.assign( - this, - self - ) - }; -}; - -function createTransaction () { - return new Transaction(this); -} - -Transaction.prototype.transaction = createTransaction - -module.exports = Transaction \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index a384fe1..0000000 --- a/lib/utils.js +++ /dev/null @@ -1,109 +0,0 @@ -var { states } = require("./const"); - -// it is faster to reuse the existing buffer than allocating a new one -var preallocBuf = Buffer.allocUnsafe(Buffer.poolSize); -var shouldSetImmediate = true; -var bufferSizeMultiplier = 2; - -exports.createBuffer = function createBuffer (size){ - // prevent the old existing buffers from being overwritten - if (this.enableAutoPipelining || this.nonWritableHostPolicy || !this.socket || !this.socket.writable || (this.state !== states.CONNECT)) { - return Buffer.allocUnsafe(size); - } - - // flush preallocated buffer at the end of loop to prevent OOM - if (shouldSetImmediate) { - shouldSetImmediate = false; - setImmediate(function () { - preallocBuf = Buffer.allocUnsafe(Buffer.poolSize); - shouldSetImmediate = true; - }); - } - - // create a bigger buffer - if (size > preallocBuf.length) { - preallocBuf = Buffer.allocUnsafe(size * bufferSizeMultiplier); - } - - return preallocBuf.subarray(0, size); -}; - -// draft; Performance is a bit worse in autopipelining mode -var offset = 0; -exports.createBuffer2 = function createBuffer (size){ - // flush preallocated buffer at the end of event loop to prevent OOM - if (shouldSetImmediate) { - shouldSetImmediate = false; - setImmediate(function () { - preallocateBuf(Buffer.poolSize) - shouldSetImmediate = true; - }); - } - - // check if should create a bigger buffer - var fullLen = size + offset; - if (fullLen > preallocBuf.length) { - preallocateBuf(fullLen) - } - - return preallocBuf.subarray(offset, offset += size); -}; - -function preallocateBuf (size) { - preallocBuf = Buffer.allocUnsafe(size * bufferSizeMultiplier); - offset = 0; -} - -exports.parseURL = function(str, reserve){ - var result = {}; - if (str.startsWith('/')) { - result.path = str - return result - } - var parsed = str.split(':'); - if(reserve){ - result.username = null; - result.password = null; - } - switch (parsed.length){ - case 1: - result.host = parsed[0]; - break; - case 2: - result.host = parsed[0]; - result.port = parsed[1]; - break; - default: - result.username = parsed[0]; - result.password = parsed[1].split('@')[0]; - result.host = parsed[1].split('@')[1]; - result.port = parsed[2]; - } - return result; -}; - -exports.TarantoolError = function (msg, opts) { - var err = new Error(msg, opts); - err.name = 'TarantoolError'; - return err; -} - -function withResolversPoly () { - var resolve = Promise.resolve; - var reject = Promise.reject; - var promise = new Promise(function (_resolve, _reject) { - resolve = _resolve; - reject = _reject; - }); - - return { - promise, - resolve, - reject - }; -}; - -function withResolvers () { - return Promise.withResolvers() -} -exports.withResolvers = Promise.withResolvers ? withResolvers : withResolversPoly \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100755 index ae76a4e..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2248 +0,0 @@ -{ - "name": "tarantool-driver", - "version": "3.0.0", - "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 - }, - "acorn": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", - "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=", - "dev": true - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "3.3.0" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "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.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "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.9", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", - "dev": true, - "requires": { - "sprintf-js": "1.0.3" - } - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "1.0.3" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "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.0.2", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", - "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", - "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.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", - "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.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "benchmark": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", - "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", - "dev": true, - "requires": { - "lodash": "4.17.4", - "platform": "1.3.4" - } - }, - "bluebird": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=", - "dev": true - }, - "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.16.3" - } - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "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.4", - "lazy-cache": "1.0.4" - } - }, - "chai": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.0.tgz", - "integrity": "sha1-MxoDkbVcOvh0CunDt0WLwcOAXm0=", - "dev": true, - "requires": { - "assertion-error": "1.0.2", - "check-error": "1.0.2", - "deep-eql": "2.0.2", - "get-func-name": "2.0.0", - "pathval": "1.1.0", - "type-detect": "4.0.3" - } - }, - "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.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "circular-json": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", - "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "1.0.1" - } - }, - "cli-width": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", - "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", - "dev": true - }, - "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.3", - "right-align": "0.1.3", - "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 - } - } - }, - "cluster-key-slot": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.8.tgz", - "integrity": "sha1-dlRVYIWmUzCTKi6LWXb44tCz5BQ=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "commander": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", - "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", - "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 - }, - "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.2.11", - "typedarray": "0.0.6" - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.1.tgz", - "integrity": "sha1-1wu5rMGDXsTwY/+drFQjwXsR8Xg=", - "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" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "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.9", - "esprima": "2.7.3" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "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.10.1" - } - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "0.10.23" - } - }, - "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=", - "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": "2.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-2.0.2.tgz", - "integrity": "sha1-sbrAblbwp2d3aG1Qyf63XC7XZ5o=", - "dev": true, - "requires": { - "type-detect": "3.0.0" - }, - "dependencies": { - "type-detect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-3.0.0.tgz", - "integrity": "sha1-RtDMhVOrt7E6NSsNbeov1Y8tm1U=", - "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 - }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.0", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.6.1" - } - }, - "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 - }, - "denque": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.2.1.tgz", - "integrity": "sha512-Ak/DUA1K1wMpamAfz3BYXHdeN6Bmbw6CC48QCMbn8DL8idfxEGIdVNjCwpkdTcT34uRY16/+faA6RzwXh9t6mw==" - }, - "diff": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", - "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", - "dev": true - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "2.0.2", - "isarray": "1.0.0" - } - }, - "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.1" - } - }, - "es5-ext": { - "version": "0.10.23", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.23.tgz", - "integrity": "sha1-dXi1G+l0IHpUh4IbVlOMIk5Oezg=", - "dev": true, - "requires": { - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" - } - }, - "es6-iterator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", - "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23", - "es6-symbol": "3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23", - "es6-iterator": "2.0.1", - "es6-set": "0.1.5", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23", - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23", - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" - } - }, - "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.3", - "estraverse": "1.9.3", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.2.0" - }, - "dependencies": { - "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 - } - } - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "0.1.5", - "es6-weak-map": "2.0.2", - "esrecurse": "4.1.0", - "estraverse": "4.2.0" - } - }, - "eslint": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", - "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "concat-stream": "1.6.0", - "debug": "2.6.8", - "doctrine": "1.5.0", - "es6-map": "0.1.5", - "escope": "3.6.0", - "espree": "3.4.3", - "estraverse": "4.2.0", - "esutils": "2.0.2", - "file-entry-cache": "1.3.1", - "glob": "7.1.2", - "globals": "9.18.0", - "ignore": "3.3.3", - "imurmurhash": "0.1.4", - "inquirer": "0.12.0", - "is-my-json-valid": "2.16.0", - "is-resolvable": "1.0.0", - "js-yaml": "3.8.4", - "json-stable-stringify": "1.0.1", - "levn": "0.3.0", - "lodash": "4.17.4", - "mkdirp": "0.5.1", - "optionator": "0.8.2", - "path-is-absolute": "1.0.1", - "path-is-inside": "1.0.2", - "pluralize": "1.2.1", - "progress": "1.1.8", - "require-uncached": "1.0.3", - "shelljs": "0.6.1", - "strip-json-comments": "1.0.4", - "table": "3.8.3", - "text-table": "0.2.0", - "user-home": "2.0.0" - } - }, - "espree": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", - "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", - "dev": true, - "requires": { - "acorn": "5.0.3", - "acorn-jsx": "3.0.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "esrecurse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz", - "integrity": "sha1-RxO2U2rffyrE8yfVWed1a/9kgiA=", - "dev": true, - "requires": { - "estraverse": "4.1.1", - "object-assign": "4.1.1" - }, - "dependencies": { - "estraverse": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz", - "integrity": "sha1-9srKcokzqFDvkGYdDheYK6RxEaI=", - "dev": true - } - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.23" - } - }, - "event-lite": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.1.tgz", - "integrity": "sha1-R88IqNN9C2lM23s7F7UfqsZXYIY=" - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "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.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", - "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 - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" - } - }, - "file-entry-cache": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", - "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", - "dev": true, - "requires": { - "flat-cache": "1.2.2", - "object-assign": "4.1.1" - } - }, - "flat-cache": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", - "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", - "dev": true, - "requires": { - "circular-json": "0.3.1", - "del": "2.2.2", - "graceful-fs": "4.1.11", - "write": "0.2.1" - } - }, - "flexbuffer": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/flexbuffer/-/flexbuffer-0.0.6.tgz", - "integrity": "sha1-A5/fI/iCPkQMOPMnfm/vEXQhWzA=", - "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.16" - } - }, - "formatio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true, - "requires": { - "samsam": "1.2.1" - } - }, - "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.2" - } - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "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": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "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.10", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", - "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", - "dev": true, - "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" - }, - "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": "1.0.1" - } - } - } - }, - "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.3", - "commander": "2.11.0", - "is-my-json-valid": "2.16.0", - "pinkie-promise": "2.0.1" - }, - "dependencies": { - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - } - } - }, - "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.1.1" - } - }, - "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.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "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.4.0", - "sshpk": "1.13.1" - } - }, - "ieee754": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" - }, - "ignore": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", - "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "1.4.0", - "ansi-regex": "2.1.1", - "chalk": "1.1.3", - "cli-cursor": "1.0.2", - "cli-width": "2.1.0", - "figures": "1.7.0", - "lodash": "4.17.4", - "readline2": "1.0.1", - "run-async": "0.1.0", - "rx-lite": "3.1.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "through": "2.3.8" - } - }, - "int64-buffer": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.9.tgz", - "integrity": "sha1-ngOdoEOyT3ixlrKD4EZT716ZD2E=" - }, - "ioredis": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-3.1.2.tgz", - "integrity": "sha512-YZlF/P58/DKBvRnXTFeMVo759c5BeJvD0ao9pLiHRM8Mpfxx7mMktdLdWtDcqLATnLVaUbWw/pZS8R66TBIe/w==", - "dev": true, - "requires": { - "bluebird": "3.5.0", - "cluster-key-slot": "1.0.8", - "debug": "2.6.8", - "denque": "1.2.1", - "flexbuffer": "0.0.6", - "lodash": "4.17.4", - "redis-commands": "1.3.1", - "redis-parser": "2.6.0" - } - }, - "is-buffer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-my-json-valid": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", - "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", - "dev": true, - "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", - "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", - "dev": true, - "requires": { - "is-path-inside": "1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", - "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", - "dev": true, - "requires": { - "path-is-inside": "1.0.2" - } - }, - "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-resolvable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", - "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", - "dev": true, - "requires": { - "tryit": "1.0.3" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "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.9", - "async": "1.5.2", - "escodegen": "1.8.1", - "esprima": "2.7.3", - "glob": "5.0.15", - "handlebars": "4.0.10", - "js-yaml": "3.8.4", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "once": "1.4.0", - "resolve": "1.1.7", - "supports-color": "3.2.3", - "which": "1.3.0", - "wordwrap": "1.0.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "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.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "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" - } - } - } - }, - "jade": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "dev": true, - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", - "dev": true - }, - "mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", - "dev": true - } - } - }, - "js-yaml": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz", - "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=", - "dev": true, - "requires": { - "argparse": "1.0.9", - "esprima": "3.1.3" - } - }, - "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-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "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 - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "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.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", - "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "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 - } - } - }, - "just-extend": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.22.tgz", - "integrity": "sha1-MzCvdWyralQnAMZLLk5KoGLVL/8=", - "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": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "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 - }, - "lolex": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.1.2.tgz", - "integrity": "sha1-JpS5U8nqTQE+W4v7qJHJkQJbJik=", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", - "dev": true - }, - "mime-db": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", - "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=", - "dev": true - }, - "mime-types": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", - "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", - "dev": true, - "requires": { - "mime-db": "1.29.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.8" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "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" - } - }, - "mocha": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", - "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", - "dev": true, - "requires": { - "commander": "2.3.0", - "debug": "2.2.0", - "diff": "1.4.0", - "escape-string-regexp": "1.0.2", - "glob": "3.2.11", - "growl": "1.9.2", - "jade": "0.26.3", - "mkdirp": "0.5.1", - "supports-color": "1.2.0", - "to-iso-string": "0.0.2" - }, - "dependencies": { - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true, - "requires": { - "ms": "0.7.1" - } - }, - "escape-string-regexp": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", - "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", - "dev": true - }, - "glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "minimatch": "0.3.0" - } - }, - "minimatch": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - }, - "supports-color": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", - "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", - "dev": true - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "msgpack-lite": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", - "integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=", - "requires": { - "event-lite": "0.1.1", - "ieee754": "1.1.8", - "int64-buffer": "0.1.9", - "isarray": "1.0.0" - } - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "nanotimer": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/nanotimer/-/nanotimer-0.3.14.tgz", - "integrity": "sha1-ENgR+NBkeIGACWzh+WxwhG/Voro=", - "dev": true - }, - "native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", - "dev": true - }, - "nise": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.0.1.tgz", - "integrity": "sha1-DakrEKhU6XwPSW9sKEWjASgLPu8=", - "dev": true, - "requires": { - "formatio": "1.2.0", - "just-extend": "1.1.22", - "lolex": "1.6.0", - "path-to-regexp": "1.7.0" - }, - "dependencies": { - "lolex": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", - "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", - "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.0.9" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "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 - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "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.0.2" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "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.8", - "wordwrap": "0.0.3" - }, - "dependencies": { - "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.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "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 - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "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.4" - } - }, - "platform": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.4.tgz", - "integrity": "sha1-bw+xftqqSPIUQrOpdcBjEw8cPr0=", - "dev": true - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "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 - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "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 - }, - "readable-stream": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.11.tgz", - "integrity": "sha512-h+8+r3MKEhkiVrwdKL8aWs1oc1VvBu33ueshOvS26RsZQ3Amhx/oO3TKe4lApSV9ueY6as8EAh7mtuFjdlhg9Q==", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.0.1", - "string_decoder": "1.0.2", - "util-deprecate": "1.0.2" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "mute-stream": "0.0.5" - } - }, - "redis-commands": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz", - "integrity": "sha1-gdgm9F+pyLIBH0zXoP5ZfSQdRCs=", - "dev": true - }, - "redis-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", - "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=", - "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.6.0", - "caseless": "0.11.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "2.0.6", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.16", - "oauth-sign": "0.8.2", - "qs": "6.3.2", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.4.3", - "uuid": "3.1.0" - } - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "0.1.0", - "resolve-from": "1.0.1" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "1.1.1", - "onetime": "1.1.0" - } - }, - "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.4" - } - }, - "rimraf": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "1.4.0" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "safe-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", - "dev": true - }, - "samsam": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz", - "integrity": "sha1-7dOQk6MYQ3DLhZJDsr3yVefY6mc=", - "dev": true - }, - "shelljs": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", - "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=", - "dev": true - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true - }, - "sinon": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-3.0.0.tgz", - "integrity": "sha512-oGoqOflgmoKm+lHkGsUw5IjxOu3Nat0WfoJpWFj8sklC1KDCGamkg/nDJGJAv9hXLY5KiflDoY/7ewgfsbNLTA==", - "dev": true, - "requires": { - "diff": "3.3.0", - "formatio": "1.2.0", - "lolex": "2.1.2", - "native-promise-only": "0.8.1", - "nise": "1.0.1", - "path-to-regexp": "1.7.0", - "samsam": "1.2.1", - "text-encoding": "0.6.4", - "type-detect": "4.0.3" - }, - "dependencies": { - "diff": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", - "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==", - "dev": true - } - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "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.16.3" - } - }, - "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": "1.0.1" - } - }, - "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.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "dev": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "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 - } - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.2.tgz", - "integrity": "sha1-sp4fThEl+pehA4K4pTNze3SR4Xk=", - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "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.1.1" - } - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - }, - "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 - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "4.11.8", - "ajv-keywords": "1.5.1", - "chalk": "1.1.3", - "lodash": "4.17.4", - "slice-ansi": "0.0.4", - "string-width": "2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz", - "integrity": "sha1-Y1xUNsxypuDDh87KJ41OLuxSaH4=", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "text-encoding": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "to-iso-string": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", - "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", - "dev": true - }, - "tough-cookie": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", - "dev": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tryit": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", - "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", - "dev": true - }, - "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": "4.0.3", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", - "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "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.6", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", - "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 - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "1.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", - "dev": true - }, - "verror": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", - "dev": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "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 - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "0.5.1" - } - }, - "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.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" - } - } - } -} diff --git a/test.sh b/test.sh deleted file mode 100755 index 7e4a248..0000000 --- a/test.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# curl http://tarantool.org/dist/public.key | sudo apt-key add - -# echo "deb http://tarantool.org/dist/master/ubuntu/ `lsb_release -c -s` main" | sudo tee -a /etc/apt/sources.list.d/tarantool.list -# sudo apt-get update > /dev/null -sudo docker pull tarantool/tarantool:1.7 -sudo docker run --name tarantool -p33013:33013 -d -v `pwd`/test:/opt/tarantool tarantool/tarantool:1.7 tarantool /opt/tarantool/box.lua -sudo docker run --name reserve -p33014:33014 -d -v `pwd`/test:/opt/tarantool tarantool/tarantool:1.7 tarantool /opt/tarantool/box1.lua -sudo docker run --name reserve_2 -p33015:33015 -d -v `pwd`/test:/opt/tarantool tarantool/tarantool:1.7 tarantool /opt/tarantool/box2.lua -npm run test -sudo docker stop -t 2 tarantool -sudo docker rm tarantool -sudo docker stop -t 2 reserve -sudo docker rm reserve -sudo docker stop -t 2 reserve_2 -sudo docker rm reserve_2 \ No newline at end of file diff --git a/test/box.lua b/test/box.lua deleted file mode 100755 index 0a68abf..0000000 --- a/test/box.lua +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env tarantool -box.cfg{ - listen = { - 33013, - '/tmp/tarantool-test.sock' -- most of the UNIX-like systems (including MacOS) have the '/tmp' folder - } -} - -lp = { - test = 'test', - test_empty = '', - test_big = '123456789012345678901234567890123456789012345678901234567890' -- '1234567890' * 6 -} - -for k, v in pairs(lp) do - if #box.space._user.index.name:select{k} == 0 then - box.schema.user.create(k, { password = v }) - if k == 'test' then - box.schema.user.grant('test', 'read', 'space', '_space') - box.schema.user.grant('test', 'read', 'space', '_index') - box.schema.user.grant('test', 'execute', 'universe') - end - end -end - -if not box.space.test then - local test = box.schema.space.create('test') - test:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'NUM'}}) - test:create_index('secondary', {type = 'TREE', unique = false, parts = {2, 'NUM', 3, 'STR'}}) - box.schema.user.grant('test', 'read,write', 'space', 'test') -end - -function test_delete(num) - box.space.test:delete{num} -end - -function myprint(some) - print(some) -end - - -if not box.space.msgpack then - local msgpack = box.schema.space.create('msgpack') - msgpack:create_index('primary', {parts = {1, 'NUM'}}) - box.schema.user.grant('test', 'read,write', 'space', 'msgpack') - msgpack:insert{1, 'float as key', {[2.7] = {1, 2, 3}}} - msgpack:insert{2, 'array as key', {[{2, 7}] = {1, 2, 3}}} - msgpack:insert{3, 'array with float key as key', {[{[2.7] = 3, [7] = 7}] = {1, 2, 3}}} - msgpack:insert{6, 'array with string key as key', {['megusta'] = {1, 2, 3}}} -end - - -if not box.space.batched then - local batched = box.schema.space.create('batched') - batched:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'NUM'}}) - box.schema.user.grant('test', 'read,write', 'space', 'batched') -end - -function batch (data) - print(data) - for index,value in pairs(data) do - box.space.batched:insert(value) - end -end - -function myget(id) - val = box.space.batched:select{id} - return val[1] -end - -if not box.space.toaddmore then - local toaddmore = box.schema.space.create('toaddmore') - toaddmore:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'STR'}}) - box.schema.user.grant('test', 'read,write', 'space', 'toaddmore') - box.schema.user.grant('test', 'read,write', 'space', '_index') -end - -if not box.space.upstest then - local s = box.schema.space.create('upstest') - s:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'NUM'}}) - box.schema.user.grant('test', 'read,write', 'space', 'upstest') -end - -function clearaddmore() - local values = box.space.toaddmore:select{} - for k,v in pairs(values) do - if v[1] then - box.space.toaddmore:delete{v[1]} - end - end -end - -if not box.schema.func.exists('clearaddmore') then - box.schema.func.create('clearaddmore') - box.schema.user.grant('test', 'execute', 'function', 'clearaddmore') -end -if not box.schema.func.exists('myget') then - box.schema.func.create('myget') - box.schema.user.grant('test', 'execute', 'function', 'myget') -end -if not box.schema.func.exists('batch') then - box.schema.func.create('batch') - box.schema.user.grant('test', 'execute', 'function', 'batch') -end -if not box.schema.func.exists('myprint') then - box.schema.func.create('myprint') - box.schema.user.grant('test', 'execute', 'function', 'myprint') -end -if not box.schema.func.exists('test_delete') then - box.schema.func.create('test_delete') - box.schema.user.grant('test', 'execute', 'function', 'test_delete') -end - -function func_foo() - return {foo='foo', bar=42} -end - -function func_sum(x, y) - return x + y -end - -function func_arg(arg) - return arg -end \ No newline at end of file diff --git a/test/box1.lua b/test/box1.lua deleted file mode 100644 index 7506b78..0000000 --- a/test/box1.lua +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env tarantool -box.cfg{listen=33014} - -if not box.schema.user.exists('test') then - box.schema.user.create('test', {password = 'test'}) - box.schema.user.grant('test', 'execute', 'universe') -end \ No newline at end of file diff --git a/test/box2.lua b/test/box2.lua deleted file mode 100644 index c5abc92..0000000 --- a/test/box2.lua +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env tarantool -box.cfg{listen=33015} \ No newline at end of file From 8e3cbec14d2078581ff3066d6cf33d0fa1b1d236 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:51:51 +0100 Subject: [PATCH 10/12] Create tests.yml --- .github/workflows/tests.yml | 53 +++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1dc2b06 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,53 @@ +name: Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20, 22, 24] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Start Tarantool container + run: | + docker run \ + --name tarantool-test-box \ + -v ${{ github.workspace }}/assets:/opt/tarantool \ + -v /tmp:/tmp \ + --rm \ + -d \ + -p 3301:3301 \ + -e TT_DATABASE_USE_MVCC_ENGINE=true \ + -e TT_IPROTO_THREADS=2 \ + tarantool/tarantool:latest tarantool /opt/tarantool/box.lua + + - name: Wait for Tarantool to be ready + run: sleep 2 + + - name: Lint + run: npm run lint + + - name: Run tests + run: npm test + + - name: Stop Tarantool container + if: always() + run: docker stop tarantool-test-box || true From af9f1f699b0c31e09921e4eb7771ed0d9306f1e4 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:57:22 +0100 Subject: [PATCH 11/12] Update tests.yml Fix tests --- .github/workflows/tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1dc2b06..350331e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,10 +21,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'npm' - name: Install dependencies - run: npm ci + run: npm install - name: Start Tarantool container run: | From 130a843329b639c0f2efd37ef72ef54718809d02 Mon Sep 17 00:00:00 2001 From: goodwise <159438611+goodwise@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:00:45 +0100 Subject: [PATCH 12/12] Update index.js Fix support for NodeJS v20 --- lib/utils/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils/index.js b/lib/utils/index.js index eeb1d5e..8a13851 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -44,7 +44,7 @@ function withResolversPoly() { }; } -exports.withResolvers = Promise.withResolvers.bind(Promise) ?? withResolversPoly; +exports.withResolvers = Promise.withResolvers ? Promise.withResolvers.bind(Promise) : withResolversPoly; exports.applyMixin = function applyMixin(derivedConstructor, mixinConstructor) { Object.getOwnPropertyNames(mixinConstructor.prototype).forEach((name) => { @@ -186,4 +186,4 @@ const rejectSentCommands = function (err) { } this.sentCommands.clear(); } -module.exports.rejectSentCommands = rejectSentCommands; \ No newline at end of file +module.exports.rejectSentCommands = rejectSentCommands;