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/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..350331e
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,52 @@
+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 }}
+
+ - name: Install dependencies
+ run: npm install
+
+ - 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
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.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
+
+
\ 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:
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index ccab1e2..9683524 100755
--- a/README.md
+++ b/README.md
@@ -1,426 +1,714 @@
-# Node.js driver for tarantool 1.7+
+# Node.js driver for Tarantool 1.7+ β‘
[](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 [ioredis](https://github.com/luin/ioredis).
+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: - `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.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
+
+### π Connection Methods
-`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.
+#### tarantool.connect() β `Promise`
-**This feature is borrowed from the [ioredis](https://github.com/luin/ioredis)**
+Establishes connection to the Tarantool server.
-## Usage example
+**Returns:** Promise that resolves when connected and authenticated, rejects on error.
-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');
+await conn.connect();
+```
-// select arguments space_id, index_id, limit, offset, iterator, key
-conn.select(512, 0, 1, 0, 'eq', [50])
- .then(funtion(results){
- doSomeThingWithResults(results);
- });
+#### tarantool.disconnect() β `undefined`
+
+Closes the connection immediately. Pending commands may be lost.
+
+**Returns:** `undefined`
+
+```javascript
+conn.disconnect();
```
+#### tarantool.quit() β `Promise`
-## Msgpack implementation
+Gracefully closes the connection after all sent commands complete.
-You can use any implementation that can be duck typing with next interface:
+**Returns:** Promise that resolves after all pending commands finish and connection closes.
-```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
+await conn.quit();
```
-By default use msgpack-lite package.
+#### tarantool._auth(login: `string`, password: `string`) β `Promise`
-## API reference
+**Internal method.** Authenticates with Tarantool using CHAP-SHA1 mechanism.
-### tarantool.connect() β Promise
+**Note:** Called automatically during connection if credentials are provided.
-Resolve if connected. Or reject if not.
+See [Tarantool authentication docs](http://tarantool.org/doc/book/box/authentication.html) for details.
-### tarantool._auth(login: String, password: String) β Promise
+---
-**An internal method. The connection should be established before invoking.**
+### π Data Query Methods
-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.select(spaceId, indexId, limit, offset, iterator, key, [opts], [cb]) β `Promise`
-### tarantool.packUuid(uuid: String)
+Performs a SELECT query on the database.
-**Method for converting [UUID values](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#uuid) to Tarantool-compatible format.**
+**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)`
-If passing UUID without converion via this method, server will accept it as simple String.
+**Returns:** Promise resolving to array of tuples. If callback provided, returns `undefined`.
-### tarantool.packDecimal(numberToConvert: Number)
+**Examples:**
-**Method for converting Numbers (Float or Integer) to Tarantool [Decimal](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#decimal) type.**
+```javascript
+// By space/index ID
+const results = await conn.select(512, 0, 10, 0, 'eq', [50]);
-If passing number without converion via this method, server will accept it as Integer or Double (for JS Float type).
+// By space/index name
+const results = await conn.select('users', 'primary', 10, 0, 'eq', [50]);
-### tarantool.packInteger(numberToConvert: Number)
+// With callback
+conn.select(512, 0, 10, 0, 'eq', [50], {}, (err, results) => {
+ if (err) console.error(err);
+ else console.log(results);
+});
-**Method for safely passing numbers up to int64 to bind params**
+// With UUID
+const results = await conn.select(
+ 'users', 'id', 1, 0, 'eq',
+ [conn.packUuid('550e8400-e29b-41d4-a716-446655440000')]
+);
+```
-Otherwise msgpack will encode anything bigger than int32 as a double number.
+#### tarantool.selectCb(spaceId, indexId, limit, offset, iterator, key, successCb, errorCb, [opts]) β `undefined`
-### tarantool.select(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple) β Promise
+**Deprecated.** Use `.select()` with callback parameter instead.
-[Iterators](http://tarantool.org/doc/book/box/box_index.html). Available iterators: 'eq', 'req', 'all', 'lt', 'le', 'ge', 'gt', 'bitsAllSet', 'bitsAnySet', 'bitsAllNotSet'.
+Legacy callback-style select. Parameters order differs from `.select()`.
-It's just select. Promise resolve array of tuples.
+```javascript
+conn.selectCb(512, 0, 10, 0, 'eq', [50],
+ (results) => console.log(results),
+ (error) => console.error(error)
+);
+```
+
+#### tarantool.insert(spaceId, tuple, [opts], [cb]) β `Promise`
+
+Inserts a tuple into the database.
+
+**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)`
-Some examples:
+**Returns:** Promise resolving to the inserted tuple.
-```Javascript
-conn.select(512, 0, 1, 0, 'eq', [50]);
-//same as
-conn.select('test', 'primary', 1, 0, 'eq', [50]);
+```javascript
+const result = await conn.insert('users', [1, 'Alice', 'alice@example.com']);
```
-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.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.
-For tests, we will create a Space named 'users' on the Tarantool server-side, where the 'id' index is of UUID type:
+See [Tarantool replace docs](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.replace).
-```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}
-})
+```javascript
+const result = await conn.replace('users', [1, 'Alice Updated', 'alice.new@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.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.
+
+**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.selectCb(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple, callback: function(success), callback: function(error))
+#### tarantool.delete(spaceId, indexId, key, [opts], [cb]) β `Promise`
-Same as [tarantool.select](#select) but with callbacks.
+Deletes a tuple from the database.
-### tarantool.delete(spaceId: Number or String, indexId: Number or String, key: tuple) β 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)`
-Promise resolve an array of deleted tuples.
+**Returns:** Promise resolving to the deleted tuple.
-### tarantool.update(spaceId: Number or String, indexId: Number or String, key: tuple, ops) β Promise
+```javascript
+const deleted = await conn.delete('users', 'primary', [1]);
+```
+
+#### tarantool.upsert(spaceId, tuple, ops, [opts], [cb]) β `Promise`
-[Possible operators.](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.update)
+Updates or inserts a tuple (insert if not exists, update if exists).
-Promise resolve an array of updated tuples.
+**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.insert(spaceId: Number or String, tuple) β Promise
+**Returns:** Promise that resolves (typically with no value).
-More you can read here: [Insert](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.insert)
+See [Tarantool upsert docs](http://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.upsert).
-Promise resolve a new tuple.
+```javascript
+await conn.upsert('users', [1, 'Alice', 'alice@example.com'], [['+', 'counter', 1]]);
+```
-### tarantool.upsert(spaceId: Number or String, ops: array of operations, tuple: tuple) β Promise
+---
-About operation: [Upsert](http://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.upsert)
+### π Transaction Methods
-[Possible operators.](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.update)
+Transactions use streams to maintain isolation. Use within `connection.transaction()` context.
-Promise resolve nothing.
+#### tarantool.transaction() β `Transaction`
-### tarantool.replace(spaceId: Number or String, tuple: tuple) β Promise
+Creates a new transaction context for executing commands.
+
+**Returns:** Transaction object that routes commands to same stream.
+
+```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();
+```
-More you can read here: [Replace](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.replace)
+#### transaction.begin([transTimeoutSec], [isolationLevel], [opts], [cb]) β `Promise`
-Promise resolve a new or replaced tuple.
+Starts a transaction.
-### tarantool.call(functionName: String, args...) β Promise
+**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)`
-Call a function with arguments.
+**Returns:** Promise that resolves when transaction begins.
-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.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;
+}
```
-And then use something like this:
-```Javascript
-conn.call('myget', 4)
- .then(function(value){
- console.log(value);
- });
+#### 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]);
```
-If you have a 2 arguments function just send a second arguments in this way:
-```Javascript
-conn.call('my2argumentsfunc', 'first', 'second argument')
+#### 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.
+---
+
+### π§ Utility Methods
+
+#### tarantool.packUuid(uuid: `string`) β `Buffer`
-It doesn't work for space without format.
+Converts UUID string to Tarantool-compatible format.
-### tarantool.pipeline().<...>.exec()
+**Parameters:**
+- `uuid` (`string`) - UUID string (e.g., `'550e8400-e29b-41d4-a716-446655440000'`)
-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)
+**Returns:** Encoded buffer for use in queries.
-Example:
+**Note:** Without conversion, UUIDs are sent as plain strings.
-```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()
+```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).
-### 3.1.0
+**Parameters:**
+- `number` (`number`) - Number to convert
-- 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.
+**Returns:** Encoded buffer for use in queries.
-### 3.0.7
+**Note:** Without conversion, numbers > int32 are encoded as Double.
-Fix in header decoding to support latest Tarantool versions. Update to tests to support latest Tarantool versions.
+```javascript
+const bigInt = conn.packInteger(9223372036854775807); // Max int64
+await conn.insert('bigdata', [1, bigInt]);
+```
-### 3.0.6
+#### tarantool.packInterval(value) β `Buffer`
-Remove let for support old nodejs version
+Converts value to Tarantool Interval type.
-### 3.0.5
+**Parameters:**
+- `value` (`Object` | `number`) - Interval specification
-Add support SQL
+**Returns:** Encoded buffer for use in queries.
-### 3.0.4
+```javascript
+const interval = conn.packInterval({ years: 1, months: 2, days: 3 });
+```
+
+#### tarantool.fetchSchema() β `Promise`
+
+Fetches and caches database schema (spaces and indexes).
+
+**Returns:** Promise resolving to namespace object with space/index metadata.
-Fix eval and call
+**Use case:** Required if using space/index by name instead of ID without `prefetchSchema: true`.
-### 3.0.3
+```javascript
+await conn.fetchSchema();
+const userId = conn.namespace['users'].id;
+```
-Increase request id limit to SMI Maximum
+---
-### 3.0.2
+## π Debugging
-Fix parser thx @tommiv
+Enable debug logging by setting the `DEBUG` environment variable:
+
+```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. Streams
-2. Events and subscriptions
-3. Graceful shutdown protocol
-4. Prepared SQL statements
\ 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 0000000..731a355
Binary files /dev/null and b/assets/read-results-table.png differ
diff --git a/benchmark/box.lua b/benchmark/box.lua
deleted file mode 100755
index 7b614ef..0000000
--- a/benchmark/box.lua
+++ /dev/null
@@ -1,32 +0,0 @@
-box.cfg{listen=3301}
-
-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')
- pr = c:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'STR'}})
- 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'}})
-end
-
-function clear()
- box.session.su('admin')
- box.space.bench:truncate{}
-end
\ No newline at end of file
diff --git a/benchmark/read.js b/benchmark/read.js
index d9d11ec..c348a61 100755
--- a/benchmark/read.js
+++ b/benchmark/read.js
@@ -1,127 +1,202 @@
-/* global Promise */
'use strict';
+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');
-var exec = require('child_process').exec;
-var Benchmark = require('benchmark');
-var suite = new Benchmark.Suite();
-var Driver = require('../lib/connection.js');
-var promises;
+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
+});
-var conn = new Driver(process.argv[process.argv.length - 1], {lazyConnect: true});
+bench.addEventListener('cycle', (evt) => {
+ const result = evt.task.result;
+ if (result.state != 'completed') return;
-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);
- }});
+ 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`);
+});
- suite.add('select promise', {defer: true, fn: function(defer){
- conn.select('counter', 0, 1, 0, 'eq', ['test'])
- .then(function(){ defer.resolve();});
- }});
+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';
- 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);
- }
- }});
+/**
+ * Resets the counter variable
+ */
+const resetCounter = () => {
+ counter = 0;
+};
- 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);
- });
- }
+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
+ }
+ )
- chain.then(function(){ defer.resolve(); })
- .catch(function(e){
- console.error(e, e.stack);
- });
- } catch(e){
- console.error(e, e.stack);
- }
- }});
+ // 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
+ })
- 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);
- });
- }
+ // 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
+ })
- chain.then(function(){ defer.resolve(); })
- .catch(function(e){
- console.error(e, e.stack);
- });
- } catch(e){
- console.error(e, e.stack);
- }
- }});
+ // 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
+ });
- 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']);
- }
+ 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;
+ }
- pipelinedConn.exec()
- .then(function(){ defer.resolve(); })
- .catch(function(e){ defer.reject(e); });
- }});
+ console.info(`Benchmark "${bench.name}" finished, results: `)
+ console.table(bench.table());
- 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 });
- });
+ 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 fc195fb..7a64733 100755
--- a/benchmark/write.js
+++ b/benchmark/write.js
@@ -1,78 +1,207 @@
-/* global Promise */
'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);
- });
- }});
-
- 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);
- }
- }});
+/**
+ * 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);
+};
+
+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('pipelined insert by 10', {defer: true, fn: function(defer){
- var pipelinedConn = conn.pipeline()
+bench.addEventListener('cycle', (evt) => {
+ const result = evt.task.result;
+ if (result.state != 'completed') return;
- for (var i=0;i<10;i++) {
- pipelinedConn.insert('bench', [c++, {user: 'username', data: 'Some data.'}])
- }
+ 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 = `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/commands.js b/lib/commands.js
deleted file mode 100755
index bd1b9fa..0000000
--- a/lib/commands.js
+++ /dev/null
@@ -1,607 +0,0 @@
-/* global Promise */
-var { createHash } = require('crypto');
-var tarantoolConstants = require('./const');
-var {
- bufferFrom,
- createBuffer,
- TarantoolError,
- bufferSubarrayPoly
-} = require('./utils');
-var { encode: msgpackEncode } = require('msgpack-lite');
-var { codec } = require('./msgpack-extensions');
-
-function Commands() {}
-Commands.prototype.sendCommand = function () {};
-
-var maxSmi = 1<<30
-
-Commands.prototype._getRequestId = function(){
- if (this._id > maxSmi)
- this._id =0;
- return this._id++;
-};
-
-Commands.prototype._getSpaceId = function(name){
- var _this = this;
- return this.select(tarantoolConstants.Space.space, tarantoolConstants.IndexSpace.name, 1, 0,
- 'eq', [name])
- .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] = {
- id: spaceId,
- name: name,
- indexes: {}
- };
- return spaceId;
- }
- else
- {
- throw new TarantoolError('Cannot read a space name or space is not defined');
- }
- });
-};
-Commands.prototype._getIndexId = function(spaceId, indexName){
- var _this = this;
- return this.select(tarantoolConstants.Space.index, tarantoolConstants.IndexSpace.indexName, 1, 0,
- 'eq', [spaceId, indexName])
- .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');
- });
-};
-Commands.prototype.select = function(spaceId, indexId, limit, offset, iterator, key, _isPipelined) {
- 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
- );
- });
-};
-
-Commands.prototype._getMetadata = function(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(){
- var _this = this;
- return new Promise(function (resolve, reject) {
- var reqId = _this._getRequestId();
- var len = 9;
- var buffer = 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);
-
- _this.sendCommand([
- tarantoolConstants.RequestCode.rqPing,
- reqId,
- {resolve: resolve, reject: reject}
- ], buffer);
- });
-};
-
-Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator, key, success, error){
- 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])
- 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);
- })
- .catch(error);
- }
-
- 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: success, reject: error}
- ], buffer);
-};
-
-Commands.prototype.delete = function(spaceId, indexId, key){
- var _this = this;
- 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 = 17+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.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([
- tarantoolConstants.RequestCode.rqDelete,
- reqId,
- {resolve: resolve, reject: reject}
- ], buffer);
- }
- else
- reject(new TarantoolError('need array'));
- });
-};
-
-Commands.prototype.update = function(spaceId, indexId, key, ops){
- var _this = this;
- 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 = 18+bufKey.length+bufOps.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.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);
- }
- else
- reject(new TarantoolError('need array'));
- });
-};
-
-Commands.prototype.upsert = function(spaceId, ops, tuple){
- var _this = this;
- 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 = 16+bufTuple.length+bufOps.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.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([
- tarantoolConstants.RequestCode.rqUpsert,
- reqId,
- {resolve: resolve, reject: reject}
- ], buffer);
- }
- else
- reject(new TarantoolError('need ops array'));
- });
-};
-
-
-Commands.prototype.eval = function(expression){
- var _this = this;
- 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 buffer = createBuffer(len+5);
-
- buffer[0] = 0xce;
- buffer.writeUInt32BE(len, 1);
- buffer[5] = 0x82;
- 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([
- tarantoolConstants.RequestCode.rqEval,
- reqId,
- {resolve: resolve, reject: reject}
- ], buffer);
- });
-};
-
-Commands.prototype.call = function(functionName){
- var _this = this;
- 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 buffer = createBuffer(len+5);
-
- buffer[0] = 0xce;
- buffer.writeUInt32BE(len, 1);
- buffer[5] = 0x82;
- 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([
- tarantoolConstants.RequestCode.rqCall,
- reqId,
- {resolve: resolve, reject: reject}
- ], buffer);
- });
-};
-
-Commands.prototype.sql = function(query, bindParams = []){
- var _this = this;
- 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 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[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);
- });
-};
-
-Commands.prototype.insert = function(spaceId, tuple){
- var reqId = this._getRequestId();
- return this._replaceInsert(tarantoolConstants.RequestCode.rqInsert, reqId, spaceId, tuple);
-};
-
-Commands.prototype.replace = function(spaceId, tuple){
- var reqId = this._getRequestId();
- return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, reqId, spaceId, tuple);
-};
-
-Commands.prototype._replaceInsert = function(cmd, reqId, spaceId, tuple){
- var _this = this;
- 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 = 15+bufTuple.length;
- var buffer = createBuffer(len+5);
-
- buffer[0] = 0xce;
- buffer.writeUInt32BE(len, 1);
- buffer[5] = 0x82;
- 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([
- cmd,
- reqId,
- {resolve: resolve, reject: reject}
- ], buffer);
- }
- else
- reject(new TarantoolError('need array'));
- });
-};
-
-Commands.prototype._auth = function(username, password){
- var _this = this;
- return new Promise(function (resolve, reject) {
- var reqId = _this._getRequestId();
-
- var user = msgpackEncode(username);
- var scrambled = scramble(password, _this.salt);
- var len = 44+user.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.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.commandsQueue.push([
- tarantoolConstants.RequestCode.rqAuth,
- reqId,
- {resolve: resolve, reject: reject},
- ]);
- _this.socket.write(buffer);
- });
-};
-
-function shatransform(t){
- return createHash('sha1').update(t).digest();
-}
-
-function xor(a, b) {
- if (!Buffer.isBuffer(a)) a = bufferFrom(a);
- if (!Buffer.isBuffer(b)) b = bufferFrom(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 bufferFrom(res);
-}
-
-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]));
- return xor(step1, step3);
-}
-
-module.exports = Commands;
\ No newline at end of file
diff --git a/lib/connection.js b/lib/connection.js
index dd3b02b..e5dec49 100755
--- a/lib/connection.js
+++ b/lib/connection.js
@@ -1,327 +1,546 @@
-/* global Promise */
-var EventEmitter = require('events').EventEmitter;
-var util = require('util');
-var debug = require('debug')('tarantool-driver:main');
-var _ = require('lodash');
-var {
- parseURL,
- TarantoolError,
- findPipelineError,
- 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 Connector = require('./connector');
-var eventHandler = require('./event-handler');
-var SliderBuffer = require('./sliderBuffer')
-
-var 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'
-};
-TarantoolConnection.defaultOptions = {
- host: 'localhost',
- port: 3301,
- path: null,
- username: null,
- password: null,
- reserveHosts: [],
- beforeReserve: 2,
- timeout: 0,
- noDelay: true,
- keepAlive: true,
- 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:
- - 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
- */
- 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
-};
+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');
-function TarantoolConnection (){
- if (!(this instanceof TarantoolConnection)) {
- return new TarantoolConnection(arguments[0], arguments[1], arguments[2]);
- }
- EventEmitter.call(this);
- this.reserve = [];
- this.parseOptions(arguments[0], arguments[1], arguments[2]);
- this.connector = new Connector(this.options);
- this.schemaId = null;
- 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
+class TarantoolConnection extends Commander {
+ static MsgPack = MsgPack;
+ static PreparedStatement = PreparedStatement;
+ static PipelineResponse = PipelineResponse;
+ static errors = {
+ ReplyError,
+ TarantoolError
};
- this.dataState = this.states.PREHELLO;
- this.commandsQueue = new Denque();
- this.offlineQueue = new Denque();
- this.namespace = {};
- this.bufferSlide = new SliderBuffer()
- this.awaitingResponseLength = -1;
- this.retryAttempts = 0;
- this._id = 0;
- this.findPipelineError = findPipelineError
- this.findPipelineErrors = findPipelineErrors
- if (this.options.lazyConnect) {
- this.setState(this.states.INITED);
- } else {
- this.connect().catch(_.noop);
+ static iterators = Iterators;
+
+ connect = connect;
+ destroy = deprecate(
+ disconnect,
+ '\'.destroy()\' method is deprecated. Use \'.disconnect()\' instead.',
+ 'Tarantool-Driver'
+ );
+ disconnect = disconnect;
+ quit = quit;
+ useNextReserve = useNextReserve;
+ setState = setState;
+ silentEmit = silentEmit;
+ errorHandler = eventHandler.errorHandler;
+ _awaitSentCommandsDrain = _awaitSentCommandsDrain;
+
+ // parser
+ processResponse = parser.processResponse;
+ _returnBool = parser._returnBool;
+
+ // pipeline
+ pipeline = pipeline;
+
+ transaction = createTransaction;
+
+ 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;
+
+ /**
+ * 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
+ );
+
+ 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);
+ }
+ }
+
+ /**
+ * Checks if the connection is in CONNECT state
+ * @returns {boolean} True if connected
+ */
+ isConnectedState() {
+ return this._state[0] & states.CONNECT;
}
-}
-util.inherits(TarantoolConnection, EventEmitter);
-_.assign(TarantoolConnection.prototype, Commands.prototype);
-_.assign(TarantoolConnection.prototype, Pipeline.prototype);
-_.assign(TarantoolConnection.prototype, require('./parser'));
+ /**
+ * Sends a command to the server
+ * @private
+ * @param {number} requestCode - Request code
+ * @param {number} reqId - Request ID
+ * @param {Buffer} buffer - Command buffer
+ * @param {Function} [cb] - Callback function
+ * @param {Array} commandArguments - Command arguments
+ * @returns {Promise}
+ */
+ sendCommand(requestCode, reqId, buffer, cb, commandArguments) {
+ // create promise for async methods
+ let promise;
+ if (!cb) {
+ const p = withResolvers();
+ promise = p.promise;
-for (var packerName of Object.keys(packAs)) {
- TarantoolConnection.prototype['pack' + packerName] = packAs[packerName]
-}
+ cb = (error, success) => {
+ if (error) {return p.reject(error);}
-TarantoolConnection.prototype.resetOfflineQueue = function () {
- this.offlineQueue = new Denque();
-};
+ p.resolve(success);
+ };
+ }
-TarantoolConnection.prototype.parseOptions = function(){
- this.options = {};
- var i;
- for (i = 0; i < arguments.length; ++i) {
- var arg = arguments[i];
- if (arg === null || typeof arg === 'undefined') {
- continue;
+
+ 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;
}
- if (typeof arg === 'object') {
- _.defaults(this.options, arg);
- } else if (typeof arg === 'string') {
- if(!isNaN(arg) && (parseFloat(arg) | 0) === parseFloat(arg)){
- this.options.port = arg;
- continue;
+
+ 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,
+ cb,
+ null, // command arguments
+ null, // timeoud id
+ opts
+ ];
+
+ if (opts.tupleToObject ?? this.options.tupleToObject) {
+ setValue[2] = commandArguments;
+ }
+
+ 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[pipelinedSym] === true
+ ? false
+ : (opts.autoPipeline ??
+ this.options.enableAutoPipelining);
+ // check if should be autopipelined
+ if (shouldAutoPipeline) {
+ 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;
}
- _.defaults(this.options, parseURL(arg));
- } else if (typeof arg === 'number') {
- this.options.port = arg;
- } else {
- throw new TarantoolError('Invalid argument ' + arg);
+ 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;
}
- _.defaults(this.options, TarantoolConnection.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\'')
- }
- if (typeof this.options.port === 'string') {
- this.options.port = parseInt(this.options.port, 10);
+}
+applyMixin(TarantoolConnection, EventEmitter);
+
+/**
+ * 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.options.path != null) {
- delete this.options.port
- delete this.options.host
+ 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'
+ );
}
- if (reserveHostsLength > 0){
- this.reserveIterator = 1;
- this.reserve.push(_.pick(this.options, ['port', 'host', 'username', 'password', 'path']));
- for(i = 0; i {
+ if (src !== undefined) {
+ return src;
+ } else {
+ return target;
}
- }
- this.options.beforeReserve = this.options.beforeReserve < 0 ? 0 : this.options.beforeReserve;
-};
+ });
-TarantoolConnection.prototype.useNextReserve = function(){
- this.retryAttempts = 0;
- if(this.reserveIterator == this.reserve.length) this.reserveIterator = 0;
- delete this.options.port
- delete this.options.host
- delete this.options.path
- delete this.options.port
- delete this.options.username
- delete this.options.password
- 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.')
-
- return reserveOptions
+ return reserveOptions;
};
-TarantoolConnection.prototype.sendCommand = function(command, buffer, isPipelined){
- 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]);
- } 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
- }
- break;
- case this.states.END:
- command[2].reject(new TarantoolError('Connection is closed.'));
- break;
- default:
- debug('queue -> %s(%s)', command[0], command[1]);
- if (!this.options.enableOfflineQueue) {
- return command[2].reject(new TarantoolError('Connection not established yet!'));
- }
- this.offlineQueue.push([command, buffer]);
- }
+/**
+ * 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(revertStates[state], arg);
+ });
};
-TarantoolConnection.prototype.setState = function (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
- } else {
- address = this.options.host + ':' + this.options.port;
- }
+/**
+ * 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')
+ );
}
- debug('state[%s]: %s -> %s', address, revertStates[this.state] || '[empty]', revertStates[state]);
- this.state = state;
- process.nextTick(this.emit.bind(this, revertStates[state], arg));
-};
-TarantoolConnection.prototype.connect = function(){
- 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'));
- return;
- }
- this.setState(this.states.CONNECTING);
- var _this = this;
- this.connector.connect(function(err, socket){
- if(err){
- _this.flushQueue(err);
+ // create new promise if it doesn't exist
+ this.pendingPromises.connect ||= new Promise((resolve, reject) => {
+ this.setState(states.CONNECTING);
+ 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(_this.states.END);
+ _this.setState(states.END);
return;
}
- _this.socket = socket;
- socket.once('connect', eventHandler.connectHandler(_this));
- socket.once('error', eventHandler.errorHandler(_this));
- socket.once('close', eventHandler.closeHandler(_this));
- socket.on('data', eventHandler.dataHandler(_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';
- eventHandler.errorHandler(_this)(error);
- });
- socket.once('connect', function () {
- socket.setTimeout(0);
- });
+
+ socket.on('error', eventHandler.errorHandler.bind(_this));
+ socket.once('close', eventHandler.closeHandler.bind(_this));
+ 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));
-};
+ });
-TarantoolConnection.prototype.flushQueue = function (error) {
- while (this.offlineQueue.length > 0) {
- this.offlineQueue.shift()[0][2].reject(error);
- }
- while (this.commandsQueue.length > 0) {
- this.commandsQueue.shift()[2].reject(error);
- }
+ return this.pendingPromises.connect
+ .then(() => this.connectionErrors = [])
+ // clear created promise
+ .finally(() => {
+ this.pendingPromises.connect = null;
+ });
};
-TarantoolConnection.prototype.silentEmit = function (eventName) {
- var error;
- if (eventName === 'error') {
- error = arguments[1];
+/**
+ * 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 (this.status === 'end') {
- return;
+ 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()
+ );
+ }
}
- if (this.manuallyClosing) {
- if (
- error instanceof Error &&
- (
- error.message === 'Connection manually closed' ||
- error.syscall === 'connect' ||
- error.syscall === 'read'
- )
- ) {
- return;
- }
+ if (listenersLen > 0) {
+ return this.emit.apply(this, arguments);
}
- }
- 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;
+
+ return false;
};
-TarantoolConnection.prototype.destroy = function () {
- this.disconnect();
+
+/**
+ * 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);
};
-TarantoolConnection.prototype.disconnect = function(reconnect){
- if (!reconnect) {
- this.manuallyClosing = true;
- }
- if (this.reconnectTimeout) {
- clearTimeout(this.reconnectTimeout);
- this.reconnectTimeout = null;
- }
- if (this.state === this.states.INITED) {
- eventHandler.closeHandler(this)();
- } else {
- this.connector.disconnect();
- }
+
+/**
+ * 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);
};
-TarantoolConnection.prototype.IteratorsType = tarantoolConstants.IteratorsType;
+/**
+ * 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;
+};
+
+/**
+ * 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;
diff --git a/lib/connector.js b/lib/connector.js
deleted file mode 100755
index 33a8acd..0000000
--- a/lib/connector.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/* global Promise */
-var net = require('net');
-var tls = require('tls');
-var { TarantoolError } = require('./utils');
-
-function Connector(options) {
- this.options = options;
-}
-
-Connector.prototype.disconnect = function () {
- this.connecting = false;
- if (this.socket) {
- this.socket.end();
- }
-};
-
-Connector.prototype.connect = function (callback) {
- this.connecting = true;
-
- var _this = this;
- process.nextTick(function () {
- 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);
- });
-};
-
-module.exports = Connector;
\ No newline at end of file
diff --git a/lib/const.js b/lib/const.js
index d7754d0..9501451 100755
--- a/lib/const.js
+++ b/lib/const.js
@@ -1,105 +1,165 @@
-var {bufferFrom} = require('./utils')
-// 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,
- rqCallNew: 0x0a,
- rqExecute: 0x0b,
- rqDestroy: 0x100, //fake for destroy socket cmd
- rqPing: 0x40
+const MsgPack = require('./MsgPack');
+const StandaloneConnector = require('./StandaloneConnector');
+
+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
};
-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,
- meta: 0x32,
- sql_text: 0x40,
- sql_bind: 0x41,
- sql_info: 0x42,
- iproto_error: 0x52
+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
};
-// 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.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 OkCode = 0;
-var NetErrCode = 0xfffffff1; // fake code to wrap network problems into response
-var TimeoutErrCode = 0xfffffff2; // fake code to wrap timeout error into repsonse
+// 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 PacketLengthBytes = 5;
+module.exports.Space = {
+ schema: 272,
+ space: 281,
+ index: 289,
+ func: 296,
+ user: 304,
+ priv: 312,
+ cluster: 320
+};
-var Space = {
- schema: 272,
- space: 281,
- index: 289,
- func: 296,
- user: 304,
- priv: 312,
- cluster: 320
+module.exports.IndexSpace = {
+ id: 0,
+ name: 2,
+ format: 6
};
-var IndexSpace = {
- primary: 0,
- name: 2,
- indexPrimary: 0,
- indexName: 2
+module.exports.states = {
+ CONNECTING: 0,
+ // CONNECTED: 1,
+ // AWAITING: 2,
+ INITED: 4,
+ // PREHELLO: 8,
+ // AWAITING_LENGTH: 16,
+ END: 32,
+ RECONNECTING: 64,
+ AUTH: 128,
+ CONNECT: 256
};
-var BufferedIterators = {};
-for(t in IteratorsType)
-{
- BufferedIterators[t] = bufferFrom([KeysCode.iterator, IteratorsType[t], KeysCode.key]);
-}
+module.exports.revertStates = Object.fromEntries(
+ Object.entries(module.exports.states).map((arr) => [
+ arr[1],
+ arr[0].toLowerCase()
+ ])
+);
-var BufferedKeys = {};
-for(k in KeysCode)
-{
- BufferedKeys[k] = bufferFrom([KeysCode[k]]);
-}
+module.exports.passEnter = Buffer.from('a9636861702d73686131', 'hex'); /* from msgpack.encode('chap-sha1') */
-var ExportPackage = {
- RequestCode: RequestCode,
- KeysCode: KeysCode,
- IteratorsType: IteratorsType,
- OkCode: OkCode,
- passEnter: bufferFrom('a9636861702d73686131', 'hex') /* from msgpack.encode('chap-sha1') */,
- Space: Space,
- IndexSpace: IndexSpace,
- BufferedIterators: BufferedIterators,
- BufferedKeys: BufferedKeys
+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 = ExportPackage;
\ No newline at end of file
+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 3c99ba1..68ce6e2 100755
--- a/lib/event-handler.js
+++ b/lib/event-handler.js
@@ -1,226 +1,108 @@
-var debug = require('debug')('tarantool-driver:handler');
-var {
- TarantoolError,
- bufferSubarrayPoly
+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');
-var _ = require('lodash');
-exports.connectHandler = function (self) {
- return function () {
- self.retryAttempts = 0;
- switch(self.state){
- case self.states.CONNECTING:
- 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);
- debug('authenticated [%s]', self.options.username);
- sendOfflineQueue(self);
- }, function(err){
- self.flushQueue(err);
- self.silentEmit('error', err);
- self.disconnect(true);
- });
- } else {
- self.setState(self.states.CONNECT, connectionOptions);
- sendOfflineQueue(self);
- }
- break;
- }
- };
-};
+/**
+ * Handles the initial handshake data from server
+ * @private
+ */
+exports.dataHandler_prehello = function (data) {
+ this.salt = data.toString('utf8').split('\n')[1].replaceAll(' ', '');
-function sendOfflineQueue(self){
- if (self.offlineQueue.length) {
- debug('send %d commands in offline queue', self.offlineQueue.length);
- var offlineQueue = self.offlineQueue;
- self.resetOfflineQueue();
- while (offlineQueue.length > 0) {
- var command = offlineQueue.shift();
- self.sendCommand(
- command[0],
- command[1]
- );
- }
- }
-}
+ this.connector.socket.on('data', this.mpDecoderStream.write.bind(this.mpDecoderStream));
-exports.dataHandler = function(self){
- return function(data){
- switch(self.dataState){
- case self.states.PREHELLO:
- self.salt = data[bufferSubarrayPoly](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;
- 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;
- }
- break;
- }
- };
+ 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']));
+ }
};
-exports.errorHandler = function(self){
- return function(error){
- debug('error: %s', error);
- self.silentEmit('error', error);
- };
+/**
+ * Handles errors from socket
+ * @private
+ * @param {Error} error - Error object
+ */
+const errorHandler = function (error) {
+ debug('handled 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;
+
+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');
-exports.closeHandler = function (self) {
- function close () {
- self.setState(self.states.END);
- self.flushQueue(new TarantoolError('Connection is 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(self.emit.bind(self, 'close'));
- if (self.manuallyClosing) {
- self.manuallyClosing = false;
- debug('skip reconnecting since the connection is manually closed.');
- return close();
- }
- if (typeof self.options.retryStrategy !== 'function') {
- debug('skip reconnecting because `retryStrategy` is not a function');
- return close();
- }
- var retryDelay = self.options.retryStrategy(++self.retryAttempts);
+ const close = closeConnection.bind(this);
- 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 && self.options.reserveHosts.length || 0) > 0) {
- if (self.retryAttempts-1 == self.options.beforeReserve){
- self.useNextReserve();
- self.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)'
+ );
- self.reconnectTimeout = setTimeout(function () {
- self.reconnectTimeout = null;
- self.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/msgpack-extensions.js b/lib/msgpack-extensions.js
deleted file mode 100644
index 83cb63e..0000000
--- a/lib/msgpack-extensions.js
+++ /dev/null
@@ -1,128 +0,0 @@
-var { createCodec: msgpackCreateCodec} = require('msgpack-lite');
-var {
- TarantoolError,
- createBuffer,
- bufferFrom
-} = require('./utils');
-var {
- parse: uuidParse,
- stringify: uuidStringify
-} = require('uuid');
-var {
- Uint64BE,
- Int64BE
-} = 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) {
- 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
-}
-
-codec.addExtPacker(0x02, packAs.Uuid, (data) => {
- return uuidParse(data.value);
-});
-
-codec.addExtUnpacker(0x02, (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;
-}
-
-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')
- + rawNum.replace('.', '')
- + (strNum.startsWith('-') ? 'b' : 'a')
-
- if (isOdd(bufHexed.length)) {
- bufHexed = bufHexed.slice(0, 2) + '0' + bufHexed.slice(2)
- }
-
- return bufferFrom(bufHexed, 'hex')
-});
-
-codec.addExtUnpacker(0x01, (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)
-
- if (scale > 0) {
- var nScale = scale * -1
- slicedValue = slicedValue.slice(0, nScale) + '.' + slicedValue.slice(nScale)
- }
-
- return parseFloat(sign + slicedValue)
-});
-
-// Datetime extension
-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)
- /*
- Node.Js 'Date' doesn't provide nanoseconds, so just using milliseconds.
- tzoffset is set to UTC, and tzindex is omitted.
- */
-
- return buffer;
-});
-
-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)
- }
-
- return time
-})
-
-exports.packAs = packAs
-exports.codec = codec
\ No newline at end of file
diff --git a/lib/parser.js b/lib/parser.js
index 0e1fb44..fd9589a 100644
--- a/lib/parser.js
+++ b/lib/parser.js
@@ -1,107 +1,135 @@
-var { Decoder: msgpackDecoder } = require('msgpack-lite');
-var tarantoolConstants = require('./const');
-var { TarantoolError } = require('./utils');
-var { codec } = require('./msgpack-extensions');
+const { KeysCode, RequestCode } = require('./const');
+const { TarantoolError } = require('./errors');
+const { ReplyError } = require('./errors');
+const debug = require('./utils/debug').extend('parser');
+const PreparedStatement = require('./PreparedStatement');
-var decoder = new msgpackDecoder({codec});
+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);
-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)
- {
- this.schemaId = schemaId;
- this.namespace = {};
+ this.schemaId ||= schemaId;
+ if (this.schemaId != schemaId) {
+ this.schemaId = schemaId;
+ this.offlineQueue.set();
+ this.fetchSchema().then(() => {
+ this.offlineQueue.unset();
+ this.offlineQueue.send();
+ });
}
- }
- else
- {
- this.schemaId = schemaId;
- }
- var task;
- for(var i = 0; i {
+ obj[keys[index]] = value;
+ });
+
+ return obj;
+}
diff --git a/lib/pipeline.js b/lib/pipeline.js
deleted file mode 100644
index 104136f..0000000
--- a/lib/pipeline.js
+++ /dev/null
@@ -1,101 +0,0 @@
-var {
- prototype: commandsPrototype
-} = require('./commands');
-var {RequestCode} = require('./const');
-
-var commandsPrototypeKeys = Object.keys(commandsPrototype);
-
-function commandInterceptorFactory (method) {
- return function commandInterceptor () {
- this.pipelinedCommands.push([method, arguments]);
- return this
- }
-}
-
-var setOfCommandInterceptors = {};
-for (var commandKey of commandsPrototypeKeys) {
- setOfCommandInterceptors[commandKey] = commandInterceptorFactory(commandKey)
-}
-
-function sendCommandInterceptorFactory (self) {
- return function sendCommandInterceptor (command, buffer, isPipelined) {
- // 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;
- }
-
- self._buffersConcatenedCounter++
- if (self.pipelinedBuffer) {
- self.pipelinedBuffer = Buffer.concat([self.pipelinedBuffer, buffer])
- } else {
- self.pipelinedBuffer = buffer
- }
-
- self._trySendConcatenedBuffer()
- self._parent.sendCommand(command, buffer, true)
- }
-}
-
-function _trySendConcatenedBuffer () {
- if (this._buffersConcatenedCounter == this.pipelinedCommands.length) {
- this._parent.socket.write(this.pipelinedBuffer)
- this.flushPipelined()
- }
-}
-
-function exec () {
- var _this = this;
- var promises = [];
- 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
- }
- promises.push(
- new Promise(function (resolve) {
- _this._originalMethods[interceptedCommand[0]].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 Promise.all(promises)
-}
-
-function flushPipelined () {
- this.pipelinedCommands = [];
-}
-
-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)
- }
- )
- this.exec = exec
- this._trySendConcatenedBuffer = _trySendConcatenedBuffer
- this.flushPipelined = flushPipelined
- Object.assign(this, setOfCommandInterceptors);
-}
-
-Pipeline.prototype.pipeline = function () {
- return new Pipeline(this);
-}
-
-module.exports = Pipeline;
\ No newline at end of file
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/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/utils.js b/lib/utils.js
deleted file mode 100644
index 571df4d..0000000
--- a/lib/utils.js
+++ /dev/null
@@ -1,92 +0,0 @@
-// 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;
-}
-exports.createBuffer = function (size){
- return createBufferMethod(size);
-};
-
-// 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'
-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.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){
- Error.call(this);
- this.message = msg;
- this.name = 'TarantoolError';
- if (Error.captureStackTrace) {
- Error.captureStackTrace(this);
- } else {
- this.stack = new Error().stack;
- }
-};
-exports.TarantoolError.prototype = Object.create(Error.prototype);
-exports.TarantoolError.prototype.constructor = Error;
\ 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..8a13851
--- /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 ? 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;
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/package.json b/package.json
index 0b8779e..e6efd83 100755
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
"name": "tarantool-driver",
- "version": "3.1.0",
+ "version": "4.0.0",
"description": "Tarantool driver for 1.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,32 +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",
- "denque": "^1.2.1",
- "int64-buffer": "^1.0.1",
- "lodash": "^4.17.4",
- "msgpack-lite": "^0.1.20",
- "uuid": "^9.0.0"
+ "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",
- "ioredis": "^3.1.2",
- "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.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/app.js b/test/app.js
index 55f2dc5..891bbd6 100755
--- a/test/app.js
+++ b/test/app.js
@@ -1,750 +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 spy = sinon.spy.bind(sinon);
-var stub = sinon.stub.bind(sinon);
-
-var fs = require('fs');
-var assert = require('assert');
-var TarantoolConnection = require('../lib/connection');
-var SliderBuffer = require('../lib/sliderBuffer');
-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(6380);
- expect(option).to.have.property('port', 6380);
- 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('6380');
- expect(option).to.have.property('port', 6380);
+// Global error handlers for better debugging
+process.on('unhandledRejection', (error, promise) => {
+ console.error('β Unhandled Rejection:', error);
+});
- option = getOption(6381, '192.168.1.1');
- expect(option).to.have.property('port', 6381);
- expect(option).to.have.property('host', '192.168.1.1');
+process.on('uncaughtException', (error) => {
+ console.error('β Uncaught Exception:', error);
+ process.exit(1);
+});
- option = getOption(6381, '192.168.1.1', {
- password: '123',
- username: 'userloser'
- });
- expect(option).to.have.property('port', 6381);
- expect(option).to.have.property('host', '192.168.1.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:3301');
- 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('/var/run/tarantool/unix.sock');
- expect(option).to.have.property('path', '/var/run/tarantool/unix.sock');
-
- option = getOption({
- port: 6380,
- host: '192.168.1.1'
- });
- expect(option).to.have.property('port', 6380);
- expect(option).to.have.property('host', '192.168.1.1');
+// mainly for 'constructor' tests
+const optLazyConnect = {
+ lazyConnect: true
+};
+// load fast and save on network bandwith
+const optDontPrefetchSchema = {
+ prefetchSchema: false
+};
- option = getOption({
- port: 6380,
- host: '192.168.1.1',
- reserveHosts: ['notguest:sesame@mail.ru:3301', 'mail.ru:3301']
- });
- expect(option).to.have.property('port', 6380);
- expect(option).to.have.property('host', '192.168.1.1');
- expect(option).to.have.property('reserveHosts');
- expect(option.reserveHosts).to.deep.equal(['notguest:sesame@mail.ru:3301', 'mail.ru:3301']);
-
- option = new TarantoolConnection({
- port: 6380,
- host: '192.168.1.1',
- reserveHosts: ['notguest:sesame@mail.ru:3301', 'mail.ru:3301']
- });
- expect(option.reserve).to.deep.include(
- {
- port: 6380,
- host: '192.168.1.1',
- path: null,
- username: null,
- password: null
- },
- {
- port: 3301,
- host: 'mail.ru'
- },
- {
- port: 3301,
- host: 'mail.ru',
- username: 'notguest',
- password: 'sesame'
- }
- );
-
- option = getOption({
- port: 6380,
- host: '192.168.1.1'
- });
- expect(option).to.have.property('port', 6380);
- expect(option).to.have.property('host', '192.168.1.1');
+const truncateSpace = async (conn, spaceName) => {
+ return conn.sql(`DELETE FROM "${spaceName}" INDEXED BY "tree_idx" WHERE true`);
+};
- option = getOption({
- port: '6380'
- });
- expect(option).to.have.property('port', 6380);
+describe('constructor', () => {
+ test('should parse options correctly', () => {
+ try {
+ let option;
- option = getOption(6380, {
- host: '192.168.1.1'
- });
- expect(option).to.have.property('port', 6380);
- expect(option).to.have.property('host', '192.168.1.1');
+ option = getOption(33013);
+ assert.strictEqual(option.port, 33013);
+ assert.strictEqual(option.host, 'localhost');
- option = getOption('6380', {
- host: '192.168.1.1'
- });
- expect(option).to.have.property('port', 6380);
- } 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){
- console.log('before call');
- 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/box.lua b/test/box.lua
deleted file mode 100755
index a3fe7aa..0000000
--- a/test/box.lua
+++ /dev/null
@@ -1,119 +0,0 @@
-#!/usr/bin/env tarantool
-box.cfg{listen=33013}
-
-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
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;
+ }
+ });
+});