Skip to content

Commit f882eba

Browse files
Add request deduplication for Bungie API client (#108)
Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com> Co-authored-by: Owen <98496129+owens1127@users.noreply.github.com>
1 parent b89d25f commit f882eba

1 file changed

Lines changed: 68 additions & 45 deletions

File tree

src/integrations/bungie/client.ts

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,68 +13,91 @@ const inMemoryCache = new Map<
1313
}
1414
>()
1515

16+
const inFlightRequests = new Map<string, Promise<unknown>>()
17+
1618
export const bungiePlatformHttp = (opts: { ttl: number }) => ({
1719
fetch: async <T>(config: BungieFetchConfig) => {
1820
const cacheKey = config.url.toString()
21+
22+
// Check cache first
1923
if (inMemoryCache.has(cacheKey)) {
2024
return inMemoryCache.get(cacheKey)!.data as T
2125
}
2226

23-
const apiKey = process.env.BUNGIE_API_KEY
24-
if (!apiKey) {
25-
throw new Error("Missing Bungie API Key")
27+
// Check if request is already in-flight
28+
if (inFlightRequests.has(cacheKey)) {
29+
return inFlightRequests.get(cacheKey)! as Promise<T>
2630
}
2731

28-
const headers = new Headers(config.headers)
29-
headers.append("X-API-KEY", apiKey)
30-
headers.append("User-Agent", `RaidHub-API/${apiVersion} (contact=admin@raidhub.io)`)
32+
// Create the request promise
33+
const requestPromise = (async () => {
34+
const apiKey = process.env.BUNGIE_API_KEY
35+
if (!apiKey) {
36+
throw new Error("Missing Bungie API Key")
37+
}
3138

32-
const response = await fetch(config.url, {
33-
method: config.method,
34-
headers: headers,
35-
body: config.body
36-
})
39+
const headers = new Headers(config.headers)
40+
headers.append("X-API-KEY", apiKey)
41+
headers.append("User-Agent", `RaidHub-API/${apiVersion} (contact=admin@raidhub.io)`)
3742

38-
if (response.headers.get("content-type")?.includes("application/json")) {
39-
const data = (await response.json()) as BungieNetResponse<unknown>
43+
const response = await fetch(config.url, {
44+
method: config.method,
45+
headers: headers,
46+
body: config.body
47+
})
4048

41-
if (!("ErrorCode" in data)) {
42-
throw new Error("Invalid JSON response", {
43-
cause: data
44-
})
45-
}
49+
if (response.headers.get("content-type")?.includes("application/json")) {
50+
const data = (await response.json()) as BungieNetResponse<unknown>
4651

47-
if (data.ErrorCode !== 1) {
48-
throw new BungieApiError({
49-
cause: data,
50-
url: config.url
51-
})
52-
} else {
53-
// This is needed because we could have fired multiple requests to the same URL simultaneously
54-
if (inMemoryCache.has(cacheKey)) {
55-
clearTimeout(inMemoryCache.get(cacheKey)!.timer)
52+
if (!("ErrorCode" in data)) {
53+
throw new Error("Invalid JSON response", {
54+
cause: data
55+
})
5656
}
57-
inMemoryCache.set(cacheKey, {
58-
data,
59-
timer: setTimeout(() => {
60-
inMemoryCache.delete(cacheKey)
61-
}, opts.ttl)
62-
})
6357

64-
return data as T
65-
}
66-
} else {
67-
const body = await response.text()
68-
const match = body.match(htmlRegex)
69-
if (match) {
70-
throw new Error(`Invalid HTML response (${response.status}): ${match[1]}`, {
71-
cause: body
72-
})
58+
if (data.ErrorCode !== 1) {
59+
throw new BungieApiError({
60+
cause: data,
61+
url: config.url
62+
})
63+
} else {
64+
// This is needed because we could have fired multiple requests to the same URL simultaneously
65+
if (inMemoryCache.has(cacheKey)) {
66+
clearTimeout(inMemoryCache.get(cacheKey)!.timer)
67+
}
68+
inMemoryCache.set(cacheKey, {
69+
data,
70+
timer: setTimeout(() => {
71+
inMemoryCache.delete(cacheKey)
72+
}, opts.ttl)
73+
})
74+
75+
return data as T
76+
}
7377
} else {
74-
throw new Error(`Invalid response (${response.status}): ${response.statusText}`, {
75-
cause: body
76-
})
78+
const body = await response.text()
79+
const match = body.match(htmlRegex)
80+
if (match) {
81+
throw new Error(`Invalid HTML response (${response.status}): ${match[1]}`, {
82+
cause: body
83+
})
84+
} else {
85+
throw new Error(`Invalid response (${response.status}): ${response.statusText}`, {
86+
cause: body
87+
})
88+
}
7789
}
90+
})()
91+
92+
// Track the in-flight request
93+
inFlightRequests.set(cacheKey, requestPromise)
94+
95+
try {
96+
const result = await requestPromise
97+
return result
98+
} finally {
99+
// Clean up in-flight request tracker
100+
inFlightRequests.delete(cacheKey)
78101
}
79102
}
80103
})

0 commit comments

Comments
 (0)