Fix Beds24 token race condition.

Deduplicate in-flight /authentication/token requests per house so
that concurrent page loads do not each trigger a separate token
refresh, which was exhausting the Beds24 rate limit on first load.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Markus
2026-08-24 21:40:24 +02:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 2b24334bfa
commit 74592a9e14
+31 -16
View File
@@ -64,6 +64,7 @@ function getRefreshToken(houseId: string): string | undefined {
} }
const accessTokenCache = new Map<string, { token: string; expiresAt: number }>(); const accessTokenCache = new Map<string, { token: string; expiresAt: number }>();
const pendingTokenRequests = new Map<string, Promise<string>>();
async function getAccessToken(houseId: string): Promise<string> { async function getAccessToken(houseId: string): Promise<string> {
const refreshToken = getRefreshToken(houseId); const refreshToken = getRefreshToken(houseId);
@@ -78,26 +79,40 @@ async function getAccessToken(houseId: string): Promise<string> {
return cached.token; return cached.token;
} }
const response = await fetch(`${BEDS24_API_BASE}/authentication/token`, { const inFlight = pendingTokenRequests.get(houseId);
method: "GET", if (inFlight) {
headers: { refreshToken }, return inFlight;
});
if (!response.ok) {
throw new Error(
`Beds24 authentication error for house ${houseId}: ${response.status} ${response.statusText}`
);
} }
const data: { token: string; expiresIn: number } = await response.json(); const tokenPromise = (async () => {
try {
const response = await fetch(`${BEDS24_API_BASE}/authentication/token`, {
method: "GET",
headers: { refreshToken },
});
// Refresh a minute early to avoid using a token that expires mid-request. if (!response.ok) {
accessTokenCache.set(houseId, { throw new Error(
token: data.token, `Beds24 authentication error for house ${houseId}: ${response.status} ${response.statusText}`
expiresAt: Date.now() + Math.max(data.expiresIn - 60, 30) * 1000, );
}); }
return data.token; const data: { token: string; expiresIn: number } = await response.json();
// Refresh a minute early to avoid using a token that expires mid-request.
accessTokenCache.set(houseId, {
token: data.token,
expiresAt: Date.now() + Math.max(data.expiresIn - 60, 30) * 1000,
});
return data.token;
} finally {
pendingTokenRequests.delete(houseId);
}
})();
pendingTokenRequests.set(houseId, tokenPromise);
return tokenPromise;
} }
async function beds24Get<T>( async function beds24Get<T>(