mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Add retry logic with exponential backoff to AudibleService
Introduces a fetchWithRetry method to handle network errors and rate limiting (503, 429) with exponential backoff in AudibleService. Updates getPopularAudiobooks and getNewReleases to use this retry logic and improves error handling to stop pagination on errors but return collected results. Test cases are updated to use non-retryable errors (404) for more accurate coverage.
This commit is contained in:
@@ -114,23 +114,64 @@ export class AudibleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch with retry logic and exponential backoff
|
||||||
|
* Retries on network errors and rate limiting (503, 429)
|
||||||
|
*/
|
||||||
|
private async fetchWithRetry(
|
||||||
|
url: string,
|
||||||
|
config: any = {},
|
||||||
|
maxRetries: number = 3
|
||||||
|
): Promise<any> {
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await this.client.get(url, config);
|
||||||
|
} catch (error: any) {
|
||||||
|
lastError = error;
|
||||||
|
const status = error.response?.status;
|
||||||
|
const isRetryable = !status || status === 503 || status === 429 || status >= 500;
|
||||||
|
|
||||||
|
// Don't retry on 404, 403, etc.
|
||||||
|
if (!isRetryable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't retry on last attempt
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponential backoff: 2^attempt * 1000ms (1s, 2s, 4s, 8s...)
|
||||||
|
const backoffMs = Math.pow(2, attempt) * 1000;
|
||||||
|
logger.info(` Request failed (${status || 'network error'}), retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries})...`);
|
||||||
|
|
||||||
|
await this.delay(backoffMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All retries exhausted
|
||||||
|
throw lastError || new Error('Request failed after retries');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get popular audiobooks from best sellers (with pagination support)
|
* Get popular audiobooks from best sellers (with pagination support)
|
||||||
*/
|
*/
|
||||||
async getPopularAudiobooks(limit: number = 20): Promise<AudibleAudiobook[]> {
|
async getPopularAudiobooks(limit: number = 20): Promise<AudibleAudiobook[]> {
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
|
|
||||||
try {
|
logger.info(` Fetching popular audiobooks (limit: ${limit})...`);
|
||||||
logger.info(` Fetching popular audiobooks (limit: ${limit})...`);
|
|
||||||
|
|
||||||
const audiobooks: AudibleAudiobook[] = [];
|
const audiobooks: AudibleAudiobook[] = [];
|
||||||
let page = 1;
|
let page = 1;
|
||||||
const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page
|
const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page
|
||||||
|
|
||||||
while (audiobooks.length < limit && page <= maxPages) {
|
while (audiobooks.length < limit && page <= maxPages) {
|
||||||
|
try {
|
||||||
logger.info(` Fetching page ${page}/${maxPages}...`);
|
logger.info(` Fetching page ${page}/${maxPages}...`);
|
||||||
|
|
||||||
const response = await this.client.get('/adblbestsellers', {
|
const response = await this.fetchWithRetry('/adblbestsellers', {
|
||||||
params: page > 1 ? { page } : {},
|
params: page > 1 ? { page } : {},
|
||||||
});
|
});
|
||||||
const $ = cheerio.load(response.data);
|
const $ = cheerio.load(response.data);
|
||||||
@@ -192,14 +233,18 @@ export class AudibleService {
|
|||||||
if (page <= maxPages && audiobooks.length < limit) {
|
if (page <= maxPages && audiobooks.length < limit) {
|
||||||
await this.delay(1500);
|
await this.delay(1500);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to fetch page ${page} of popular audiobooks`, {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
collectedSoFar: audiobooks.length
|
||||||
|
});
|
||||||
|
// Stop pagination on error, but return what we collected
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(` Found ${audiobooks.length} popular audiobooks across ${page} pages`);
|
|
||||||
return audiobooks;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to fetch popular audiobooks', { error: error instanceof Error ? error.message : String(error) });
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(` Found ${audiobooks.length} popular audiobooks across ${page - 1} pages`);
|
||||||
|
return audiobooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,17 +253,17 @@ export class AudibleService {
|
|||||||
async getNewReleases(limit: number = 20): Promise<AudibleAudiobook[]> {
|
async getNewReleases(limit: number = 20): Promise<AudibleAudiobook[]> {
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
|
|
||||||
try {
|
logger.info(` Fetching new releases (limit: ${limit})...`);
|
||||||
logger.info(` Fetching new releases (limit: ${limit})...`);
|
|
||||||
|
|
||||||
const audiobooks: AudibleAudiobook[] = [];
|
const audiobooks: AudibleAudiobook[] = [];
|
||||||
let page = 1;
|
let page = 1;
|
||||||
const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page
|
const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page
|
||||||
|
|
||||||
while (audiobooks.length < limit && page <= maxPages) {
|
while (audiobooks.length < limit && page <= maxPages) {
|
||||||
|
try {
|
||||||
logger.info(` Fetching page ${page}/${maxPages}...`);
|
logger.info(` Fetching page ${page}/${maxPages}...`);
|
||||||
|
|
||||||
const response = await this.client.get('/newreleases', {
|
const response = await this.fetchWithRetry('/newreleases', {
|
||||||
params: page > 1 ? { page } : {},
|
params: page > 1 ? { page } : {},
|
||||||
});
|
});
|
||||||
const $ = cheerio.load(response.data);
|
const $ = cheerio.load(response.data);
|
||||||
@@ -279,14 +324,18 @@ export class AudibleService {
|
|||||||
if (page <= maxPages && audiobooks.length < limit) {
|
if (page <= maxPages && audiobooks.length < limit) {
|
||||||
await this.delay(1500);
|
await this.delay(1500);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to fetch page ${page} of new releases`, {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
collectedSoFar: audiobooks.length
|
||||||
|
});
|
||||||
|
// Stop pagination on error, but return what we collected
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(` Found ${audiobooks.length} new releases across ${page} pages`);
|
|
||||||
return audiobooks;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to fetch new releases', { error: error instanceof Error ? error.message : String(error) });
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(` Found ${audiobooks.length} new releases across ${page - 1} pages`);
|
||||||
|
return audiobooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -355,7 +355,10 @@ describe('AudibleService', () => {
|
|||||||
|
|
||||||
it('returns empty popular audiobooks on errors', async () => {
|
it('returns empty popular audiobooks on errors', async () => {
|
||||||
configServiceMock.getAudibleRegion.mockResolvedValue('us');
|
configServiceMock.getAudibleRegion.mockResolvedValue('us');
|
||||||
clientMock.get.mockRejectedValue(new Error('boom'));
|
// Use 404 error which is not retryable
|
||||||
|
const error: any = new Error('Not Found');
|
||||||
|
error.response = { status: 404 };
|
||||||
|
clientMock.get.mockRejectedValue(error);
|
||||||
|
|
||||||
const service = new AudibleService();
|
const service = new AudibleService();
|
||||||
const results = await service.getPopularAudiobooks(5);
|
const results = await service.getPopularAudiobooks(5);
|
||||||
@@ -365,7 +368,10 @@ describe('AudibleService', () => {
|
|||||||
|
|
||||||
it('returns empty new releases on errors', async () => {
|
it('returns empty new releases on errors', async () => {
|
||||||
configServiceMock.getAudibleRegion.mockResolvedValue('us');
|
configServiceMock.getAudibleRegion.mockResolvedValue('us');
|
||||||
clientMock.get.mockRejectedValue(new Error('boom'));
|
// Use 404 error which is not retryable
|
||||||
|
const error: any = new Error('Not Found');
|
||||||
|
error.response = { status: 404 };
|
||||||
|
clientMock.get.mockRejectedValue(error);
|
||||||
|
|
||||||
const service = new AudibleService();
|
const service = new AudibleService();
|
||||||
const results = await service.getNewReleases(5);
|
const results = await service.getNewReleases(5);
|
||||||
|
|||||||
Reference in New Issue
Block a user