@jlenon7/zedjs
Version:
Riot API Library for NodeJS
246 lines (231 loc) • 7.48 kB
text/typescript
import rp from 'request-promise'
import * as _ from 'lodash'
import { ApiKeyNotFound } from '../errors'
import { IEndpoint } from '../endpoints'
import { TOO_MANY_REQUESTS, SERVICE_UNAVAILABLE } from 'http-status-codes'
import { config } from 'dotenv'
import { ApiResponseDTO } from '../models-dto/api-response/api-response'
import { RateLimitDto } from '../models-dto/rate-limit/rate-limit.dto'
import { GenericError } from '../errors/Generic.error'
import { RateLimitError } from '../errors/rate-limit.error'
import { IBaseApiParams, IParams, waiter } from './base.utils'
import { ServiceUnavailable } from '../errors/service-unavailable.error'
import { BaseConstants, BaseApiGames } from './base.const'
import { Logger } from './logger.base'
import { RequestBase } from './request.base'
config()
export class BaseApi<Region extends string> {
protected readonly game: BaseApiGames = BaseApiGames.LOL
private readonly baseUrl = BaseConstants.BASE_URL
private key: string
private concurrency: number | undefined
private rateLimitRetry: boolean = true
private rateLimitRetryAttempts: number = BaseConstants.RETRY_ATTEMPTS
private debug = {
logTime: false,
logUrls: false,
logRatelimits: false
}
constructor ()
constructor (params: IBaseApiParams)
/**
* Base api
* @param key Riot games api key
*/
constructor (key: string)
constructor (param?: string | IBaseApiParams) {
this.key = process.env.RIOT_API_KEY || ''
if (typeof param === 'string') {
this.key = param
} else if (param) {
if (typeof param.key === 'string') {
this.key = param.key
}
this.setParams(param)
}
}
private setParams (param: IBaseApiParams) {
if (typeof param.rateLimitRetry !== 'undefined') {
this.rateLimitRetry = param.rateLimitRetry
}
if (typeof param.rateLimitRetryAttempts !== 'undefined') {
this.rateLimitRetryAttempts = param.rateLimitRetryAttempts
}
if (typeof param.debug !== 'undefined') {
if (typeof param.debug.logTime !== 'undefined') {
_.set(this.debug, 'logTime', param.debug.logTime)
}
if (typeof param.debug.logUrls !== 'undefined') {
_.set(this.debug, 'logUrls', param.debug.logUrls)
}
if (typeof param.debug.logRatelimits !== 'undefined') {
_.set(this.debug, 'logRatelimits', param.debug.logRatelimits)
}
}
this.concurrency = param.concurrency
if (typeof param.concurrency !== 'undefined') {
RequestBase.setConcurrency(param.concurrency)
} else {
RequestBase.setConcurrency(Infinity)
}
}
private getRateLimits (headers: any): RateLimitDto {
return {
Type: _.get(headers, 'x-rate-limit-type', null),
AppRateLimit: _.get(headers, 'x-app-rate-limit', null),
AppRateLimitCount: _.get(headers, 'x-app-rate-limit-count', null),
MethodRateLimit: _.get(headers, 'x-method-rate-limit'),
MethodRatelimitCount: _.get(headers, 'x-method-rate-limit-count', null),
RetryAfter: +_.get(headers, 'retry-after', 0),
EdgeTraceId: _.get(headers, 'x-riot-edge-trace-id')
}
}
private getBaseUrl () {
return this.baseUrl.replace(':game', this.game)
}
private getApiUrl (endpoint: IEndpoint, params: IParams) {
const {
prefix,
version,
path
} = endpoint
const basePath = `${prefix}/v${version}/${path}`
const re = /\$\(([^\)]+)?\)/g
let base = `${this.getBaseUrl()}/${basePath}`
let match
// tslint:disable:no-conditional-assignment
while (match = re.exec(base)) {
const [key] = match
const value = encodeURI(String(params[match[1]]))
base = base.replace(key, value)
re.lastIndex = 0
}
return base
}
private isRateLimitError (e: any) {
if (!e) {
return false
}
const {
statusCode = e.status
} = e || e.error
return statusCode === TOO_MANY_REQUESTS
}
private isServiceUnavailableError (e: any) {
if (!e) {
return false
}
const {
statusCode = e.status
} = e || e.error
return statusCode === SERVICE_UNAVAILABLE
}
private getError (e: any) {
const headers = this.getRateLimits(_.get(e, 'response.headers'))
if (this.isRateLimitError(e)) {
return new RateLimitError(headers)
}
if (this.isServiceUnavailableError(e)) {
return new ServiceUnavailable(headers, e)
}
// Otherwise generic error
return new GenericError(headers, e)
}
private internalRequest<T> (options: rp.OptionsWithUri): Promise<T> {
return RequestBase.request<T>(options)
}
private async retryRateLimit<T> (region: Region, endpoint: IEndpoint, params?: IParams, e?: any): Promise<ApiResponseDTO<T>> {
let baseError = this.getError(e)
const isRateLimitError = this.isRateLimitError(e) || this.isServiceUnavailableError(e)
if (!this.rateLimitRetry || !isRateLimitError || this.rateLimitRetryAttempts < 1) {
throw baseError
}
const forceError = true
for (let i = 0; i < this.rateLimitRetryAttempts; i++) {
try {
const response = await this.request<T>(region, endpoint, params, forceError)
return response
} catch (error) {
const parseError = this.getError(error)
// Isn't rate limit error
if (!this.isRateLimitError(error) && !this.isServiceUnavailableError(error)) {
throw parseError
}
// Set a new attemp
const {
rateLimits: {
RetryAfter
}
} = parseError
const waitSeconds =
this.isServiceUnavailableError(e) ?
BaseConstants.SERVICE_UNAVAILABLE :
BaseConstants.RATE_LIMIT
const msToWait = ((RetryAfter || 0) * 1000) + (waitSeconds * 1000 * Math.random())
// Log
if (this.debug.logRatelimits) {
Logger.rateLimit(endpoint, msToWait)
}
// Wait
await waiter(msToWait)
}
}
// Throw rate limit
throw baseError
}
protected getParam (): IBaseApiParams {
return {
key: this.key,
rateLimitRetry: this.rateLimitRetry,
rateLimitRetryAttempts: this.rateLimitRetryAttempts,
concurrency: this.concurrency,
debug: this.debug
}
}
protected async request<T> (region: Region, endpoint: IEndpoint, params?: IParams, forceError?: boolean, qs?: any): Promise<ApiResponseDTO<T>> {
if (!this.key) {
throw new ApiKeyNotFound()
}
// Logger
if (this.debug.logTime) {
Logger.start(endpoint)
}
// Url params
params = params || {}
params.region = region.toLowerCase()
// Format
const uri = this.getApiUrl(endpoint, params)
const options: rp.OptionsWithUri = {
uri,
method: 'GET',
headers: {
Origin: null,
'X-Riot-Token': this.key
},
qs,
useQuerystring: true,
resolveWithFullResponse: true,
json: true
}
if (this.debug.logUrls) {
Logger.uri(options, endpoint)
}
try {
const apiResponse = await this.internalRequest<any>(options)
const { body, headers } = apiResponse
return {
rateLimits: this.getRateLimits(headers),
response: body
}
} catch (e) {
if (forceError) {
throw e
}
return await this.retryRateLimit<T>(region, endpoint, params, e)
} finally {
if (this.debug.logTime) {
Logger.end(endpoint)
}
}
}
}