fix(HiFi.ts): ensure only one token is fetched
If multiple calls to the HiFi methods were called at once, you could potentially have ended up with multiple simultaneous token api calls
This commit is contained in:
parent
a385cb558a
commit
5ac4d23199
1 changed files with 34 additions and 27 deletions
61
js/HiFi.ts
61
js/HiFi.ts
|
|
@ -20,8 +20,9 @@ export class TidalResponse extends Response {
|
|||
|
||||
export class HiFiClient {
|
||||
private static token: string | null;
|
||||
private countryCode: string;
|
||||
private static appTokenExpiry = 0;
|
||||
private static tokenPromise: Promise<string> | null = null;
|
||||
private countryCode: string;
|
||||
private static albumTracksMax = 20;
|
||||
private static albumTracksActive = 0;
|
||||
private static albumTracksQueue: Array<() => void> = [];
|
||||
|
|
@ -35,7 +36,7 @@ export class HiFiClient {
|
|||
return u.toString();
|
||||
}
|
||||
|
||||
private encodeBasic(id: string, secret: string) {
|
||||
private static encodeBasic(id: string, secret: string) {
|
||||
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
|
||||
return window.btoa(`${id}:${secret}`);
|
||||
}
|
||||
|
|
@ -43,35 +44,41 @@ export class HiFiClient {
|
|||
return Buffer.from(`${id}:${secret}`).toString('base64');
|
||||
}
|
||||
|
||||
private async fetchAppToken(signal: AbortSignal = new AbortController().signal): Promise<string> {
|
||||
private static async fetchAppToken(signal: AbortSignal = new AbortController().signal) {
|
||||
const now = Date.now();
|
||||
if (HiFiClient.token && now < HiFiClient.appTokenExpiry) return HiFiClient.token;
|
||||
|
||||
const res = await fetch('https://auth.tidal.com/v1/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
authorization: `Basic ${this.encodeBasic(CLIENT_ID, CLIENT_SECRET)}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
return await (HiFiClient.tokenPromise ??= (async () => {
|
||||
try {
|
||||
const res = await fetch('https://auth.tidal.com/v1/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
authorization: `Basic ${this.encodeBasic(CLIENT_ID, CLIENT_SECRET)}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => '');
|
||||
throw new Error(`Failed to obtain app token: ${res.status} ${txt}`);
|
||||
}
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => '');
|
||||
throw new Error(`Failed to obtain app token: ${res.status} ${txt}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const token = json.access_token;
|
||||
const expires_in = json.expires_in ?? 3600;
|
||||
HiFiClient.token = token;
|
||||
HiFiClient.appTokenExpiry = Date.now() + expires_in * 1000 - 60_000;
|
||||
return token;
|
||||
const json = await res.json();
|
||||
const token = json.access_token;
|
||||
const expires_in = json.expires_in ?? 3600;
|
||||
HiFiClient.token = token;
|
||||
HiFiClient.appTokenExpiry = Date.now() + expires_in * 1000 - 60_000;
|
||||
return token;
|
||||
} finally {
|
||||
HiFiClient.tokenPromise = null;
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
constructor(countryCode = 'US') {
|
||||
|
|
@ -81,7 +88,7 @@ export class HiFiClient {
|
|||
private async fetchJson(url: string, params?: Params, signal: AbortSignal = new AbortController().signal) {
|
||||
const final = HiFiClient.buildUrl(url, params);
|
||||
const res = await fetch(final, {
|
||||
headers: { authorization: `Bearer ${await this.fetchAppToken(signal)}` },
|
||||
headers: { authorization: `Bearer ${await HiFiClient.fetchAppToken(signal)}` },
|
||||
signal,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue