diff --git a/experiments/get-friend-requests.js b/experiments/get-friend-requests.js index a352b50..386a4d2 100644 --- a/experiments/get-friend-requests.js +++ b/experiments/get-friend-requests.js @@ -1,10 +1,12 @@ const { VK } = require('vk-io'); -const { getToken } = require('../utils'); +const { getToken, withRetry } = require('../utils'); const token = getToken(); const vk = new VK({ token }); (async () => { const maxFriendRequestsCount = 23; - const requests = await vk.api.friends.getRequests({ count: maxFriendRequestsCount, sort: 1 }); // , extended: true + const requests = await withRetry(() => + vk.api.friends.getRequests({ count: maxFriendRequestsCount, sort: 1 }) + ); // , extended: true console.log(JSON.stringify(requests, null, 2)); })(); diff --git a/friends.js b/friends.js index a8d8c04..4f4c8d5 100644 --- a/friends.js +++ b/friends.js @@ -1,5 +1,5 @@ const { VK } = require('vk-io'); -const { sleep, getToken, second, ms } = require('./utils'); +const { sleep, getToken, second, ms, withRetry } = require('./utils'); const token = getToken(); const vk = new VK({ token }); @@ -15,7 +15,9 @@ const requestsLimit = 10000; // Maximum number of requests you expect const requestsSegmentSize = 1000; // Number of requests fetched per segment async function fetchRequests(segment, offset) { - const req = await vk.api.friends.getRequests({ out: 1, count: segment, offset: offset }); + const req = await withRetry(() => + vk.api.friends.getRequests({ out: 1, count: segment, offset: offset }) + ); return req || []; } diff --git a/reject-deactivated-friend-requests.js b/reject-deactivated-friend-requests.js index f2cf315..2de2d72 100644 --- a/reject-deactivated-friend-requests.js +++ b/reject-deactivated-friend-requests.js @@ -1,12 +1,14 @@ const { VK } = require('vk-io'); -const { sleep, getToken, second, ms } = require('./utils'); +const { sleep, getToken, second, ms, withRetry } = require('./utils'); const token = getToken(); const vk = new VK({ token }); const rejectDeactivatedFriendRequests = async () => { try { const maxFriendRequestsCount = 1000; - const requests = await vk.api.friends.getRequests({ count: maxFriendRequestsCount, sort: 1 }); + const requests = await withRetry(() => + vk.api.friends.getRequests({ count: maxFriendRequestsCount, sort: 1 }) + ); for (let userId of requests.items) { try { diff --git a/requests.js b/requests.js index c67c1d2..415aab3 100644 --- a/requests.js +++ b/requests.js @@ -1,5 +1,5 @@ const { VK } = require('vk-io'); -const { getToken } = require('./utils'); +const { getToken, withRetry } = require('./utils'); const token = getToken(); const vk = new VK({ token }); @@ -7,7 +7,9 @@ const requestsLimit = 10000; // Maximum number of requests you expect const requestsSegmentSize = 1000; // Number of requests fetched per segment async function fetchRequests(segment, offset) { - const req = await vk.api.friends.getRequests({ out: 1, count: segment, offset: offset }); + const req = await withRetry(() => + vk.api.friends.getRequests({ out: 1, count: segment, offset: offset }) + ); return req || []; } diff --git a/triggers/accept-friend-requests.js b/triggers/accept-friend-requests.js index 3a37b62..9113436 100644 --- a/triggers/accept-friend-requests.js +++ b/triggers/accept-friend-requests.js @@ -1,5 +1,5 @@ const { getAllFriends, loadAllFriends } = require('../friends-cache'); -const { sleep, priorityFriendIds, second, minute, ms } = require('../utils'); +const { sleep, priorityFriendIds, second, minute, ms, withRetry } = require('../utils'); const sortByMutuals = { sort: 1 }; const maxFriends = 10000; @@ -27,7 +27,7 @@ async function acceptFriendRequests({ vk }) { console.log(`Friend request is sent to priority friend with id ${friendId}.`); } catch (error) { if (error.code === 177) { // APIError: Code №177 - Cannot add this user to friends as user not found - console.log(`Could not send friend request to priority friend with id ${friendId}, because this friend is not found.`); + console.log(`Could not accept ${friendId} friend request, because this friend is not found.`); } else if (error.code === 242) { // APIError: Code №242 - Too many friends: friends count exceeded console.log(`Could not send friend request to priority friend with id ${friendId}, because friends count (10000) exceeded.`); break; @@ -44,7 +44,9 @@ async function acceptFriendRequests({ vk }) { } const maxFriendRequestsCount = 23; - const requests = await vk.api.friends.getRequests({ count: maxFriendRequestsCount, ...sortByMutuals }); + const requests = await withRetry(() => + vk.api.friends.getRequests({ count: maxFriendRequestsCount, ...sortByMutuals }) + ); await sleep((2 * second) / ms); if (requests?.items?.length <= 0) { console.log('No incoming friend requests to be accepted.'); diff --git a/triggers/delete-outgoing-requests.js b/triggers/delete-outgoing-requests.js index 25873b2..e717525 100644 --- a/triggers/delete-outgoing-requests.js +++ b/triggers/delete-outgoing-requests.js @@ -1,4 +1,4 @@ -const { sleep, priorityFriendIds, second, ms } = require('../utils'); +const { sleep, priorityFriendIds, second, ms, withRetry } = require('../utils'); async function deleteOutgoingFriendRequests(context) { try { @@ -6,7 +6,9 @@ async function deleteOutgoingFriendRequests(context) { if (count <= 0) { return; } - const requests = await context.vk.api.friends.getRequests({ count, out: 1, need_viewed: 1 }); + const requests = await withRetry(() => + context.vk.api.friends.getRequests({ count, out: 1, need_viewed: 1 }) + ); if (requests.items.length <= 0) { console.log('No outgoing friend requests to be deleted'); return requests; diff --git a/triggers/react-to-cancelled-friendships.js b/triggers/react-to-cancelled-friendships.js index 23a5bc0..a318d0d 100644 --- a/triggers/react-to-cancelled-friendships.js +++ b/triggers/react-to-cancelled-friendships.js @@ -1,5 +1,5 @@ const { DateTime } = require('luxon'); -const { getRandomElement, sleep, second, ms } = require('../utils'); +const { getRandomElement, sleep, second, ms, withRetry } = require('../utils'); const { trigger: greetingTrigger } = require('./greeting'); const { enqueueMessage } = require('../outgoing-messages'); const { setConversation } = require('../friends-conversations-cache'); @@ -14,7 +14,9 @@ async function reactToCancelledFriendships(context) { if (count <= 0) { return; } - const requests = await context.vk.api.friends.getRequests({ count, out: 1, need_viewed: 1 }); + const requests = await withRetry(() => + context.vk.api.friends.getRequests({ count, out: 1, need_viewed: 1 }) + ); await sleep((3 * second) / ms); if (requests.items.length <= 0) { console.log('No cancelled friendships to react to.'); diff --git a/utils.js b/utils.js index e7fea65..5d0da93 100644 --- a/utils.js +++ b/utils.js @@ -178,6 +178,59 @@ async function executeTrigger(trigger, context) { } } +/** + * Executes an async function with retry logic and exponential backoff + * @param {Function} fn - The async function to execute + * @param {Object} options - Retry options + * @param {number} options.maxRetries - Maximum number of retry attempts (default: 3) + * @param {number} options.initialDelay - Initial delay in milliseconds (default: 1000) + * @param {number} options.maxDelay - Maximum delay in milliseconds (default: 30000) + * @param {number} options.backoffMultiplier - Multiplier for exponential backoff (default: 2) + * @param {Array} options.retryableErrors - List of error codes to retry (default: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND']) + * @returns {Promise} The result of the function execution + */ +async function withRetry(fn, options = {}) { + const { + maxRetries = 3, + initialDelay = 1000, + maxDelay = 30000, + backoffMultiplier = 2, + retryableErrors = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'] + } = options; + + let lastError; + let delay = initialDelay; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + + // Check if error is retryable + const isRetryable = retryableErrors.includes(error.code) || + retryableErrors.includes(error.errno) || + (error.type === 'system' && retryableErrors.includes(error.code)); + + if (!isRetryable || attempt === maxRetries) { + throw error; + } + + // Log retry attempt + console.warn(`Attempt ${attempt + 1}/${maxRetries + 1} failed with ${error.code || error.errno}: ${error.message}`); + console.log(`Retrying in ${delay}ms...`); + + // Wait before retrying + await new Promise(resolve => setTimeout(resolve, delay)); + + // Exponential backoff with max delay cap + delay = Math.min(delay * backoffMultiplier, maxDelay); + } + } + + throw lastError; +} + module.exports = { getToken, getRandomElement, @@ -190,6 +243,7 @@ module.exports = { readJsonSync, saveTextSync, saveJsonSync, + withRetry, app, ...timeUnits, priorityFriendIds,