Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto batch #72

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
npm install gw2api-client
```

This module can be used for Node.js as well as browsers using [Browserify](https://github.com/substack/browserify-handbook#how-node_modules-works).
This module can be used for Node.js as well as browsers using [Browserify](https://github.com/substack/browserify-handbook#how-node_modules-works).

## Usage

Expand Down Expand Up @@ -98,6 +98,42 @@ The cache uses expiration times and not the build number, because the content of
setInterval(() => api.flushCacheIfGameUpdated(), 10 * 60 * 1000)
```

### Auto-Batching

By default, all API requests are executed individually against the live API (or the cache). You can queue/batch requests to bulk endpoints with the `.autoBatch(batchDelay)` method. When enabled, the library will pool ids from get/many for the specified `batchDelay` in ms(default 50 ms), then make API requests with all pending IDs.

```js
api.items().autoBatch().get(100)
api.items().autoBatch().get(100)
api.items().autoBatch().get(101)
api.items().autoBatch().many([100, 101, 102, 103])
api.items().autoBatch().many([100, 103, 104, 105])
// when called in immediate succession here, the only API call will be:
// https://api.guildwars2.com/v2/items?ids=100,101,102,103,104,105
```

Autobatching can be enabled for all endpoints by calling `client.autoBatch()`:

```js
api.autoBatch() // autobatching turned on for all endpoints stemmed from this client object
api.items().get(100)
api.items().get(100)
api.items().get(101)
api.items().many([100, 101, 102, 103])
api.items().many([100, 103, 104, 105])
```

Or enable only for a single Endpoint by saving a reference to an endpoint after calling autobatch:

```js
const itemsApi = api.items().autoBatch() // autobatching turned on for all calls from this endpoint
itemsApi.get(100)
itemsApi.get(100)
itemsApi.get(101)
itemsApi.many([100, 101, 102, 103])
itemsApi.many([100, 103, 104, 105])
```

### Error handling

You can use the Promise `catch` to handle all possible errors.
Expand Down
11 changes: 11 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module.exports = class Client {
this.caches = [nullCache()]
this.debug = false
this.client = this
this.autoBatching = false
this.autoBatchDelay = 50
}

// Set the schema version
Expand Down Expand Up @@ -79,6 +81,15 @@ module.exports = class Client {
})
}

// Turn on autobatching for all endpoints
autoBatch (autoBatchDelay) {
if (autoBatchDelay) {
this.autoBatchDelay = autoBatchDelay
}
this.autoBatching = true
return this
}

