From 1b31b8d7920dab5525eb1a6ab468e272fe7ed788 Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 17 Oct 2025 20:25:26 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #29 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: undefined --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..28dc16a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: undefined +Your prepared branch: issue-29-9e62b3a8 +Your prepared working directory: /tmp/gh-issue-solver-1760721923556 + +Proceed. \ No newline at end of file From 8c980eb8ccfd7c77bf6fd0afc7043a37bfa5b0e3 Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 17 Oct 2025 20:30:33 +0300 Subject: [PATCH 2/3] Add retry logic with exponential backoff for VK API calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a robust retry mechanism to handle transient network errors (ECONNRESET, ETIMEDOUT, ECONNREFUSED, ENOTFOUND) when making API calls to VK's friends.getRequests endpoint. Changes: - Add withRetry utility function in utils.js with exponential backoff - Wrap all friends.getRequests calls with retry logic - Default to 3 retries with 1s initial delay, 30s max delay - Provide detailed logging for retry attempts Fixes #29 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- experiments/get-friend-requests.js | 6 ++- friends.js | 6 ++- reject-deactivated-friend-requests.js | 6 ++- requests.js | 6 ++- triggers/accept-friend-requests.js | 8 ++-- triggers/delete-outgoing-requests.js | 6 ++- triggers/react-to-cancelled-friendships.js | 6 ++- utils.js | 54 ++++++++++++++++++++++ 8 files changed, 83 insertions(+), 15 deletions(-) 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, From 6a4a220146b48f259be68462bcb6e733f0df7923 Mon Sep 17 00:00:00 2001 From: konard Date: Fri, 17 Oct 2025 20:31:40 +0300 Subject: [PATCH 3/3] Revert "Initial commit with task details for issue #29" This reverts commit 1b31b8d7920dab5525eb1a6ab468e272fe7ed788. --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 28dc16a..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: undefined -Your prepared branch: issue-29-9e62b3a8 -Your prepared working directory: /tmp/gh-issue-solver-1760721923556 - -Proceed. \ No newline at end of file