From 19ab4204fff606746a6208a8e5d80ca83099bf42 Mon Sep 17 00:00:00 2001 From: Ryan S Date: Tue, 16 Jul 2024 18:01:17 -0300 Subject: [PATCH 1/2] feat: throttled Put request and add retry + delay --- src/api.ts | 59 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 16 deletions(-) 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); } From be0fa8574d3cf5d97318f925faffcfb8d416a859 Mon Sep 17 00:00:00 2001 From: Ryan S Date: Wed, 17 Jul 2024 10:21:57 -0300 Subject: [PATCH 2/2] docs: added a debug and change comment --- src/api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index d5bd8c6..2eaf992 100644 --- a/src/api.ts +++ b/src/api.ts @@ -31,6 +31,7 @@ async function retryRequest(fn: () => Promise, retries: numbe if (retries === 0 || !(error instanceof RequestError && error.response?.statusCode === 429)) { throw error; } + debug('Rate limited, retrying in %s ms', retryDelay); await delay(retryDelay); return retryRequest(fn, retries - 1); } @@ -96,7 +97,7 @@ export async function updateRemoteArticle(article: Article, devtoKey: string): P try { const markdown = matter.stringify(article, article.data, { lineWidth: -1 } as any); const { id } = article.data; - // Throttle API calls in case of article creation + // Throttle API calls in case of article creation or update const get = id ? throttledPutForUpdate : throttledPostForCreate; const result = await get(`${apiUrl}/articles${id ? `/${id}` : ''}`, { headers: { 'api-key': devtoKey },