diff --git a/src/api.ts b/src/api.ts index 0af7971..d5bd8c6 100644 --- a/src/api.ts +++ b/src/api.ts @@ -7,12 +7,35 @@ import { type Article, type RemoteArticleData, type ArticleStats } from './model const debug = Debug('devto'); const apiUrl = 'https://dev.to/api'; const paginationLimit = 1000; +const maxRetries = 3; +const retryDelay = 1000; // 1 second delay before retrying // There's a limit of 10 articles created each 30 seconds by the same user, // so we need to throttle the API calls in that case. // The insane type casting is due to typing issues with p-throttle. const throttledPostForCreate = pThrottle({ limit: 10, interval: 30_500 })(got.post) as any as Got['post']; +// There's a limit of 30 requests each 30 seconds by the same user, so we need to throttle the API calls in that case too. +const throttledPutForUpdate = pThrottle({ limit: 30, interval: 30_500 })( + async (url: string, options: any) => got.put(url, options) +) as any as Got['put']; + +async function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function retryRequest(fn: () => Promise, retries: number): Promise { + try { + return await fn(); + } catch (error) { + if (retries === 0 || !(error instanceof RequestError && error.response?.statusCode === 429)) { + throw error; + } + await delay(retryDelay); + return retryRequest(fn, retries - 1); + } +} + export async function getAllArticles(devtoKey: string): Promise { try { const articles = []; @@ -69,22 +92,26 @@ export async function getLastArticlesStats(devtoKey: string, number: number): Pr } export async function updateRemoteArticle(article: Article, devtoKey: string): Promise { - try { - const markdown = matter.stringify(article, article.data, { lineWidth: -1 } as any); - const { id } = article.data; - // Throttle API calls in case of article creation - const get = id ? got.put : throttledPostForCreate; - const result = await get(`${apiUrl}/articles${id ? `/${id}` : ''}`, { - headers: { 'api-key': devtoKey }, - json: { article: { title: article.data.title, body_markdown: markdown } }, - responseType: 'json' - }); - return result.body as RemoteArticleData; - } catch (error) { - if (error instanceof RequestError && error.response) { - debug('Debug infos: %O', error.response.body); + const update = async (): Promise => { + try { + const markdown = matter.stringify(article, article.data, { lineWidth: -1 } as any); + const { id } = article.data; + // Throttle API calls in case of article creation + const get = id ? throttledPutForUpdate : throttledPostForCreate; + const result = await get(`${apiUrl}/articles${id ? `/${id}` : ''}`, { + headers: { 'api-key': devtoKey }, + json: { article: { title: article.data.title, body_markdown: markdown } }, + responseType: 'json' + }); + return result.body as RemoteArticleData; + } catch (error) { + if (error instanceof RequestError && error.response) { + debug('Debug infos: %O', error.response.body); + } + + throw error; } + }; - throw error; - } + return retryRequest(update, maxRetries); }