Skip to content

Commit

Permalink
feat: add client options to control retries and timeout (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
Qrzy committed Dec 5, 2023
1 parent 683ec1d commit 8a9672e
Show file tree
Hide file tree
Showing 32 changed files with 158 additions and 121 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ It uses [ofetch](https://github.com/unjs/ofetch) under the hood.
## Example usage:

```js
import bggXmlApiClient from 'bgg-xml-api-client'
import { bggXmlApiClient } from 'bgg-xml-api-client'

const response = await bggXmlApiClient.get('user', { name: 'Qrzy88' })

Expand All @@ -33,3 +33,33 @@ There are also wrappers available for certain resources that accept params (alre
- `getBggThing(params)`
- `getBggThread(params)`
- `getBggUser(params)`

## Client options

Both main client as well as wrappers accept one more parameter that can override default options:

```js
interface ClientOptions {
maxRetries: number // default 10
retryInterval: number // default 5000[ms] (5s)
timeout: number // default 10000[ms] (10s)
}
```

One can use it to control the retry flow when collections API replies with 202 status code meaning the request is still processing and one should retry later for actual results.

For example, in order to increase number of retries on 202 response to 20 made in an interval of 3s:

```js
import { bggXmlApiClient } from 'bgg-xml-api-client'

const response = await bggXmlApiClient.get('collection', { username: 'Qrzy88' }, { maxRetries: 20, retryInterval: 3000 })
```

or to reduce the timeout to 5s when fetching user:

```js
import { getBggUser } from 'bgg-xml-api-client'

const response = await getBggUser({ name: 'Qrzy88' }, { timeout: 5000 })
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
},
"dependencies": {
"fast-xml-parser": "^4.2.4",
"ofetch": "^1.1.0"
"ofetch": "^1.3.3"
},
"devDependencies": {
"@antfu/eslint-config": "^0.39.5",
Expand Down
42 changes: 30 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 0 additions & 33 deletions src/client/getBggApiClient.ts

This file was deleted.

42 changes: 31 additions & 11 deletions src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
/* istanbul ignore file */
import { ofetch } from 'ofetch'
import { createResourceUrl } from '../helpers/createResourceUrl'
import type { BggParams, ResourceName } from '../types'
import { getBggApiClient } from './getBggApiClient'
import type { BggParams, ClientOptions, ResourceName } from '../types'
import { xmlParser } from '../helpers/xmlParser'
import { getBaseUrlForResource } from '../helpers/getBaseUrlForResource'

export const DEFAULT_MAX_RETRIES = 10
export const DEFAULT_INTERVAL = 5000
export const DEFAULT_TIMEOUT = 10000 // milliseconds

export const bggXmlApiClient = {
get: async <T = unknown>(
resource: ResourceName,
queryParams: BggParams,
maxRetries: number = DEFAULT_MAX_RETRIES,
retryInterval: number = DEFAULT_INTERVAL,
{
maxRetries = DEFAULT_MAX_RETRIES,
retryInterval = DEFAULT_INTERVAL,
timeout = DEFAULT_TIMEOUT,
}: Partial<ClientOptions> = {},
): Promise<T> => {
const client = getBggApiClient(resource)
const apiFetch = ofetch.create({
baseURL: getBaseUrlForResource(resource),
headers: {
'Content-Type': 'text/xml',
},
responseType: 'text',
onResponse(context) {
if (context.response.status === 202)
throw new Error('processing...')
},
})

for (let i = 0; i < maxRetries; i++) {
try {
const resourceUrl = createResourceUrl(resource, queryParams)
const response = await client.get<T>(resourceUrl)
if (typeof response === 'string' && (response as string).includes('processed'))
throw new Error('processing...')

return response
const response = await apiFetch<T, 'text'>(resourceUrl, { timeout })
const parsedResponse = xmlParser.parse<{ [key: string]: T }>(response)
return parsedResponse[Object.keys(parsedResponse).shift()!]
}
catch (err) {
await new Promise<void>(resolve => setTimeout(() => resolve(), retryInterval))
if (err instanceof Error && err.message === 'processing...')
// BGG API is still processing the request, retry after a while
await new Promise<void>(resolve => setTimeout(() => resolve(), retryInterval))
else
// an actual error occurred, throw it
throw err
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/helpers/xmlParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ const options: Partial<validationOptions & X2jOptions> = {
const parser = new XMLParser(options)

export const xmlParser: XmlParser = {
parse: (xmlString: XmlString) => parser.parse(xmlString),
parse: <T = unknown>(xmlString: XmlString): T => parser.parse(xmlString),
}
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export type ResourceName =
| 'hot'
| 'search'

export interface ClientOptions {
maxRetries: number
retryInterval: number
timeout: number
}

export type OneOrNothing = 1 | undefined

export type SingleOrMany<T> = T | T[]
Expand Down Expand Up @@ -64,5 +70,5 @@ export type BggParams =
export type XmlString = string

export interface XmlParser {
parse: (text: XmlString) => unknown
parse: <T = unknown>(text: XmlString) => T
}
5 changes: 3 additions & 2 deletions src/wrappers/getBggCollection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { bggXmlApiClient } from '../client'
import type { OfValue, OneOrNothing } from '../types'
import type { ClientOptions, OfValue, OneOrNothing } from '../types'

type BggCollectionSubtype =
| 'boardgame'
Expand Down Expand Up @@ -135,12 +135,13 @@ export interface BggCollectionResponse {

export function getBggCollection(
params: BggCollectionParams,
settings: Partial<ClientOptions> = {},
): Promise<BggCollectionResponse> {
const newParams = {
...params,
...(params.id && {
id: Array.isArray(params.id) ? params.id.join(',') : params.id,
}),
}
return bggXmlApiClient.get('collection', newParams)
return bggXmlApiClient.get('collection', newParams, settings)
}
6 changes: 3 additions & 3 deletions src/wrappers/getBggFamily.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { bggXmlApiClient } from '../client'
import type { BggFamilyType, SingleOrMany } from '../types'
import type { BggFamilyType, ClientOptions, SingleOrMany } from '../types'

export interface BggFamilyParams {
id?: number | number[] | string
Expand Down Expand Up @@ -31,10 +31,10 @@ export interface BggFamilyResponse {
[prop: string]: unknown
}

export function getBggFamily(params: BggFamilyParams): Promise<BggFamilyResponse> {
export function getBggFamily(params: BggFamilyParams, settings: Partial<ClientOptions> = {}): Promise<BggFamilyResponse> {
const newParams = {
...params,
...(params.id && { id: Array.isArray(params.id) ? params.id.join(',') : params.id }),
}
return bggXmlApiClient.get('family', newParams)
return bggXmlApiClient.get('family', newParams, settings)
}
5 changes: 3 additions & 2 deletions src/wrappers/getBggForum.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { bggXmlApiClient } from '../client'
import type { ClientOptions } from '../types'

export interface BggForumParams {
id?: number
Expand Down Expand Up @@ -28,6 +29,6 @@ export interface BggForumResponse {
[prop: string]: unknown
}

export function getBggForum(params: BggForumParams): Promise<BggForumResponse> {
return bggXmlApiClient.get('forum', params)
export function getBggForum(params: BggForumParams, settings: Partial<ClientOptions> = {}): Promise<BggForumResponse> {
return bggXmlApiClient.get('forum', params, settings)
}
5 changes: 3 additions & 2 deletions src/wrappers/getBggForumlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// TODO: get known what is it and test properly!

import { bggXmlApiClient } from '../client'
import type { ClientOptions } from '../types'

export interface BggForumlistParams {
id?: number
Expand All @@ -13,6 +14,6 @@ export interface BggForumlistResponse {
[prop: string]: unknown
}

export function getBggForumlist(params: BggForumlistParams): Promise<BggForumlistResponse> {
return bggXmlApiClient.get('forumlist', params)
export function getBggForumlist(params: BggForumlistParams, settings: Partial<ClientOptions> = {}): Promise<BggForumlistResponse> {
return bggXmlApiClient.get('forumlist', params, settings)
}
Loading

0 comments on commit 8a9672e

Please sign in to comment.