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-batching functionality #71

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = class Client {
this.caches = [nullCache()]
this.debug = false
this.client = this
this.autoBatchPool = {}
}

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

//maintains pool of static endpoints with auto-batching enabled
autoBatch (endpointName, autoBatchInterval = 1000) {
if (this.autoBatchPool[endpointName]) {
return this.autoBatchPool[endpointName]
}

if (!this[endpointName]) {
return new Error(`no enpoint ${endpointName} found`)
}

const resultEndpoint = this[endpointName]()
if (resultEndpoint.isBulk) {
this.autoBatchPool[endpointName] = resultEndpoint.enableAutoBatch(autoBatchInterval)
}
else {
this.debugMessage(`${endpointName} is not bulk expanding, endpoint will not have any autobatch behavior`)
}

return resultEndpoint
}

// All the different API endpoints
account () {
return new endpoints.AccountEndpoint(this)
Expand Down
67 changes: 65 additions & 2 deletions src/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ module.exports = class AbstractEndpoint {
this.isPaginated = false
this.maxPageSize = 200
this.isBulk = false
this.supportsBulkAll = true
this.supportsBulkAll = true
this.isLocalized = false
this.isAuthenticated = false
this.isOptionallyAuthenticated = false
this.credentials = false

this._autoBatch = null

this._skipCache = false
}

Expand Down Expand Up @@ -74,6 +76,19 @@ module.exports = class AbstractEndpoint {
return this
}

//turns on auto-batching for this endpoint
enableAutoBatch (interval) {
if (this._autoBatch === null) {
this._autoBatch = {
interval: interval || 1000,
set: new Set(),
nextBatchPromise: null,
autoBatchOverride: false,
}
}
return this
}

// Get all ids
ids () {
this.debugMessage(`ids(${this.url}) called`)
Expand Down Expand Up @@ -156,7 +171,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._autoBatch === null) {
return this._request(`${this.url}?id=${id}`)
}
else {
return this._autoBatchMany([id]).then((items) => {
return items[0]?items[0]:null
})
}
}

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

_autoBatchMany (ids) {
if (!this._autoBatch.set) {
this._autoBatch.set = new Set()
}
if (this._autoBatch.set.size === 0) {
this._autoBatch.nextBatchPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const batchedIds = Array.from(this._autoBatch.set)
this.debugMessage(`batch sending for ${batchedIds}`)
this._autoBatch.set.clear()
this._autoBatch.autoBatchOverride = true
return resolve(this.many(batchedIds))
}, this._autoBatch.interval) // by default is 1000, 1 sec
}).then(items => {
const indexedItems = {}
items.forEach(item => {
indexedItems[item.id] = item
})
return indexedItems
})
}

//add ids to set
ids.forEach(id => this._autoBatch.set.add(id))
// return array with results requested
return this._autoBatch.nextBatchPromise.then(indexedItems => {
return ids.map(id => indexedItems[id]).filter(x => x)
})
}

// Get multiple entries by ids
many (ids) {
this.debugMessage(`many(${this.url}) called (${ids.length} ids)`)
Expand Down Expand Up @@ -233,6 +285,17 @@ module.exports = class AbstractEndpoint {
_many (ids, partialRequest = false) {
this.debugMessage(`many(${this.url}) requesting from api (${ids.length} ids)`)

if (this._autoBatch !== null) {
if (this._autoBatch.autoBatchOverride) {
this._autoBatch.autoBatchOverride = false
}
else {
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
23 changes: 23 additions & 0 deletions tests/client.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,29 @@ describe('client', () => {
client.build = tmp
})

describe('autobatch', () => {
const interval = 10
it('can get an autobatching endpoint', () => {
let endpoint = client.autoBatch('items', interval)
expect(endpoint.url).toEqual('/v2/items')
expect(endpoint._autoBatch.interval).toEqual(interval)
})

it('adds the endpoint to the pool', () => {
client.autoBatch('items', interval)
expect(client.autoBatchPool.items).toBeDefined()
})

it('returns the same endpoint on subsequent calls', () => {
let endpoint1 = client.autoBatch('items', interval)
endpoint1.arbitraryPropertyNameForTesting = 'test confirmed'
let endpoint2 = client.autoBatch('items', interval)
expect(endpoint2.arbitraryPropertyNameForTesting).toEqual('test confirmed')
})


})

it('can get the account endpoint', () => {
let endpoint = client.account()
expect(endpoint.url).toEqual('/v2/account')
Expand Down
64 changes: 64 additions & 0 deletions tests/endpoint.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,70 @@ describe('abstract endpoint', () => {
})
})

describe('auto batching', () => {
const interval = 10
beforeEach(() => {
endpoint.enableAutoBatch(interval)
})

it('sets up _autoBatch variable', () => {
let x = endpoint.enableAutoBatch(interval)
expect(x).toBeInstanceOf(Module)
expect(x._autoBatch.interval).toEqual(interval)
expect(x._autoBatch.set).toBeDefined()
expect(x._autoBatch.nextBatchPromise).toBeNull()
expect(x._autoBatch.autoBatchOverride).toEqual(false)
})

it('supports batching from get', async () => {
let content = [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }]
endpoint.isBulk = true
endpoint.url = '/v2/test'
// endpoint.enableAutoBatch(10)
fetchMock.addResponse(content)

let [entry1, entry2] = await Promise.all([endpoint.get(1), endpoint.get(2)])
expect(fetchMock.lastUrl()).toEqual('https://api.guildwars2.com/v2/test?v=schema&ids=1,2')
expect(entry1).toEqual(content[0])
expect(entry2).toEqual(content[1])
})

it('supports batching from many', async () => {
let content = [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }, { id: 3, name: 'bar' }]
endpoint.isBulk = true
endpoint.url = '/v2/test'
// endpoint.enableAutoBatch(10)
fetchMock.addResponse(content)

let [entry1, entry2] = await Promise.all([endpoint.many([1,2]), endpoint.many([2,3])])
expect(fetchMock.lastUrl()).toEqual('https://api.guildwars2.com/v2/test?v=schema&ids=1,2,3')
expect(entry1).toEqual([content[0],content[1]])
expect(entry2).toEqual([content[1],content[2]])
})

it('only batches requests during the interval', async () => {
let content1 = [{ id: 1, name: 'foo' }]
let content2 = [{ id: 2, name: 'bar' }]
endpoint.isBulk = true
endpoint.url = '/v2/test'
// endpoint.enableAutoBatch(10)
fetchMock.addResponse(content1)
fetchMock.addResponse(content2)

let [entry1, entry2] = await Promise.all([
endpoint.get(1),
new Promise((resolve) => {setTimeout(() => {resolve(endpoint.get(2))}, interval+1)})
])
expect(fetchMock.urls()).toEqual([
'https://api.guildwars2.com/v2/test?v=schema&ids=1',
'https://api.guildwars2.com/v2/test?v=schema&ids=2'
])
expect(entry1).toEqual(content1[0])
expect(entry2).toEqual(content2[0])

})
})

describe('get', () => {
it('support for bulk expanding', async () => {
let content = { id: 1, name: 'foo' }
Expand Down