From 27300ac3e03d27b8a2803658b119d00303a8a54d Mon Sep 17 00:00:00 2001 From: Chiezo Date: Sat, 21 Oct 2023 15:21:30 +0900 Subject: [PATCH] perf: cache latest semvers resolved from remote (#42) --- deno.lock | 2 ++ lib/x/async.ts | 1 + src/dependency.ts | 72 ++++++++++++++++++++++++++++++++++++++++++++--- src/semver.ts | 2 +- 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 lib/x/async.ts diff --git a/deno.lock b/deno.lock index 693f5466..2eecfc6b 100644 --- a/deno.lock +++ b/deno.lock @@ -1,6 +1,7 @@ { "version": "3", "remote": { + "https://deno.land/std@0.186.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", "https://deno.land/std@0.196.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", "https://deno.land/std@0.196.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", "https://deno.land/std@0.196.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", @@ -59,6 +60,7 @@ "https://deno.land/std@0.203.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", "https://deno.land/std@0.203.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", + "https://deno.land/x/async@v2.0.2/mutex.ts": "312dcad7468c82f84fd018be157df451361ed19bdc12fd59af8d12b2e6c3ae28", "https://deno.land/x/cliffy@v1.0.0-rc.3/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004", "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/ansi_escapes.ts": "193b3c3a4e520274bd8322ca4cab1c3ce38070bed1898cb2ade12a585dddd7c9", "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/chain.ts": "eca61b1b64cad7b9799490c12c7aa5538d0f63ac65a73ddb6acac8b35f0a5323", diff --git a/lib/x/async.ts b/lib/x/async.ts new file mode 100644 index 00000000..7f820199 --- /dev/null +++ b/lib/x/async.ts @@ -0,0 +1 @@ +export { Mutex } from "https://deno.land/x/async@v2.0.2/mutex.ts"; diff --git a/src/dependency.ts b/src/dependency.ts index d3d2e78c..9a622c38 100644 --- a/src/dependency.ts +++ b/src/dependency.ts @@ -1,6 +1,8 @@ import type { Maybe } from "../lib/types.ts"; +import { Mutex } from "../lib/x/async.ts"; import type { Path, SemVerString } from "./types.ts"; -import { parseSemVer } from "./semver.ts"; +import { parseSemVer, SEMVER_REGEXP } from "./semver.ts"; +import { assertExists } from "../lib/std/assert.ts"; export interface Dependency { name: string; @@ -31,6 +33,23 @@ function parseProps( async function resolveLatestSemVer( url: URL, ): Promise> { + await LatestSemverCache.lock(url); + const result = await _resolve(url); + LatestSemverCache.unlock(url); + return result; +} + +async function _resolve( + url: URL, +): Promise> { + const cached = LatestSemverCache.get(url); + if (cached) { + return cached; + } + if (cached === null) { + // The dependency is already found to be up to date. + return; + } const props = parseProps(url); if (!props) { // The specifier is does not contain a semver. @@ -42,7 +61,9 @@ async function resolveLatestSemVer( `https://registry.npmjs.org/${props.name}`, ); if (!response.ok) { - throw new Error(`Failed to fetch npm registry: ${response.statusText}`); + throw new Error( + `Failed to fetch npm registry: ${response.statusText}`, + ); } const json = await response.json(); if (!json["dist-tags"]?.latest) { @@ -53,9 +74,10 @@ async function resolveLatestSemVer( const latestSemVer: string = json["dist-tags"].latest; if (latestSemVer === props.version) { // The dependency is up to date + LatestSemverCache.set(url, null); return; } - return latestSemVer as SemVerString; + return LatestSemverCache.set(url, latestSemVer as SemVerString); } case "http:": case "https:": { @@ -66,14 +88,19 @@ async function resolveLatestSemVer( await response.arrayBuffer(); if (!response.redirected) { // The host did not redirect to a url with semver + LatestSemverCache.set(url, null); return; } const specifierWithLatestSemVer = response.url; if (specifierWithLatestSemVer === url.href) { // The dependency is up to date + LatestSemverCache.set(url, null); return; } - return parseSemVer(specifierWithLatestSemVer); + return LatestSemverCache.set( + url, + parseSemVer(specifierWithLatestSemVer) as SemVerString, + ); } case "node:": case "file:": @@ -83,3 +110,40 @@ async function resolveLatestSemVer( return; } } + +class LatestSemverCache { + static #mutex = new Map(); + static #cache = new Map(); + + static lock(url: URL): Promise { + const key = this.getKey(url); + const mutex = this.#mutex.get(key) ?? + this.#mutex.set(key, new Mutex()).get(key)!; + return mutex.acquire(); + } + + static unlock(url: URL): void { + const key = this.getKey(url); + const mutex = this.#mutex.get(key); + assertExists(mutex); + mutex.release(); + } + + static get(url: URL): SemVerString | null | undefined { + const key = this.getKey(url); + return this.#cache.get(key); + } + + static set( + url: URL, + semver: T, + ): T { + const key = this.getKey(url); + this.#cache.set(key, semver); + return semver; + } + + static getKey(url: URL): string { + return url.href.split(SEMVER_REGEXP)[0]; + } +} diff --git a/src/semver.ts b/src/semver.ts index 83a4b367..a51761c6 100644 --- a/src/semver.ts +++ b/src/semver.ts @@ -2,7 +2,7 @@ import type { Maybe } from "../lib/types.ts"; import type { SemVerString } from "./types.ts"; // Ref: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string -const SEMVER_REGEXP = +export const SEMVER_REGEXP = /@v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/g; export function parseSemVer(