diff --git a/src/assets/js/api/release.js b/src/assets/js/api/release.js index 2815e1d..6010fdb 100644 --- a/src/assets/js/api/release.js +++ b/src/assets/js/api/release.js @@ -1,20 +1,132 @@ var RELEASE_CACHE = {}; +var RELEASE_REQUESTS = {}; +var RELEASE_CACHE_TTL = 15 * 60 * 1000; +var RELEASE_BACKOFF = {}; +var RELEASE_MAX_RETRIES = 3; function fetchProjectReleases(project) { - if (RELEASE_CACHE[project]) return Promise.resolve(RELEASE_CACHE[project]); - var url = "https://gitlab.com/api/v4/projects/vyla-entertainment%2F" + encodeURIComponent(project) + "/releases"; - return fetch(url) - .then(function (res) { - if (!res.ok) throw new Error(); - return res.json(); - }) - .then(function (data) { - RELEASE_CACHE[project] = Array.isArray(data) ? data : []; - return RELEASE_CACHE[project]; - }) + var now = Date.now(); + var cached = RELEASE_CACHE[project]; + + if (cached && now - cached.timestamp < RELEASE_CACHE_TTL) { + return Promise.resolve(cached.data); + } + + if (RELEASE_REQUESTS[project]) { + return RELEASE_REQUESTS[project]; + } + + var backoff = RELEASE_BACKOFF[project]; + + if (backoff && now < backoff.until) { + return Promise.resolve(cached ? cached.data : []); + } + + var url = "https://gitlab.com/api/v4/projects/vyla-entertainment%2F" + + encodeURIComponent(project) + "/releases"; + + var request = function (attempt) { + return fetch(url) + .then(function (res) { + if (res.status === 429) { + var retryAfter = Number(res.headers.get("Retry-After")); + var delay = retryAfter > 0 + ? retryAfter * 1000 + : Math.min(5000 * Math.pow(2, attempt), 60000); + + RELEASE_BACKOFF[project] = { + until: Date.now() + delay + }; + + if (attempt < RELEASE_MAX_RETRIES) { + return new Promise(function (resolve) { + setTimeout(resolve, delay); + }).then(function () { + return request(attempt + 1); + }); + } + + throw new Error("GitLab rate limit exceeded"); + } + + if (!res.ok) { + throw new Error("GitLab API returned " + res.status); + } + + return res.json(); + }) + .then(function (data) { + if (!Array.isArray(data)) { + throw new Error("Invalid GitLab releases response"); + } + + RELEASE_CACHE[project] = { + data: data, + timestamp: Date.now() + }; + + delete RELEASE_BACKOFF[project]; + + return data; + }); + }; + + RELEASE_REQUESTS[project] = request(0) .catch(function () { - return []; + return cached ? cached.data : []; + }) + .finally(function () { + delete RELEASE_REQUESTS[project]; }); + + return RELEASE_REQUESTS[project]; +} + +function findAssetInReleases(releases, ext) { + for (var r = 0; r < releases.length; r++) { + var rel = releases[r]; + var tag = rel.tag_name || ""; + var links = (rel.assets && rel.assets.links) || []; + + for (var i = 0; i < links.length; i++) { + var l = links[i]; + var name = (l.name || "").toLowerCase(); + var url = (l.url || "").toLowerCase(); + + if (name.indexOf(ext.toLowerCase()) !== -1 || + url.indexOf(ext.toLowerCase()) !== -1) { + return { + tag: tag, + url: l.direct_asset_url || l.url + }; + } + } + + if (rel.description) { + var escapedExt = ext.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + + var reg = new RegExp( + "\\[([^\\]]+)\\]\\((https?:[^)]+\\." + escapedExt + "[^)]*)\\)", + "i" + ); + + var match = rel.description.match(reg); + + if (match) { + return { tag: tag, url: match[2] }; + } + + var rawMatch = rel.description.match( + new RegExp("(https?:\\S+\\." + escapedExt + ")", "i") + ); + + if (rawMatch) { + return { tag: tag, url: rawMatch[1] }; + } + } + } + + return null; } function findAssetInReleases(releases, ext) {