graphbrainz/src/api.js

195 lines
5.5 KiB
JavaScript
Raw Permalink Normal View History

2016-08-08 07:54:06 +00:00
import request from 'request'
import retry from 'retry'
import qs from 'qs'
import ExtendableError from 'es6-error'
import RateLimit from './rate-limit'
import pkg from '../package.json'
2016-11-26 01:38:32 +00:00
const debug = require('debug')('graphbrainz:api')
2016-08-20 05:59:32 +00:00
// If the `request` callback returns an error, it indicates a failure at a lower
// level than the HTTP response itself. If it's any of the following error
// codes, we should retry.
2016-08-08 07:54:06 +00:00
const RETRY_CODES = {
ECONNRESET: true,
ENOTFOUND: true,
ESOCKETTIMEDOUT: true,
ETIMEDOUT: true,
ECONNREFUSED: true,
EHOSTUNREACH: true,
EPIPE: true,
EAI_AGAIN: true
}
export class MusicBrainzError extends ExtendableError {
constructor (message, statusCode) {
super(message)
this.statusCode = statusCode
}
}
export default class MusicBrainz {
2016-11-26 01:38:32 +00:00
constructor ({
baseURL = process.env.MUSICBRAINZ_BASE_URL || 'http://musicbrainz.org/ws/2/',
userAgent = `${pkg.name}/${pkg.version} ` +
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
2016-12-07 08:23:02 +00:00
extraHeaders = {},
2016-11-26 01:38:32 +00:00
timeout = 60000,
// MusicBrainz API requests are limited to an *average* of 1 req/sec.
// That means if, for example, we only need to make a few API requests to
// fulfill a query, we might as well make them all at once - as long as
// we then wait a few seconds before making more. In practice this can
// seemingly be set to about 5 requests every 5 seconds before we're
// considered to exceed the rate limit.
limit = 5,
2016-11-26 02:18:41 +00:00
period = 5500,
2016-11-26 01:38:32 +00:00
concurrency = 10,
retries = 10,
// It's OK for `retryDelayMin` to be less than one second, even 0, because
// `RateLimit` will already make sure we don't exceed the API rate limit.
// We're not doing exponential backoff because it will help with being
// rate limited, but rather to be chill in case MusicBrainz is returning
// some other error or our network is failing.
retryDelayMin = 100,
retryDelayMax = 60000,
randomizeRetry = true
} = {}) {
this.baseURL = baseURL
this.userAgent = userAgent
2016-12-07 08:23:02 +00:00
this.extraHeaders = extraHeaders
2016-11-26 01:38:32 +00:00
this.timeout = timeout
this.limiter = new RateLimit({ limit, period, concurrency })
2016-08-08 07:54:06 +00:00
this.retryOptions = {
2016-11-26 01:38:32 +00:00
retries,
minTimeout: retryDelayMin,
maxTimeout: retryDelayMax,
randomize: randomizeRetry
2016-08-08 07:54:06 +00:00
}
}
2016-08-20 05:59:32 +00:00
/**
* Determine if we should retry the request based on the given error.
* Retry any 5XX response from MusicBrainz, as well as any error in
* `RETRY_CODES`.
*/
2016-08-08 07:54:06 +00:00
shouldRetry (err) {
if (err instanceof MusicBrainzError) {
return err.statusCode >= 500 && err.statusCode < 600
}
return RETRY_CODES[err.code] || false
}
2016-08-20 05:59:32 +00:00
/**
* Send a request without any retrying or rate limiting.
* Use `get` instead.
*/
_get (path, params, info = {}) {
2016-08-08 07:54:06 +00:00
return new Promise((resolve, reject) => {
const options = {
baseUrl: this.baseURL,
url: path,
2016-08-20 05:59:32 +00:00
qs: { ...params, fmt: 'json' },
2016-08-08 07:54:06 +00:00
json: true,
gzip: true,
2016-12-07 08:23:02 +00:00
headers: { 'User-Agent': this.userAgent, ...this.extraHeaders },
2016-08-08 07:54:06 +00:00
timeout: this.timeout
}
2016-12-03 07:59:19 +00:00
debug(`Sending request. url=${path} attempt=${info.currentAttempt}`)
2016-08-08 07:54:06 +00:00
request(options, (err, response, body) => {
if (err) {
reject(err)
} else if (response.statusCode !== 200) {
const message = (body && body.error) || ''
reject(new MusicBrainzError(message, response.statusCode))
} else {
resolve(body)
}
})
})
}
2016-08-20 05:59:32 +00:00
/**
* Send a request with retrying and rate limiting.
*/
2016-08-08 07:54:06 +00:00
get (path, params) {
return new Promise((resolve, reject) => {
const fn = this._get.bind(this)
const operation = retry.operation(this.retryOptions)
operation.attempt(currentAttempt => {
2016-08-20 05:59:32 +00:00
// This will increase the priority in our `RateLimit` queue for each
// retry, so that newer requests don't delay this one further.
2016-08-08 07:54:06 +00:00
const priority = currentAttempt
2016-08-20 05:59:32 +00:00
this.limiter.enqueue(fn, [path, params, { currentAttempt }], priority)
2016-08-08 07:54:06 +00:00
.then(resolve)
.catch(err => {
if (!this.shouldRetry(err) || !operation.retry(err)) {
reject(operation.mainError() || err)
}
})
})
})
}
2016-08-20 05:59:32 +00:00
stringifyParams (params) {
2016-12-07 08:23:02 +00:00
if (Array.isArray(params.inc)) {
2016-08-08 07:54:06 +00:00
params = {
...params,
inc: params.inc.join('+')
}
}
2016-12-07 08:23:02 +00:00
if (Array.isArray(params.type)) {
2016-08-20 05:59:32 +00:00
params = {
...params,
type: params.type.join('|')
}
}
2016-12-07 08:23:02 +00:00
if (Array.isArray(params.status)) {
2016-08-20 05:59:32 +00:00
params = {
...params,
status: params.status.join('|')
}
}
return qs.stringify(params, {
2016-08-08 07:54:06 +00:00
skipNulls: true,
filter: (key, value) => value === '' ? undefined : value
})
2016-08-20 05:59:32 +00:00
}
getURL (path, params) {
const query = params ? this.stringifyParams(params) : ''
return query ? `${path}?${query}` : path
}
getLookupURL (entity, id, params) {
if (id == null) {
return this.getBrowseURL(entity, params)
}
2016-08-20 05:59:32 +00:00
return this.getURL(`${entity}/${id}`, params)
2016-08-08 07:54:06 +00:00
}
lookup (entity, id, params = {}) {
const url = this.getLookupURL(entity, id, params)
return this.get(url)
}
2016-08-20 05:59:32 +00:00
getBrowseURL (entity, params) {
return this.getURL(entity, params)
}
browse (entity, params = {}) {
const url = this.getBrowseURL(entity, params)
return this.get(url)
}
getSearchURL (entity, query, params) {
return this.getURL(entity, { ...params, query })
}
search (entity, query, params = {}) {
const url = this.getSearchURL(entity, query, params)
return this.get(url)
}
2016-08-08 07:54:06 +00:00
}