// All the different API endpoints
account () {
return new endpoints.AccountEndpoint(this)
Expand Down
81 changes: 76 additions & 5 deletions src/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const hashString = require('./hash')

const clone = (x) => JSON.parse(JSON.stringify(x))

const autoBatchSharedData = {}

module.exports = class AbstractEndpoint {
constructor (parent) {
this.client = parent.client
Expand All @@ -24,13 +26,18 @@ module.exports = class AbstractEndpoint {
this.isAuthenticated = false
this.isOptionallyAuthenticated = false
this.credentials = false

this.autoBatching = parent.autoBatching
this.autoBatchDelay = parent.autoBatchDelay
this.setupAutoBatchSharedData()

this._skipCache = false
}

// Set the schema version
schema (schema) {
this.schemaVersion = schema
this.setupAutoBatchSharedData()
this.debugMessage(`set the schema to ${schema}`)
return this
}
Expand All @@ -43,13 +50,15 @@ module.exports = class AbstractEndpoint {
// Set the language for locale-aware endpoints
language (lang) {
this.lang = lang
this.setupAutoBatchSharedData()
this.debugMessage(`set the language to ${lang}`)
return this
}

// Set the api key for authenticated endpoints
authenticate (apiKey) {
this.apiKey = apiKey
this.setupAutoBatchSharedData()
this.debugMessage(`set the api key to ${apiKey}`)
return this
}
Expand All @@ -74,6 +83,29 @@ module.exports = class AbstractEndpoint {
return this
}

// Turn on auto-batching for this endpoint
autoBatch (autoBatchDelay) {
if (autoBatchDelay) {
this.autoBatchDelay = autoBatchDelay
this.setupAutoBatchSharedData()
}
this.autoBatching = true
return this
}

// Sets _autoBatch to shared batching object based on _cacheHash
setupAutoBatchSharedData() {
const autoBatchId = this._cacheHash(this.constructor.name + this.autoBatchDelay)
if (!autoBatchSharedData[autoBatchId]) {
autoBatchSharedData[autoBatchId] = {
idsForNextBatch: new Set(),
nextBatchPromise: null,
}
}

this._autoBatch = autoBatchSharedData[autoBatchId]
}

// Get all ids
ids () {
this.debugMessage(`ids(${this.url}) called`)
Expand Down Expand Up @@ -156,7 +188,14 @@ module.exports = class AbstractEndpoint {

// Request the single id if the endpoint a bulk endpoint
if (this.isBulk && !url) {
return this._request(`${this.url}?id=${id}`)
if (this.autoBatching) {
return this._autoBatchMany([id]).then((items) => {
return items[0]?items[0]:null
})
}
else {
return this._request(`${this.url}?id=${id}`)
}
}

// We are dealing with a custom url instead
Expand All @@ -168,8 +207,34 @@ module.exports = class AbstractEndpoint {
return this._request(this.url)
}

_autoBatchMany (ids) {
if (this._autoBatch.idsForNextBatch.size === 0) {
this._autoBatch.nextBatchPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const batchedIds = Array.from(this._autoBatch.idsForNextBatch)
this.debugMessage(`autoBatchMany called (${batchedIds.length} ids)`)
this._autoBatch.idsForNextBatch.clear()
return resolve(this.many(batchedIds, true))
}, this.autoBatchDelay)
}).then(items => {
const indexedItems = {}
items.forEach(item => {
indexedItems[item.id] = item
})
return indexedItems
})
}

// Add the requested ids to the pending ids
ids.forEach(id => this._autoBatch.idsForNextBatch.add(id))
// Return the results based on the requested ids
return this._autoBatch.nextBatchPromise.then(indexedItems => {
return ids.map(id => indexedItems[id]).filter(x => x)
})
}

// Get multiple entries by ids
many (ids) {
many (ids, skipAutoBatch) {
this.debugMessage(`many(${this.url}) called (${ids.length} ids)`)

if (!this.isBulk) {
Expand All @@ -186,7 +251,7 @@ module.exports = class AbstractEndpoint {

// There is no cache time set, so always use the live data
if (!this.cacheTime) {
return this._many(ids)
return this._many(ids, undefined, skipAutoBatch)
}

// Get as much as possible out of the cache
Expand All @@ -201,7 +266,7 @@ module.exports = class AbstractEndpoint {

this.debugMessage(`many(${this.url}) resolving partially from cache (${cached.length} ids)`)
const missingIds = getMissingIds(ids, cached)
return this._many(missingIds, cached.length > 0).then(content => {
return this._many(missingIds, cached.length > 0, skipAutoBatch).then(content => {
const cacheContent = content.map(value => [this._cacheHash(value.id), value])
this._cacheSetMany(cacheContent)

Expand Down Expand Up @@ -230,9 +295,15 @@ module.exports = class AbstractEndpoint {
}

// Get multiple entries by ids from the live API
_many (ids, partialRequest = false) {
_many (ids, partialRequest = false, skipAutoBatch) {
this.debugMessage(`many(${this.url}) requesting from api (${ids.length} ids)`)

if (this.autoBatching && !skipAutoBatch) {
return this._autoBatchMany(ids)
}



// Chunk the requests to the max page size
const pages = chunk(ids, this.maxPageSize)
const requests = pages.map(page => `${this.url}?ids=${page.join(',')}`)
Expand Down
20 changes: 20 additions & 0 deletions tests/client.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ describe('client', () => {
client.build = tmp
})

describe('autobatch', () => {
const batchDelay = 10
it('can turn on autobatching with specified delay', () => {
let api = client.autoBatch(100)
expect(client.autoBatchDelay).toEqual(100)
expect(api).toBeInstanceOf(Module)

let endpoint = client.account().autoBatch(200)
expect(endpoint.autoBatchDelay).toEqual(200)
expect(client.autoBatch(300).autoBatchDelay).toEqual(300)
expect(endpoint.autoBatchDelay).toEqual(200)
})

it ('has default bach delay of 50', () => {
let endpoint = client.autoBatch().items()
expect(client.autoBatchDelay).toEqual(50)
expect(endpoint.autoBatchDelay).toEqual(50)
})
})

it('can get the account endpoint', () => {
let endpoint = client.account()
expect(endpoint.url).toEqual('/v2/account')
Expand Down
Loading