@vltpkg/package-info 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +63 -12
  2. package/package.json +14 -13
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { error } from '@vltpkg/error-cause';
2
2
  import { clone, resolve as gitResolve, revs } from '@vltpkg/git';
3
+ import { logRequest } from '@vltpkg/output';
3
4
  import { PackageJson } from '@vltpkg/package-json';
4
5
  import { pickManifest } from '@vltpkg/pick-manifest';
5
6
  import { Spec } from '@vltpkg/spec';
@@ -10,8 +11,10 @@ import { XDG } from '@vltpkg/xdg';
10
11
  import { createHash, randomBytes } from 'node:crypto';
11
12
  import { mkdir, readFile, rm, stat, symlink, unlink, writeFile, } from 'node:fs/promises';
12
13
  import { basename, dirname, resolve as pathResolve, relative, } from 'node:path';
14
+ import { debuglog } from 'node:util';
13
15
  import { create as tarC } from 'tar';
14
16
  import { rename } from "./rename.js";
17
+ const debug = debuglog('vlt');
15
18
  const xdg = new XDG('vlt');
16
19
  export const delimiter = '~';
17
20
  // the maximum duration of a manifest cache file
@@ -23,6 +26,15 @@ const manifestCacheMaxAge = 5 * 60 * 1000;
23
26
  const noRegistryError = (spec) => error('No registry configured to resolve this spec. Set "registry" in ' +
24
27
  'vlt.json, pass --registry, or run `vlt login --registry=<url>`. ' +
25
28
  'See https://docs.vlt.sh/cli', { code: 'ECONFIG', spec });
