Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ node_modules
.env
.DS_Store
lol-cli
lol-cli-macos-arm64
lol-cli-win-x64.exe
.last_search.json
Binary file added docs/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 98 additions & 52 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
const axios = require('axios');

// API stats tracking
const apiStats = {
pending: 0,
};

const getApiStats = () => ({ ...apiStats });

const RIOT_API_KEY = process.env.RIOT_API_KEY;

if (!RIOT_API_KEY) {
Expand All @@ -14,19 +21,59 @@ api.interceptors.request.use((config) => {
return config;
});

const retryWithBackoff = async (fn, maxRetries = 5, baseDelay = 1000) => {
apiStats.pending++;
try {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Only retry on rate limit (429)
if (error.response?.status !== 429) {
handleApiError(error); // Transform and re-throw non-429 errors
}

// Use Retry-After header if present, otherwise exponential backoff with jitter
const retryAfter = error.response?.headers?.['retry-after'];
let delay;
if (retryAfter) {
// Parse Retry-After (could be seconds or HTTP-date)
// Cap at 30 seconds to prevent excessive waits from malformed headers
const parsed = parseInt(retryAfter, 10);
const rawDelay = isNaN(parsed) ? Math.max(1000, new Date(retryAfter) - Date.now()) : parsed * 1000;
delay = Math.min(30000, rawDelay);
} else {
// Exponential backoff with jitter (50-100% of calculated delay)
delay = baseDelay * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5);
}

await new Promise(res => setTimeout(res, delay));
}
}
throw lastError || new Error('Max retries exceeded');
} finally {
apiStats.pending--;
}
};

const handleApiError = (error) => {
if (error.response) {
const { status, data } = error.response;
if (status === 429) {
throw error; // Re-throw original error for retry logic
}
if (status === 403) {
throw new Error('Forbidden: Invalid API Key or insufficient permissions.');
}
if (status === 404) {
throw new Error('Not Found: The requested resource was not found.');
}
throw new Error(data.status ? data.status.message : 'An API error occurred.');
} else {
throw new Error('An unexpected error occurred.');
}
// Re-throw network errors (no response) to preserve original error info
throw error;
};

const getRegionalPlatform = (region) => {
Expand All @@ -43,14 +90,12 @@ const getRegionalPlatform = (region) => {
};

const getSummonerByPuuid = async (region, puuid) => {
try {
return retryWithBackoff(async () => {
const response = await api.get(
`https://${region}.api.riotgames.com/lol/summoner/v4/summoners/by-puuid/${puuid}`
);
return response.data;
} catch (error) {
handleApiError(error);
}
});
};

const getSummonerDataByRiotId = async (region, riotId) => {
Expand All @@ -64,15 +109,12 @@ const getSummonerDataByRiotId = async (region, riotId) => {
const accountRegion = getRegionalPlatform(region);
const accountApiUrl = `https://${accountRegion}.api.riotgames.com`;

let puuid;
try {
const response = await api.get(
`${accountApiUrl}/riot/account/v1/accounts/by-riot-id/${encodeURIComponent(gameName)}/${tagLine}`
const response = await retryWithBackoff(async () => {
return api.get(
`${accountApiUrl}/riot/account/v1/accounts/by-riot-id/${encodeURIComponent(gameName)}/${encodeURIComponent(tagLine)}`
);
puuid = response.data.puuid;
} catch (error) {
handleApiError(error);
}
});
const puuid = response?.data?.puuid;

if (!puuid) {
throw new Error('Could not retrieve PUUID for the given Riot ID.');
Expand All @@ -84,72 +126,74 @@ const getSummonerDataByRiotId = async (region, riotId) => {


const getMatchHistory = async (region, puuid) => {
try {
return retryWithBackoff(async () => {
const response = await api.get(
`https://${getRegionalPlatform(region)}.api.riotgames.com/lol/match/v5/matches/by-puuid/${puuid}/ids?start=0&count=10`
);
return response.data;
} catch (error) {
handleApiError(error);
}
});
};

const getMatchDetails = async (region, matchId) => {
try {
return retryWithBackoff(async () => {
const response = await api.get(
`https://${getRegionalPlatform(region)}.api.riotgames.com/lol/match/v5/matches/${matchId}`
);
return response.data;
} catch (error) {
handleApiError(error);
}
});
};

const getMatchTimeline = async (region, matchId) => {
try {
return retryWithBackoff(async () => {
const response = await api.get(
`https://${getRegionalPlatform(region)}.api.riotgames.com/lol/match/v5/matches/${matchId}/timeline`
);
return response.data;
} catch (error) {
handleApiError(error);
}
});
};

const getRankedData = async (region, puuid) => {
try {
const response = await api.get(
`https://${region}.api.riotgames.com/lol/league/v4/entries/by-puuid/${puuid}`
);
return response.data;
} catch (error) {
handleApiError(error);
}
return retryWithBackoff(async () => {
const response = await api.get(
`https://${region}.api.riotgames.com/lol/league/v4/entries/by-puuid/${puuid}`
);
return response.data;
});
};

const getChampionMastery = async (region, puuid, championId) => {
try {
const response = await api.get(
`https://${region}.api.riotgames.com/lol/champion-mastery/v4/champion-masteries/by-puuid/${puuid}/by-champion/${championId}`
);
return response.data;
} catch (error) {
handleApiError(error);
}
return retryWithBackoff(async () => {
const response = await api.get(
`https://${region}.api.riotgames.com/lol/champion-mastery/v4/champion-masteries/by-puuid/${puuid}/by-champion/${championId}`
);
return response.data;
});
};

const getLiveGame = async (region, encryptedPUUID) => {
const getTopChampionMasteries = async (region, puuid, count = 5) => {
return retryWithBackoff(async () => {
const response = await api.get(
`https://${region}.api.riotgames.com/lol/champion-mastery/v4/champion-masteries/by-puuid/${puuid}/top?count=${count}`
);
return response.data;
});
};

const getLiveGame = async (region, puuid) => {
return retryWithBackoff(async () => {
try {
const response = await api.get(
`https://${region}.api.riotgames.com/lol/spectator/v5/active-games/by-summoner/${encryptedPUUID}`
);
return response.data;
const response = await api.get(
`https://${region}.api.riotgames.com/lol/spectator/v5/active-games/by-summoner/${puuid}`
);
return response.data;
} catch (error) {
if (error.response && error.response.status === 404) {
return null; // Not in a game, return null instead of throwing
}
handleApiError(error); // For all other errors, use the handler
// 404 means no active game - return null instead of throwing
if (error.response?.status === 404) {
return null;
}
throw error; // Let retryWithBackoff handle other errors
}
});
};

module.exports = {
Expand All @@ -159,5 +203,7 @@ module.exports = {
getMatchTimeline,
getRankedData,
getChampionMastery,
getTopChampionMasteries,
getLiveGame,
};
getApiStats,
};
Loading