var RELEASE_CACHE = {}; var RELEASE_REQUESTS = {}; var RELEASE_CACHE_TTL = 15 * 60 * 1000; var RELEASE_BACKOFF = {}; var RELEASE_MAX_RETRIES = 3; function fetchProjectReleases(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 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) { 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) !== -1 || url.indexOf(ext) !== -1) { return { tag: tag, url: l.direct_asset_url || l.url }; } } if (rel.description) { var reg = new RegExp("\\[([^\\]]+)\\]\\((https?:[^)]+\\." + ext + "[^)]*)\\)", "i"); var match = rel.description.match(reg); if (match) return { tag: tag, url: match[2] }; var rawMatch = rel.description.match(new RegExp("(https?:\\S+\\." + ext + ")", "i")); if (rawMatch) return { tag: tag, url: rawMatch[1] }; } } return null; }