29
+ /**
30
+ * A selector that can point at a different version tomorrow: a dist tag,
31
+ * or a range matching anything (`*`, empty string). Manifest results for
32
+ * these are not cached to disk, and packument requests for them force a
33
+ * revalidation of the registry client's cache entry.
34
+ *
35
+ * Takes a *final* spec (`spec.final`), same as `pickManifest` sees.
36
+ */
37
+ const isMovingSelector = (f) => !!(f.distTag || f.range?.isAny);
26
38
  export class PackageInfoClient {
27
39
  #registryClient;
28
40
  #projectRoot;
@@ -36,7 +48,9 @@ export class PackageInfoClient {
36
48
  #cachePath;
37
49
  // In-flight coalescing key is `${registry}${name}` — no representation
38
50
  // component. Safe only because every caller requests the same full
39
- // packument (see #fetchPackument).
51
+ // packument (see #fetchPackument). The one thing that does vary per
52
+ // caller is forceRevalidate, so record it and let a moving selector
53
+ // reuse a forced promise but never a non-forced one (see packument()).
40
54
  #packumentPromises = new Map();
41
55
  // unique temp file names for atomic manifest cache writes
42
56
  #manifestWriteRandom = randomBytes(6).toString('hex');
@@ -129,6 +143,25 @@ export class PackageInfoClient {
129
143
  // fallthrough if a remote tarball url present
130
144
  }
131
145
  case 'registry': {
146
+ // if the tarball is already on disk, unpack it straight from
147
+ // the cache file: it never has to be held in the client's
148
+ // in-memory cache. anything unexpected falls through to the
149
+ // fetch path, which throws its own error if the body is
150
+ // genuinely bad.
151
+ const cached = (await this.getRegistryClient()).cachedBody(r.resolved, { integrity: r.integrity });
152
+ if (cached) {
153
+ try {
154
+ await (await this.getTarPool()).unpack(cached.body, target);
155
+ logRequest(r.resolved, 'cache');
156
+ return r;
157
+ }
158
+ catch (er) {
159
+ // a systematically failing fast path (every entry still
160
+ // gzipped, EACCES, ENOSPC...) doubles the IO of the fetch
161
+ // path, so leave a trace behind. NODE_DEBUG=vlt to see it.
162
+ debug('cached tarball unpack failed: %s: %s', cached.path, er);
163
+ }
164
+ }
132
165
  const fetchTarball = async (useCache) => {
133
166
  const trustIntegrity = this.#trustedIntegrities.get(r.resolved) === r.integrity;
134
167
  const response = await (await this.getRegistryClient()).request(r.resolved, {
@@ -244,7 +277,7 @@ export class PackageInfoClient {
244
277
  const st = await stat(path);
245
278
  if (st.isFile()) {
246
279
  try {
247
- await (await this.getTarPool()).unpack(await this.tarball(spec, options), target);
280
+ await (await this.getTarPool()).unpackFile(path, target);
248
281
  }
249
282
  catch (er) {
250
283
  throw this.#resolveError(spec, options, 'tar unpack failed', { cause: er });
@@ -311,11 +344,9 @@ export class PackageInfoClient {
311
344
  if (options.before) {
312
345
  return;
313
346
  }
314
- // if the final resolved spec is either a dist tag or something that
315
- // matches any range (such as a semver range of `*` or empty string)
316
- // then we skip caching
347
+ // a moving selector's result is variable, so don't cache it
317
348
  const f = spec.final;
318
- if (f.distTag || f.range?.isAny) {
349
+ if (isMovingSelector(f)) {
319
350
  return;
320
351
  }
321
352
  const key = this.#manifestCacheKey(f, options);
@@ -586,7 +617,7 @@ export class PackageInfoClient {
586
617
  const s = spec;
587
618
  return await this.#tmpdir(async (dir) => {
588
619
  try {
589
- await (await this.getTarPool()).unpack(await readFile(path), dir);
620
+ await (await this.getTarPool()).unpackFile(path, dir);
590
621
  }
591
622
  catch (er) {
592
623
  throw this.#resolveError(s, options, 'tar unpack failed', { cause: er });
@@ -638,16 +669,30 @@ export class PackageInfoClient {
638
669
  throw noRegistryError(spec);
639
670
  // Coalescing key has no representation component (see #fetchPackument).
640
671
  const packumentKey = `${registry}${name}`;
672
+ const forced = isMovingSelector(f);
641
673
  const inflight = this.#packumentPromises.get(packumentKey);
642
- if (inflight)
643
- return inflight;
674
+ // a moving selector must not ride along on a non-forced request:
675
+ // that one can settle to a fresh-but-stale cache hit, which is
676
+ // exactly what forceRevalidate exists to avoid. the other
677
+ // direction is fine -- a forced result is never staler.
678
+ // costs at most one extra concurrent GET for the same packument
679
+ // when both shapes are asked for at once.
680
+ if (inflight && (!forced || inflight.forced))
681
+ return inflight.promise;
644
682
  const pakuURL = new URL(name, registry);
645
683
  const promise = this.#fetchPackument(spec, options, pakuURL);
646
- this.#packumentPromises.set(packumentKey, promise);
647
- // Clean up once settled so we don't leak memory.
684
+ const record = { promise, forced };
685
+ this.#packumentPromises.set(packumentKey, record);
686
+ // Clean up once settled so we don't leak memory, unless a forced
687
+ // request has since taken the slot over.
648
688
  // Use .then/.catch instead of .finally to avoid creating
649
689
  // an unhandled rejection from the derived promise.
650
- promise.then(() => this.#packumentPromises.delete(packumentKey), () => this.#packumentPromises.delete(packumentKey));
690
+ const clear = () => {
691
+ if (this.#packumentPromises.get(packumentKey) === record) {
692
+ this.#packumentPromises.delete(packumentKey);
693
+ }
694
+ };
695
+ promise.then(clear, clear);
651
696
  return promise;
652
697
  }
653
698
  }
@@ -681,6 +726,12 @@ export class PackageInfoClient {
681
726
  const response = await (await this.getRegistryClient()).request(pakuURL, {
682
727
  headers: { accept: 'application/json' },
683
728
  ...(useCache === false ? { useCache } : {}),
729
+ // costs a conditional GET per moving selector on an otherwise warm
730
+ // cache, install included. 304s are cheap but not free; the
731
+ // alternative is serving a dist tag that moved (#1656).
732
+ ...(isMovingSelector(spec.final) ?
733
+ { forceRevalidate: true }
734
+ : {}),
684
735
  });
685
736
  if (response.statusCode !== 200) {
686
737
  throw this.#resolveError(spec, options, 'failed to fetch packument', {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/package-info",
3
3
  "description": "Resolve and fetch package metadata and tarballs",
4
- "version": "1.0.8",
4
+ "version": "1.0.10",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,16 +13,17 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/error-cause": "1.0.8",
17
- "@vltpkg/git": "1.0.8",
18
- "@vltpkg/package-json": "1.0.8",
19
- "@vltpkg/pick-manifest": "1.0.8",
20
- "@vltpkg/registry-client": "1.0.8",
21
- "@vltpkg/spec": "1.0.8",
22
- "@vltpkg/tar": "1.0.8",
23
- "@vltpkg/types": "1.0.8",
24
- "@vltpkg/workspaces": "1.0.8",
25
- "@vltpkg/xdg": "1.0.8",
16
+ "@vltpkg/error-cause": "1.0.10",
17
+ "@vltpkg/git": "1.0.10",
18
+ "@vltpkg/output": "1.0.10",
19
+ "@vltpkg/package-json": "1.0.10",
20
+ "@vltpkg/pick-manifest": "1.0.10",
21
+ "@vltpkg/registry-client": "1.0.10",
22
+ "@vltpkg/spec": "1.0.10",
23
+ "@vltpkg/tar": "1.0.10",
24
+ "@vltpkg/types": "1.0.10",
25
+ "@vltpkg/workspaces": "1.0.10",
26
+ "@vltpkg/xdg": "1.0.10",
26
27
  "ssri": "^13.0.0",
27
28
  "tar": "^7.5.2"
28
29
  },
@@ -31,8 +32,8 @@
31
32
  "@types/node": "^22.19.2",
32
33
  "@types/pacote": "^11.1.8",
33
34
  "@vltpkg/benchmark": "0.0.0",
34
- "@vltpkg/cache-unzip": "1.0.8",
35
- "@vltpkg/vlt-json": "1.0.8",
35
+ "@vltpkg/cache-unzip": "1.0.10",
36
+ "@vltpkg/vlt-json": "1.0.10",
36
37
  "eslint": "^9.39.1",
37
38
  "pacote": "^21.0.4",
38
39
  "prettier": "^3.7.4",