diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 1d119b1..44d823f 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -45,11 +45,36 @@ To do so, you need to pass the **app_id and app_key**. .. code-block:: js - let conn = new driver.Connection('https://test.bigchaindb.com/api/v1/', { + const conn = new driver.Connection('https://test.bigchaindb.com/api/v1/', { app_id: 'Get one from testnet.bigchaindb.com', app_key: 'Get one from testnet.bigchaindb.com' }) +A more complex connection can be created if the intention is to connect to +different nodes of a BigchainDB network. +The connection strategy will be the one specified in the BEP-14_ + +.. _BEP-14: https://github.com/bigchaindb/BEPs/tree/master/14#connection-strategy + +.. code-block:: js + + const conn = new driver.Connection([ + 'https://test.bigchaindb.com', // the first node does not use custom headers, only common headers + {endpoint: 'https://test.bigchaindb.com/api/v1/', + headers: {app_id: 'your_app_id', + app_key: 'your_app_key'}}, + {endpoint: 'https://test2.bigchaindb.com/api/v1/', + headers: {app_id: 'your_app_id', + app_key: 'your_app_key', + extra_header: 'extra value'}}, + {endpoint: 'https://test3.bigchaindb.com/api/v1/', + headers: {app_id: 'your_app_id', + app_key: 'your_app_key', + other_header: 'other value'}}, + {endpoint: 'https://test4.bigchaindb.com/api/v1/', + headers: {custom_auth: 'custom token'}], + {'Content-Type': 'application/json'}, // this header is used by all nodes) + Cryptographic Identities Generation ----------------------------------- Alice and Bob are represented by public/private key pairs. The private key is diff --git a/src/baseRequest.js b/src/baseRequest.js index 3b97b05..147a2a9 100644 --- a/src/baseRequest.js +++ b/src/baseRequest.js @@ -2,9 +2,13 @@ // SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) // Code is Apache-2.0 and docs are CC-BY-4.0 -import { Promise } from 'es6-promise' +import { + Promise +} from 'es6-promise' import fetchPonyfill from 'fetch-ponyfill' -import { vsprintf } from 'sprintf-js' +import { + vsprintf +} from 'sprintf-js' import formatText from './format_text' import stringifyAsQueryParam from './stringify_as_query_param' @@ -12,6 +16,46 @@ import stringifyAsQueryParam from './stringify_as_query_param' const fetch = fetchPonyfill(Promise) +/** + * @private + * Timeout function following https://github.com/github/fetch/issues/175#issuecomment-284787564 + * @param {integer} obj Source object + * @param {Promise} filter Array of key names to select or function to invoke per iteration + * @return {Object} TimeoutError if the time was consumed, otherwise the Promise will be resolved + */ +function timeout(ms, promise) { + return new Promise((resolve, reject) => { + setTimeout(() => { + const errorObject = { + message: 'TimeoutError' + } + reject(new Error(errorObject)) + }, ms) + promise.then(resolve, reject) + }) +} + +/** + * @private + * @param {Promise} res Source object + * @return {Promise} Promise that will resolve with the response if its status was 2xx; + * otherwise rejects with the response + */ +function handleResponse(res) { + // If status is not a 2xx (based on Response.ok), assume it's an error + // See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch + if (!(res && res.ok)) { + const errorObject = { + message: 'HTTP Error: Requested page not reachable', + status: `${res.status} ${res.statusText}`, + requestURI: res.url + } + throw errorObject + } + return res +} + + /** * @private * imported from https://github.com/bigchaindb/js-utility-belt/ @@ -36,13 +80,17 @@ const fetch = fetchPonyfill(Promise) * decamelized into snake case first. * @param {*[]|Object} config.urlTemplateSpec Format spec to use to expand the url (see sprintf). * @param {*} config.* All other options are passed through to fetch. + * @param {integer} requestTimeout Timeout for a single request * - * @return {Promise} Promise that will resolve with the response if its status was 2xx; - * otherwise rejects with the response + * @return {Promise} If requestTimeout the timeout function will be called. Otherwise resolve the + * Promise with the handleResponse function */ export default function baseRequest(url, { - jsonBody, query, urlTemplateSpec, ...fetchConfig -} = {}) { + jsonBody, + query, + urlTemplateSpec, + ...fetchConfig +} = {}, requestTimeout) { let expandedUrl = url if (urlTemplateSpec != null) { @@ -73,19 +121,11 @@ export default function baseRequest(url, { if (jsonBody != null) { fetchConfig.body = JSON.stringify(jsonBody) } - - return fetch.fetch(expandedUrl, fetchConfig) - .then((res) => { - // If status is not a 2xx (based on Response.ok), assume it's an error - // See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch - if (!(res && res.ok)) { - const errorObject = { - message: 'HTTP Error: Requested page not reachable', - status: `${res.status} ${res.statusText}`, - requestURI: res.url - } - throw errorObject - } - return res - }) + if (requestTimeout) { + return timeout(requestTimeout, fetch.fetch(expandedUrl, fetchConfig)) + .then(handleResponse) + } else { + return fetch.fetch(expandedUrl, fetchConfig) + .then(handleResponse) + } } diff --git a/src/connection.js b/src/connection.js index 4b2641b..90f09b5 100644 --- a/src/connection.js +++ b/src/connection.js @@ -2,27 +2,60 @@ // SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) // Code is Apache-2.0 and docs are CC-BY-4.0 -import request from './request' +import Transport from './transport' const HEADER_BLACKLIST = ['content-type'] +const DEFAULT_NODE = 'http://localhost:9984/api/v1/' +const DEFAULT_TIMEOUT = 20000 // The default value is 20 seconds /** - * Base connection + * + * @param {String, Array} nodes Nodes for the connection. String possible to be backwards compatible + * with version before 4.1.0 version + * @param {Object} headers Common headers for every request + * @param {float} timeout Optional timeout in secs + * + * */ + export default class Connection { - constructor(path, headers = {}) { - this.path = path + // This driver implements the BEP-14 https://github.com/bigchaindb/BEPs/tree/master/14 + constructor(nodes, headers = {}, timeout = DEFAULT_TIMEOUT) { + // Copy object this.headers = Object.assign({}, headers) + // Validate headers Object.keys(headers).forEach(header => { if (HEADER_BLACKLIST.includes(header.toLowerCase())) { throw new Error(`Header ${header} is reserved and cannot be set.`) } }) + + this.normalizedNodes = [] + if (!nodes) { + this.normalizedNodes.push(Connection.normalizeNode(DEFAULT_NODE, this.headers)) + } else if (Array.isArray(nodes)) { + nodes.forEach(node => { + this.normalizedNodes.push(Connection.normalizeNode(node, this.headers)) + }) + } else { + this.normalizedNodes.push(Connection.normalizeNode(nodes, this.headers)) + } + + this.transport = new Transport(this.normalizedNodes, timeout) } - getApiUrls(endpoint) { - return this.path + { + static normalizeNode(node, headers) { + if (typeof node === 'string') { + return { 'endpoint': node, 'headers': headers } + } else { + const allHeaders = Object.assign({}, headers, node.headers) + return { 'endpoint': node.endpoint, 'headers': allHeaders } + } + } + + static getApiUrls(endpoint) { + return { 'blocks': 'blocks', 'blocksDetail': 'blocks/%(blockHeight)s', 'outputs': 'outputs', @@ -38,16 +71,14 @@ export default class Connection { } _req(path, options = {}) { - // NOTE: `options.headers` could be undefined, but that's OK. - options.headers = Object.assign({}, options.headers, this.headers) - return request(path, options) + return this.transport.forwardRequest(path, options) } /** * @param blockHeight */ getBlock(blockHeight) { - return this._req(this.getApiUrls('blocksDetail'), { + return this._req(Connection.getApiUrls('blocksDetail'), { urlTemplateSpec: { blockHeight } @@ -58,7 +89,7 @@ export default class Connection { * @param transactionId */ getTransaction(transactionId) { - return this._req(this.getApiUrls('transactionsDetail'), { + return this._req(Connection.getApiUrls('transactionsDetail'), { urlTemplateSpec: { transactionId } @@ -70,7 +101,7 @@ export default class Connection { * @param status */ listBlocks(transactionId) { - return this._req(this.getApiUrls('blocks'), { + return this._req(Connection.getApiUrls('blocks'), { query: { transaction_id: transactionId, } @@ -90,7 +121,7 @@ export default class Connection { if (spent !== undefined) { query.spent = spent.toString() } - return this._req(this.getApiUrls('outputs'), { + return this._req(Connection.getApiUrls('outputs'), { query }) } @@ -100,7 +131,7 @@ export default class Connection { * @param operation */ listTransactions(assetId, operation) { - return this._req(this.getApiUrls('transactions'), { + return this._req(Connection.getApiUrls('transactions'), { query: { asset_id: assetId, operation @@ -112,7 +143,7 @@ export default class Connection { * @param blockId */ listVotes(blockId) { - return this._req(this.getApiUrls('votes'), { + return this._req(Connection.getApiUrls('votes'), { query: { block_id: blockId } @@ -130,7 +161,7 @@ export default class Connection { * @param transaction */ postTransactionSync(transaction) { - return this._req(this.getApiUrls('transactionsSync'), { + return this._req(Connection.getApiUrls('transactionsSync'), { method: 'POST', jsonBody: transaction }) @@ -141,7 +172,7 @@ export default class Connection { * @param transaction */ postTransactionAsync(transaction) { - return this._req(this.getApiUrls('transactionsAsync'), { + return this._req(Connection.getApiUrls('transactionsAsync'), { method: 'POST', jsonBody: transaction }) @@ -152,7 +183,7 @@ export default class Connection { * @param transaction */ postTransactionCommit(transaction) { - return this._req(this.getApiUrls('transactionsCommit'), { + return this._req(Connection.getApiUrls('transactionsCommit'), { method: 'POST', jsonBody: transaction }) @@ -162,7 +193,7 @@ export default class Connection { * @param search */ searchAssets(search) { - return this._req(this.getApiUrls('assets'), { + return this._req(Connection.getApiUrls('assets'), { query: { search } @@ -173,7 +204,7 @@ export default class Connection { * @param search */ searchMetadata(search) { - return this._req(this.getApiUrls('metadata'), { + return this._req(Connection.getApiUrls('metadata'), { query: { search } diff --git a/src/request.js b/src/request.js index 66ebd58..967a41f 100644 --- a/src/request.js +++ b/src/request.js @@ -11,32 +11,106 @@ const DEFAULT_REQUEST_CONFIG = { } } +const BACKOFF_DELAY = 500 // 0.5 seconds +const ERROR_FROM_SERVER = 'HTTP Error: Requested page not reachable' /** * @private * Small wrapper around js-utility-belt's request that provides url resolving, * default settings, and response handling. */ -export default function request(url, config = {}) { - // Load default fetch configuration and remove any falsy query parameters - const requestConfig = Object.assign({}, DEFAULT_REQUEST_CONFIG, config, { - query: config.query && sanitize(config.query) - }) - const apiUrl = url - if (requestConfig.jsonBody) { - requestConfig.headers = Object.assign({}, requestConfig.headers, { - 'Content-Type': 'application/json' - }) + +export default class Request { + constructor(node) { + this.node = node + this.backoffTime = null + this.retries = 0 + this.connectionError = null } - if (!url) { - return Promise.reject(new Error('Request was not given a url.')) + async request(urlPath, config, timeout, maxBackoffTime) { + if (!urlPath) { + return Promise.reject(new Error('Request was not given a url.')) + } + // Load default fetch configuration and remove any falsy query parameters + const requestConfig = Object.assign({}, this.node.headers, DEFAULT_REQUEST_CONFIG, config, { + query: config.query && sanitize(config.query) + }) + const apiUrl = this.node.endpoint + urlPath + if (requestConfig.jsonBody) { + requestConfig.headers = Object.assign({}, requestConfig.headers, { + 'Content-Type': 'application/json' + }) + } + + // If connectionError occurs, a timestamp equal to now + + // `backoffTimedelta` is assigned to the object. + // Next time the function is called, it either + // waits till the timestamp is passed or raises `TimeoutError`. + // If `ConnectionError` occurs two or more times in a row, + // the retry count is incremented and the new timestamp is calculated + // as now + the `backoffTimedelta` + // The `backoffTimedelta` is the minimum between the default delay + // multiplied by two to the power of the + // number of retries or timeout/2 or 10. See Transport class for that + // If a request is successful, the backoff timestamp is removed, + // the retry count is back to zero. + + const backoffTimedelta = this.getBackoffTimedelta() + + if (timeout != null && timeout < backoffTimedelta) { + const errorObject = { + message: 'TimeoutError' + } + throw errorObject + } + if (backoffTimedelta > 0) { + await Request.sleep(backoffTimedelta) + } + + const requestTimeout = timeout ? timeout - backoffTimedelta : timeout + return baseRequest(apiUrl, requestConfig, requestTimeout) + .then(async (res) => { + this.connectionError = null + return res.json() + }) + .catch(err => { + // ConnectionError + this.connectionError = err + }) + .finally(() => { + this.updateBackoffTime(maxBackoffTime) + }) } - return baseRequest(apiUrl, requestConfig) - .then(res => res.json()) - .catch(err => { - console.error(err) - throw err - }) + updateBackoffTime(maxBackoffTime) { + if (!this.connectionError) { + this.retries = 0 + this.backoffTime = null + } else if (this.connectionError.message === ERROR_FROM_SERVER) { + // If status is not a 2xx (based on Response.ok), throw error + this.retries = 0 + this.backoffTime = null + throw this.connectionError + } else { + // Timeout or no connection could be stablished + const backoffTimedelta = Math.min(BACKOFF_DELAY * (2 ** this.retries), maxBackoffTime) + this.backoffTime = Date.now() + backoffTimedelta + this.retries += 1 + if (this.connectionError.message === 'TimeoutError') { + throw this.connectionError + } + } + } + + getBackoffTimedelta() { + if (!this.backoffTime) { + return 0 + } + return (this.backoffTime - Date.now()) + } + + static sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) + } } diff --git a/src/transport.js b/src/transport.js new file mode 100644 index 0000000..2136595 --- /dev/null +++ b/src/transport.js @@ -0,0 +1,73 @@ +// Copyright BigchainDB GmbH and BigchainDB contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import Request from './request' + + +/** + * + * @private + * If initialized with ``>1`` nodes, the driver will send successive + * requests to different nodes in a round-robin fashion (this will be + * customizable in the future). + */ + + +export default class Transport { + constructor(nodes, timeout) { + this.connectionPool = [] + this.timeout = timeout + // the maximum backoff time is 10 seconds + this.maxBackoffTime = timeout ? timeout / 2 : 10000 + nodes.forEach(node => { + this.connectionPool.push(new Request(node)) + }) + } + + // Select the connection with the earliest backoff time, in case of a tie, + // prefer the one with the smaller list index + pickConnection() { + let connection = this.connectionPool[0] + + this.connectionPool.forEach(conn => { + // 0 the lowest value is the time for Thu Jan 01 1970 01:00:00 GMT+0100 (CET) + conn.backoffTime = conn.backoffTime ? conn.backoffTime : 0 + connection = (conn.backoffTime < connection.backoffTime) ? conn : connection + }) + return connection + } + + async forwardRequest(path, headers) { + let response + let connection + // A new request will be executed until there is a valid response or timeout < 0 + while (this.timeout >= 0) { + connection = this.pickConnection() + // Date in milliseconds + const startTime = Date.now() + try { + // eslint-disable-next-line no-await-in-loop + response = await connection.request( + path, + headers, + this.timeout, + this.maxBackoffTime + ) + const elapsed = Date.now() - startTime + if (connection.backoffTime > 0 && this.timeout > 0) { + this.timeout -= elapsed + } else { + // No connection error, the response is valid + return response + } + } catch (err) { + throw err + } + } + const errorObject = { + message: 'TimeoutError', + } + throw errorObject + } +} diff --git a/test/base-request/test_baseRequest.js b/test/base-request/test_baseRequest.js index 9f89b5b..ffc11cb 100644 --- a/test/base-request/test_baseRequest.js +++ b/test/base-request/test_baseRequest.js @@ -3,7 +3,21 @@ // Code is Apache-2.0 and docs are CC-BY-4.0 import test from 'ava' -import baseRequest from '../../src/baseRequest' +import rewire from 'rewire' + +const baseRequestFile = rewire('../../src/baseRequest.js') +const baseRequest = baseRequestFile.__get__('baseRequest') +const handleResponse = baseRequestFile.__get__('handleResponse') + +test('HandleResponse does not throw error for response ok', t => { + const testObj = { + ok: true + } + const expected = testObj + const actual = handleResponse(testObj) + + t.deepEqual(actual, expected) +}) test('baseRequest test query and vsprint', async t => { const target = { diff --git a/test/connection/test_connection.js b/test/connection/test_connection.js index 7f9c65b..9539f60 100644 --- a/test/connection/test_connection.js +++ b/test/connection/test_connection.js @@ -5,9 +5,12 @@ import test from 'ava' import sinon from 'sinon' -import * as request from '../../src/request' // eslint-disable-line -import { Connection } from '../../src' -import { API_PATH } from '../constants' +import { + Connection +} from '../../src' +import { + API_PATH +} from '../constants' const conn = new Connection(API_PATH) @@ -36,28 +39,65 @@ test('Generate API URLS', t => { 'assets': 'assets', } Object.keys(endpoints).forEach(endpointName => { - const url = conn.getApiUrls(endpointName) - const expected = API_PATH + endpoints[endpointName] + const url = Connection.getApiUrls(endpointName) + const expected = endpoints[endpointName] t.is(url, expected) }) }) - -test('Request with custom headers', t => { - const testConn = new Connection(API_PATH, { hello: 'world' }) - const expectedOptions = { +test('Normalize node from an object', t => { + const headers = { + custom: 'headers' + } + const node = { + endpoint: API_PATH, headers: { + hello: 'world' + } + } + const expectedNode = { + 'endpoint': API_PATH, + 'headers': { hello: 'world', custom: 'headers' } } - // request is read only, cannot be mocked? - sinon.spy(request, 'default') - testConn._req(API_PATH, { headers: { custom: 'headers' } }) + t.deepEqual(Connection.normalizeNode(node, headers), expectedNode) +}) - t.truthy(request.default.calledWith(API_PATH, expectedOptions)) - request.default.restore() +test('Normalize node from a string', t => { + const headers = { + custom: 'headers' + } + const expectedNode = { + 'endpoint': API_PATH, + 'headers': { + custom: 'headers' + } + } + + t.deepEqual(Connection.normalizeNode(API_PATH, headers), expectedNode) +}) + +test('Request with custom headers', t => { + const testConn = new Connection(API_PATH, { + hello: 'world' + }) + const expectedOptions = { + headers: { + custom: 'headers' + } + } + const PATH = 'blocks' + testConn.transport.forwardRequest = sinon.spy() + + testConn._req(PATH, { + headers: { + custom: 'headers' + } + }) + t.truthy(testConn.transport.forwardRequest.calledWith(PATH, expectedOptions)) }) @@ -66,7 +106,7 @@ test('Get block for a block id', t => { const blockHeight = 'abc' conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.getBlock(blockHeight) t.truthy(conn._req.calledWith( @@ -81,7 +121,7 @@ test('Get transaction for a transaction id', t => { const transactionId = 'abc' conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.getTransaction(transactionId) t.truthy(conn._req.calledWith( @@ -96,7 +136,7 @@ test('Get list of blocks for a transaction id', t => { const transactionId = 'abc' conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.listBlocks(transactionId) t.truthy(conn._req.calledWith( @@ -116,7 +156,7 @@ test('Get list of transactions for an asset id', t => { const operation = 'operation' conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.listTransactions(assetId, operation) t.truthy(conn._req.calledWith( @@ -136,7 +176,7 @@ test('Get outputs for a public key and no spent flag', t => { const publicKey = 'publicKey' conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.listOutputs(publicKey) t.truthy(conn._req.calledWith( @@ -152,7 +192,7 @@ test('Get outputs for a public key and spent=false', t => { const spent = false conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.listOutputs(publicKey, spent) t.truthy(conn._req.calledWith( @@ -168,7 +208,7 @@ test('Get outputs for a public key and spent=true', t => { const spent = true conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.listOutputs(publicKey, spent) t.truthy(conn._req.calledWith( @@ -183,7 +223,7 @@ test('Get votes for a block id', t => { const blockId = 'abc' conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.listVotes(blockId) t.truthy(conn._req.calledWith( @@ -198,7 +238,7 @@ test('Get asset for text', t => { const search = 'abc' conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.searchAssets(search) t.truthy(conn._req.calledWith( @@ -213,7 +253,7 @@ test('Get metadata for text', t => { const search = 'abc' conn._req = sinon.spy() - conn.getApiUrls = sinon.stub().returns(expectedPath) + Connection.getApiUrls = sinon.stub().returns(expectedPath) conn.searchMetadata(search) t.truthy(conn._req.calledWith( diff --git a/test/integration/test_integration.js b/test/integration/test_integration.js index 5218935..b19c4a9 100644 --- a/test/integration/test_integration.js +++ b/test/integration/test_integration.js @@ -28,8 +28,8 @@ test('Keypair is created', t => { // TODO: The following tests are a bit messy currently, please do: // // - tidy up dependency on `pollStatusAndFetchTransaction` -test('Valid CREATE transaction', t => { - const conn = new Connection(API_PATH) +test('Valid CREATE transaction with default node', t => { + const conn = new Connection() const tx = Transaction.makeCreateTransaction( asset(), @@ -40,7 +40,9 @@ test('Valid CREATE transaction', t => { const txSigned = Transaction.signTransaction(tx, alice.privateKey) return conn.postTransaction(txSigned) - .then(resTx => t.truthy(resTx)) + .then(resTx => { + t.truthy(resTx) + }) }) diff --git a/test/request/test_request.js b/test/request/test_request.js new file mode 100644 index 0000000..99e3ce6 --- /dev/null +++ b/test/request/test_request.js @@ -0,0 +1,31 @@ +// Copyright BigchainDB GmbH and BigchainDB contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import test from 'ava' +import Connection from '../../src/connection' + + +const conn = new Connection() + +test('Ensure that BackoffTimedelta works properly', t => { + const req = conn.transport.pickConnection() + req.backoffTime = Date.now() + 50 + const target = req.getBackoffTimedelta() + // The value should be close to 50 + t.is(target > 45, true) +}) + +test('Ensure that updateBackoffTime throws and error on TimeoutError', async t => { + const req = conn.transport.pickConnection() + const target = { + message: 'TimeoutError' + } + req.connectionError = target + + const error = t.throws(() => { + req.updateBackoffTime() + }) + + t.deepEqual(target, error) +}) diff --git a/test/transport/test_transport.js b/test/transport/test_transport.js new file mode 100644 index 0000000..eff2741 --- /dev/null +++ b/test/transport/test_transport.js @@ -0,0 +1,23 @@ +// Copyright BigchainDB GmbH and BigchainDB contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import test from 'ava' + +import { + Connection +} from '../../src' + +test('Pick connection with earliest backoff time', async t => { + const path1 = 'http://localhost:9984/api/v1/' + const path2 = 'http://localhostwrong:9984/api/v1/' + + // Reverse order + const conn = new Connection([path2, path1]) + // This will trigger the 'forwardRequest' so the correct connection will be taken + await conn.searchAssets('example') + + const connection1 = conn.transport.connectionPool[1] + + t.deepEqual(conn.transport.pickConnection(), connection1) +})