mirror of
https://github.com/bigchaindb/js-bigchaindb-driver.git
synced 2024-11-22 09:46:58 +01:00
Merge branch 'master' into remove-votes-api
This commit is contained in:
commit
355dd62a60
@ -45,11 +45,36 @@ To do so, you need to pass the **app_id and app_key**.
|
|||||||
|
|
||||||
.. code-block:: js
|
.. 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_id: 'Get one from testnet.bigchaindb.com',
|
||||||
app_key: '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
|
Cryptographic Identities Generation
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
Alice and Bob are represented by public/private key pairs. The private key is
|
Alice and Bob are represented by public/private key pairs. The private key is
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "^0.25.0",
|
"ava": "^0.25.0",
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
"babel-eslint": "^8.2.6",
|
"babel-eslint": "^9.0.0",
|
||||||
"babel-loader": "^7.1.4",
|
"babel-loader": "^7.1.4",
|
||||||
"babel-plugin-add-module-exports": "^0.3.1",
|
"babel-plugin-add-module-exports": "^0.3.1",
|
||||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
||||||
|
@ -2,9 +2,13 @@
|
|||||||
// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||||
// Code is Apache-2.0 and docs are 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 fetchPonyfill from 'fetch-ponyfill'
|
||||||
import { vsprintf } from 'sprintf-js'
|
import {
|
||||||
|
vsprintf
|
||||||
|
} from 'sprintf-js'
|
||||||
|
|
||||||
import formatText from './format_text'
|
import formatText from './format_text'
|
||||||
import stringifyAsQueryParam from './stringify_as_query_param'
|
import stringifyAsQueryParam from './stringify_as_query_param'
|
||||||
@ -12,6 +16,46 @@ import stringifyAsQueryParam from './stringify_as_query_param'
|
|||||||
const fetch = fetchPonyfill(Promise)
|
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
|
* @private
|
||||||
* imported from https://github.com/bigchaindb/js-utility-belt/
|
* imported from https://github.com/bigchaindb/js-utility-belt/
|
||||||
@ -36,13 +80,17 @@ const fetch = fetchPonyfill(Promise)
|
|||||||
* decamelized into snake case first.
|
* decamelized into snake case first.
|
||||||
* @param {*[]|Object} config.urlTemplateSpec Format spec to use to expand the url (see sprintf).
|
* @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 {*} 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;
|
* @return {Promise} If requestTimeout the timeout function will be called. Otherwise resolve the
|
||||||
* otherwise rejects with the response
|
* Promise with the handleResponse function
|
||||||
*/
|
*/
|
||||||
export default function baseRequest(url, {
|
export default function baseRequest(url, {
|
||||||
jsonBody, query, urlTemplateSpec, ...fetchConfig
|
jsonBody,
|
||||||
} = {}) {
|
query,
|
||||||
|
urlTemplateSpec,
|
||||||
|
...fetchConfig
|
||||||
|
} = {}, requestTimeout) {
|
||||||
let expandedUrl = url
|
let expandedUrl = url
|
||||||
|
|
||||||
if (urlTemplateSpec != null) {
|
if (urlTemplateSpec != null) {
|
||||||
@ -73,19 +121,11 @@ export default function baseRequest(url, {
|
|||||||
if (jsonBody != null) {
|
if (jsonBody != null) {
|
||||||
fetchConfig.body = JSON.stringify(jsonBody)
|
fetchConfig.body = JSON.stringify(jsonBody)
|
||||||
}
|
}
|
||||||
|
if (requestTimeout) {
|
||||||
return fetch.fetch(expandedUrl, fetchConfig)
|
return timeout(requestTimeout, fetch.fetch(expandedUrl, fetchConfig))
|
||||||
.then((res) => {
|
.then(handleResponse)
|
||||||
// If status is not a 2xx (based on Response.ok), assume it's an error
|
} else {
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch
|
return fetch.fetch(expandedUrl, fetchConfig)
|
||||||
if (!(res && res.ok)) {
|
.then(handleResponse)
|
||||||
const errorObject = {
|
}
|
||||||
message: 'HTTP Error: Requested page not reachable',
|
|
||||||
status: `${res.status} ${res.statusText}`,
|
|
||||||
requestURI: res.url
|
|
||||||
}
|
|
||||||
throw errorObject
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -2,27 +2,60 @@
|
|||||||
// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||||
// Code is Apache-2.0 and docs are 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 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 {
|
export default class Connection {
|
||||||
constructor(path, headers = {}) {
|
// This driver implements the BEP-14 https://github.com/bigchaindb/BEPs/tree/master/14
|
||||||
this.path = path
|
constructor(nodes, headers = {}, timeout = DEFAULT_TIMEOUT) {
|
||||||
|
// Copy object
|
||||||
this.headers = Object.assign({}, headers)
|
this.headers = Object.assign({}, headers)
|
||||||
|
|
||||||
|
// Validate headers
|
||||||
Object.keys(headers).forEach(header => {
|
Object.keys(headers).forEach(header => {
|
||||||
if (HEADER_BLACKLIST.includes(header.toLowerCase())) {
|
if (HEADER_BLACKLIST.includes(header.toLowerCase())) {
|
||||||
throw new Error(`Header ${header} is reserved and cannot be set.`)
|
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) {
|
static normalizeNode(node, headers) {
|
||||||
return this.path + {
|
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',
|
'blocks': 'blocks',
|
||||||
'blocksDetail': 'blocks/%(blockHeight)s',
|
'blocksDetail': 'blocks/%(blockHeight)s',
|
||||||
'outputs': 'outputs',
|
'outputs': 'outputs',
|
||||||
@ -37,16 +70,14 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_req(path, options = {}) {
|
_req(path, options = {}) {
|
||||||
// NOTE: `options.headers` could be undefined, but that's OK.
|
return this.transport.forwardRequest(path, options)
|
||||||
options.headers = Object.assign({}, options.headers, this.headers)
|
|
||||||
return request(path, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param blockHeight
|
* @param blockHeight
|
||||||
*/
|
*/
|
||||||
getBlock(blockHeight) {
|
getBlock(blockHeight) {
|
||||||
return this._req(this.getApiUrls('blocksDetail'), {
|
return this._req(Connection.getApiUrls('blocksDetail'), {
|
||||||
urlTemplateSpec: {
|
urlTemplateSpec: {
|
||||||
blockHeight
|
blockHeight
|
||||||
}
|
}
|
||||||
@ -57,7 +88,7 @@ export default class Connection {
|
|||||||
* @param transactionId
|
* @param transactionId
|
||||||
*/
|
*/
|
||||||
getTransaction(transactionId) {
|
getTransaction(transactionId) {
|
||||||
return this._req(this.getApiUrls('transactionsDetail'), {
|
return this._req(Connection.getApiUrls('transactionsDetail'), {
|
||||||
urlTemplateSpec: {
|
urlTemplateSpec: {
|
||||||
transactionId
|
transactionId
|
||||||
}
|
}
|
||||||
@ -69,7 +100,7 @@ export default class Connection {
|
|||||||
* @param status
|
* @param status
|
||||||
*/
|
*/
|
||||||
listBlocks(transactionId) {
|
listBlocks(transactionId) {
|
||||||
return this._req(this.getApiUrls('blocks'), {
|
return this._req(Connection.getApiUrls('blocks'), {
|
||||||
query: {
|
query: {
|
||||||
transaction_id: transactionId,
|
transaction_id: transactionId,
|
||||||
}
|
}
|
||||||
@ -89,7 +120,7 @@ export default class Connection {
|
|||||||
if (spent !== undefined) {
|
if (spent !== undefined) {
|
||||||
query.spent = spent.toString()
|
query.spent = spent.toString()
|
||||||
}
|
}
|
||||||
return this._req(this.getApiUrls('outputs'), {
|
return this._req(Connection.getApiUrls('outputs'), {
|
||||||
query
|
query
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -99,7 +130,7 @@ export default class Connection {
|
|||||||
* @param operation
|
* @param operation
|
||||||
*/
|
*/
|
||||||
listTransactions(assetId, operation) {
|
listTransactions(assetId, operation) {
|
||||||
return this._req(this.getApiUrls('transactions'), {
|
return this._req(Connection.getApiUrls('transactions'), {
|
||||||
query: {
|
query: {
|
||||||
asset_id: assetId,
|
asset_id: assetId,
|
||||||
operation
|
operation
|
||||||
@ -118,7 +149,7 @@ export default class Connection {
|
|||||||
* @param transaction
|
* @param transaction
|
||||||
*/
|
*/
|
||||||
postTransactionSync(transaction) {
|
postTransactionSync(transaction) {
|
||||||
return this._req(this.getApiUrls('transactionsSync'), {
|
return this._req(Connection.getApiUrls('transactionsSync'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
jsonBody: transaction
|
jsonBody: transaction
|
||||||
})
|
})
|
||||||
@ -129,7 +160,7 @@ export default class Connection {
|
|||||||
* @param transaction
|
* @param transaction
|
||||||
*/
|
*/
|
||||||
postTransactionAsync(transaction) {
|
postTransactionAsync(transaction) {
|
||||||
return this._req(this.getApiUrls('transactionsAsync'), {
|
return this._req(Connection.getApiUrls('transactionsAsync'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
jsonBody: transaction
|
jsonBody: transaction
|
||||||
})
|
})
|
||||||
@ -140,7 +171,7 @@ export default class Connection {
|
|||||||
* @param transaction
|
* @param transaction
|
||||||
*/
|
*/
|
||||||
postTransactionCommit(transaction) {
|
postTransactionCommit(transaction) {
|
||||||
return this._req(this.getApiUrls('transactionsCommit'), {
|
return this._req(Connection.getApiUrls('transactionsCommit'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
jsonBody: transaction
|
jsonBody: transaction
|
||||||
})
|
})
|
||||||
@ -150,7 +181,7 @@ export default class Connection {
|
|||||||
* @param search
|
* @param search
|
||||||
*/
|
*/
|
||||||
searchAssets(search) {
|
searchAssets(search) {
|
||||||
return this._req(this.getApiUrls('assets'), {
|
return this._req(Connection.getApiUrls('assets'), {
|
||||||
query: {
|
query: {
|
||||||
search
|
search
|
||||||
}
|
}
|
||||||
@ -161,7 +192,7 @@ export default class Connection {
|
|||||||
* @param search
|
* @param search
|
||||||
*/
|
*/
|
||||||
searchMetadata(search) {
|
searchMetadata(search) {
|
||||||
return this._req(this.getApiUrls('metadata'), {
|
return this._req(Connection.getApiUrls('metadata'), {
|
||||||
query: {
|
query: {
|
||||||
search
|
search
|
||||||
}
|
}
|
||||||
|
110
src/request.js
110
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
|
* @private
|
||||||
* Small wrapper around js-utility-belt's request that provides url resolving,
|
* Small wrapper around js-utility-belt's request that provides url resolving,
|
||||||
* default settings, and response handling.
|
* 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, {
|
export default class Request {
|
||||||
'Content-Type': 'application/json'
|
constructor(node) {
|
||||||
})
|
this.node = node
|
||||||
|
this.backoffTime = null
|
||||||
|
this.retries = 0
|
||||||
|
this.connectionError = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url) {
|
async request(urlPath, config, timeout, maxBackoffTime) {
|
||||||
return Promise.reject(new Error('Request was not given a url.'))
|
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)
|
updateBackoffTime(maxBackoffTime) {
|
||||||
.then(res => res.json())
|
if (!this.connectionError) {
|
||||||
.catch(err => {
|
this.retries = 0
|
||||||
console.error(err)
|
this.backoffTime = null
|
||||||
throw err
|
} 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
73
src/transport.js
Normal file
73
src/transport.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,21 @@
|
|||||||
// Code is Apache-2.0 and docs are CC-BY-4.0
|
// Code is Apache-2.0 and docs are CC-BY-4.0
|
||||||
|
|
||||||
import test from 'ava'
|
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 => {
|
test('baseRequest test query and vsprint', async t => {
|
||||||
const target = {
|
const target = {
|
||||||
|
@ -5,9 +5,12 @@
|
|||||||
import test from 'ava'
|
import test from 'ava'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
|
|
||||||
import * as request from '../../src/request' // eslint-disable-line
|
import {
|
||||||
import { Connection } from '../../src'
|
Connection
|
||||||
import { API_PATH } from '../constants'
|
} from '../../src'
|
||||||
|
import {
|
||||||
|
API_PATH
|
||||||
|
} from '../constants'
|
||||||
|
|
||||||
const conn = new Connection(API_PATH)
|
const conn = new Connection(API_PATH)
|
||||||
|
|
||||||
@ -36,28 +39,65 @@ test('Generate API URLS', t => {
|
|||||||
'assets': 'assets',
|
'assets': 'assets',
|
||||||
}
|
}
|
||||||
Object.keys(endpoints).forEach(endpointName => {
|
Object.keys(endpoints).forEach(endpointName => {
|
||||||
const url = conn.getApiUrls(endpointName)
|
const url = Connection.getApiUrls(endpointName)
|
||||||
const expected = API_PATH + endpoints[endpointName]
|
const expected = endpoints[endpointName]
|
||||||
t.is(url, expected)
|
t.is(url, expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Normalize node from an object', t => {
|
||||||
test('Request with custom headers', t => {
|
const headers = {
|
||||||
const testConn = new Connection(API_PATH, { hello: 'world' })
|
custom: 'headers'
|
||||||
const expectedOptions = {
|
}
|
||||||
|
const node = {
|
||||||
|
endpoint: API_PATH,
|
||||||
headers: {
|
headers: {
|
||||||
|
hello: 'world'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const expectedNode = {
|
||||||
|
'endpoint': API_PATH,
|
||||||
|
'headers': {
|
||||||
hello: 'world',
|
hello: 'world',
|
||||||
custom: 'headers'
|
custom: 'headers'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// request is read only, cannot be mocked?
|
t.deepEqual(Connection.normalizeNode(node, headers), expectedNode)
|
||||||
sinon.spy(request, 'default')
|
})
|
||||||
testConn._req(API_PATH, { headers: { custom: 'headers' } })
|
|
||||||
|
|
||||||
t.truthy(request.default.calledWith(API_PATH, expectedOptions))
|
test('Normalize node from a string', t => {
|
||||||
request.default.restore()
|
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'
|
const blockHeight = 'abc'
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.getBlock(blockHeight)
|
conn.getBlock(blockHeight)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
@ -81,7 +121,7 @@ test('Get transaction for a transaction id', t => {
|
|||||||
const transactionId = 'abc'
|
const transactionId = 'abc'
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.getTransaction(transactionId)
|
conn.getTransaction(transactionId)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
@ -96,7 +136,7 @@ test('Get list of blocks for a transaction id', t => {
|
|||||||
const transactionId = 'abc'
|
const transactionId = 'abc'
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.listBlocks(transactionId)
|
conn.listBlocks(transactionId)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
@ -116,7 +156,7 @@ test('Get list of transactions for an asset id', t => {
|
|||||||
const operation = 'operation'
|
const operation = 'operation'
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.listTransactions(assetId, operation)
|
conn.listTransactions(assetId, operation)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
@ -136,7 +176,7 @@ test('Get outputs for a public key and no spent flag', t => {
|
|||||||
const publicKey = 'publicKey'
|
const publicKey = 'publicKey'
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.listOutputs(publicKey)
|
conn.listOutputs(publicKey)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
@ -152,7 +192,7 @@ test('Get outputs for a public key and spent=false', t => {
|
|||||||
const spent = false
|
const spent = false
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.listOutputs(publicKey, spent)
|
conn.listOutputs(publicKey, spent)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
@ -168,7 +208,7 @@ test('Get outputs for a public key and spent=true', t => {
|
|||||||
const spent = true
|
const spent = true
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.listOutputs(publicKey, spent)
|
conn.listOutputs(publicKey, spent)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
@ -183,7 +223,7 @@ test('Get asset for text', t => {
|
|||||||
const search = 'abc'
|
const search = 'abc'
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.searchAssets(search)
|
conn.searchAssets(search)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
@ -198,7 +238,7 @@ test('Get metadata for text', t => {
|
|||||||
const search = 'abc'
|
const search = 'abc'
|
||||||
|
|
||||||
conn._req = sinon.spy()
|
conn._req = sinon.spy()
|
||||||
conn.getApiUrls = sinon.stub().returns(expectedPath)
|
Connection.getApiUrls = sinon.stub().returns(expectedPath)
|
||||||
|
|
||||||
conn.searchMetadata(search)
|
conn.searchMetadata(search)
|
||||||
t.truthy(conn._req.calledWith(
|
t.truthy(conn._req.calledWith(
|
||||||
|
@ -28,8 +28,8 @@ test('Keypair is created', t => {
|
|||||||
// TODO: The following tests are a bit messy currently, please do:
|
// TODO: The following tests are a bit messy currently, please do:
|
||||||
//
|
//
|
||||||
// - tidy up dependency on `pollStatusAndFetchTransaction`
|
// - tidy up dependency on `pollStatusAndFetchTransaction`
|
||||||
test('Valid CREATE transaction', t => {
|
test('Valid CREATE transaction with default node', t => {
|
||||||
const conn = new Connection(API_PATH)
|
const conn = new Connection()
|
||||||
|
|
||||||
const tx = Transaction.makeCreateTransaction(
|
const tx = Transaction.makeCreateTransaction(
|
||||||
asset(),
|
asset(),
|
||||||
@ -40,7 +40,9 @@ test('Valid CREATE transaction', t => {
|
|||||||
const txSigned = Transaction.signTransaction(tx, alice.privateKey)
|
const txSigned = Transaction.signTransaction(tx, alice.privateKey)
|
||||||
|
|
||||||
return conn.postTransaction(txSigned)
|
return conn.postTransaction(txSigned)
|
||||||
.then(resTx => t.truthy(resTx))
|
.then(resTx => {
|
||||||
|
t.truthy(resTx)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
31
test/request/test_request.js
Normal file
31
test/request/test_request.js
Normal file
@ -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)
|
||||||
|
})
|
23
test/transport/test_transport.js
Normal file
23
test/transport/test_transport.js
Normal file
@ -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)
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user