renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
187 lines (186 loc) • 6.5 kB
JavaScript
import { newlineRegex } from "../../../util/regex.js";
import { copystr } from "../../../util/string.js";
import { logger } from "../../../logger/index.js";
import { parseUrl } from "../../../util/url.js";
import { LooseArray } from "../../../util/schema-utils/index.js";
import { RequestError } from "../../../util/http/got.js";
import { Result } from "../../../util/result.js";
import "../../../util/http/index.js";
import { getElapsedMinutes } from "../../../util/date.js";
import { z } from "zod/v4";
//#region lib/modules/datasource/rubygems/versions-endpoint-cache.ts
function getContentTail(content) {
return content.slice(-33);
}
function getContentHead(content) {
return content.slice(0, 33);
}
function stripContentHead(content) {
return content.slice(33);
}
function reconcilePackageVersions(packageVersions, versionLines) {
for (const line of versionLines) {
const packageName = copystr(line.packageName);
let versions = packageVersions.get(packageName) ?? [];
const { deletedVersions, addedVersions } = line;
if (deletedVersions.size > 0) versions = versions.filter((v) => !deletedVersions.has(v));
if (addedVersions.length > 0) {
const existingVersions = new Set(versions);
for (const addedVersion of addedVersions) if (!existingVersions.has(addedVersion)) {
const version = copystr(addedVersion);
versions.push(version);
}
}
packageVersions.set(packageName, versions);
}
return packageVersions;
}
function parseFullBody(body) {
const packageVersions = reconcilePackageVersions(/* @__PURE__ */ new Map(), VersionLines.parse(body));
const syncedAt = /* @__PURE__ */ new Date();
const contentLength = body.length;
const contentTail = getContentTail(body);
return Result.ok({
packageVersions,
syncedAt,
contentLength,
contentTail
});
}
const memCache = /* @__PURE__ */ new Map();
function cacheResult(registryUrl, result) {
if (parseUrl(registryUrl)?.hostname === "rubygems.org") memCache.set(registryUrl, result);
}
const VersionLines = z.string().transform((x) => x.split(newlineRegex)).pipe(LooseArray(z.string().transform((line) => line.trim()).refine((line) => line.length > 0).refine((line) => !line.startsWith("created_at:")).refine((line) => line !== "---").transform((line) => line.split(" ")).pipe(z.tuple([z.string(), z.string()]).rest(z.string())).transform(([packageName, versions]) => {
const deletedVersions = /* @__PURE__ */ new Set();
const addedVersions = [];
for (const version of versions.split(",")) if (version.startsWith("-")) deletedVersions.add(version.slice(1));
else addedVersions.push(version);
return {
packageName,
deletedVersions,
addedVersions
};
})));
function isStale(regCache) {
return getElapsedMinutes(regCache.syncedAt) >= 15;
}
var VersionsEndpointCache = class {
http;
constructor(http) {
this.http = http;
}
cacheRequests = /* @__PURE__ */ new Map();
/**
* At any given time, there should only be one request for a given registryUrl.
*/
async getCache(registryUrl) {
const oldResult = memCache.get(registryUrl);
if (!oldResult) {
const newResult = await this.fullSync(registryUrl);
cacheResult(registryUrl, newResult);
return newResult;
}
const { val: data } = oldResult.unwrap();
if (!data) return oldResult;
if (isStale(data)) {
memCache.delete(registryUrl);
const newResult = await this.deltaSync(data, registryUrl);
cacheResult(registryUrl, newResult);
return newResult;
}
return oldResult;
}
async getVersions(registryUrl, packageName) {
/**
* Ensure that only one request for a given registryUrl is in flight at a time.
*/
let cacheRequest = this.cacheRequests.get(registryUrl);
if (!cacheRequest) {
cacheRequest = this.getCache(registryUrl);
this.cacheRequests.set(registryUrl, cacheRequest);
}
let cachedResult;
try {
cachedResult = await cacheRequest;
} finally {
this.cacheRequests.delete(registryUrl);
}
const { val: cachedData } = cachedResult.unwrap();
if (!cachedData) {
logger.debug({
packageName,
registryUrl
}, "Rubygems: endpoint not supported");
return Result.err("unsupported-api");
}
const versions = cachedData.packageVersions.get(packageName);
if (!versions?.length) {
logger.debug({
packageName,
registryUrl
}, "Rubygems: versions not found");
return Result.err("package-not-found");
}
return Result.ok(versions);
}
async fullSync(registryUrl) {
try {
const url = `${registryUrl}/versions`;
const { body } = await this.http.getText(url, { headers: { "Accept-Encoding": "gzip" } });
return parseFullBody(body);
} catch (err) {
if (err instanceof RequestError && err.response?.statusCode === 404) return Result.err("unsupported-api");
throw err;
}
}
async deltaSync(oldCache, registryUrl) {
try {
const url = `${registryUrl}/versions`;
const startByte = oldCache.contentLength - oldCache.contentTail.length;
const opts = { headers: {
["Accept-Encoding"]: "deflate, compress, br",
["Range"]: `bytes=${startByte}-`
} };
const { statusCode, body } = await this.http.getText(url, opts);
/**
* Rubygems will return the full body instead of `416 Range Not Satisfiable`.
* In this case, status code will be 200 instead of 206.
*/
if (statusCode === 200) return parseFullBody(body);
if (getContentHead(body) !== oldCache.contentTail) return this.fullSync(registryUrl);
/**
* Update the cache with the new data.
*/
const delta = stripContentHead(body);
const packageVersions = reconcilePackageVersions(oldCache.packageVersions, VersionLines.parse(delta));
const syncedAt = /* @__PURE__ */ new Date();
const contentLength = oldCache.contentLength + delta.length;
const contentTail = getContentTail(body);
return Result.ok({
packageVersions,
syncedAt,
contentLength,
contentTail
});
} catch (err) {
if (err instanceof RequestError) {
const responseStatus = err.response?.statusCode;
/**
* In case of `416 Range Not Satisfiable` we do a full sync.
* This is unlikely to happen in real life, but anyway.
*/
if (responseStatus === 416) return this.fullSync(registryUrl);
/**
* If the endpoint is not supported, we stop trying.
* This is unlikely to happen in real life, but still.
*/
if (responseStatus === 404) return Result.err("unsupported-api");
}
throw err;
}
}
};
//#endregion
export { VersionsEndpointCache };
//# sourceMappingURL=versions-endpoint-cache.js.map