@vzn/vx 0.0.235 → 0.0.236

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/package.json +5 -5
  2. package/src/cache/inputs.ts +47 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.235",
3
+ "version": "0.0.236",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.235",
21
- "@vzn/vx-linux-arm64": "0.0.235",
22
- "@vzn/vx-darwin-x64": "0.0.235",
23
- "@vzn/vx-darwin-arm64": "0.0.235"
20
+ "@vzn/vx-linux-x64": "0.0.236",
21
+ "@vzn/vx-linux-arm64": "0.0.236",
22
+ "@vzn/vx-darwin-x64": "0.0.236",
23
+ "@vzn/vx-darwin-arm64": "0.0.236"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -17,7 +17,7 @@
17
17
  // be a git work tree; non-git environments are not supported.
18
18
 
19
19
  import path from 'node:path'
20
- import { lstatSync, readdirSync, realpathSync } from 'node:fs'
20
+ import { lstatSync, readdirSync, realpathSync, rmdirSync, rmSync } from 'node:fs'
21
21
  import { rm, rmdir } from 'node:fs/promises'
22
22
  import type { CacheInputs } from '../config.js'
23
23
  import {
@@ -727,6 +727,14 @@ function changedSince(
727
727
  return own
728
728
  }
729
729
 
730
+ /**
731
+ * Up to this many paths, a clean removes synchronously: the threadpool round
732
+ * trip per `rm`/`rmdir` cost more than the unlink itself, 0.30 ms against
733
+ * 0.13 for one file and 1.7 against 1.5 for 128 (min of 7). Past a few
734
+ * hundred the parallel async removal wins (512: 4.5 against 5.4).
735
+ */
736
+ const SYNC_CLEAN_MAX = 128
737
+
730
738
  /**
731
739
  * A declared output the process cannot remove (a `dist/` another user
732
740
  * wrote, a read-only checkout) is the environment's failure, not vx's:
@@ -734,15 +742,28 @@ function changedSince(
734
742
  * sends the reader to file a bug against a permission bit.
735
743
  */
736
744
  async function removeAll(files: readonly string[], root: string): Promise<void> {
745
+ const refused = (f: string, err: NodeJS.ErrnoException): UserError => {
746
+ const rel = path.relative(root, f).split(path.sep).join('/')
747
+ return new UserError(
748
+ `cannot remove declared output ${rel}: ${err.code ?? err.message} — vx clears a task's ` +
749
+ `declared outputs before it runs and before a restore; make the path removable ` +
750
+ `by this user, or stop declaring it as an output`,
751
+ )
752
+ }
753
+ if (files.length <= SYNC_CLEAN_MAX) {
754
+ for (const f of files) {
755
+ try {
756
+ rmSync(f, { force: true })
757
+ } catch (err) {
758
+ throw refused(f, err as NodeJS.ErrnoException)
759
+ }
760
+ }
761
+ return
762
+ }
737
763
  await Promise.all(
738
764
  files.map((f) =>
739
765
  rm(f, { force: true }).catch((err: NodeJS.ErrnoException) => {
740
- const rel = path.relative(root, f).split(path.sep).join('/')
741
- throw new UserError(
742
- `cannot remove declared output ${rel}: ${err.code ?? err.message} — vx clears a task's ` +
743
- `declared outputs before it runs and before a restore; make the path removable ` +
744
- `by this user, or stop declaring it as an output`,
745
- )
766
+ throw refused(f, err)
746
767
  }),
747
768
  ),
748
769
  )
@@ -772,16 +793,26 @@ async function pruneEmptiedDirs(root: string, removed: readonly string[]): Promi
772
793
  let level = new Set(removed.map((f) => path.dirname(f)))
773
794
  while (level.size > 0) {
774
795
  const parents = new Set<string>()
775
- await Promise.all(
776
- [...level].map(async (dir) => {
777
- if (dir === rootResolved || !dir.startsWith(rootResolved + path.sep)) return
778
- const gone = await rmdir(dir).then(
779
- () => true,
780
- (err: NodeJS.ErrnoException) => err.code === 'ENOENT',
781
- )
782
- if (gone) parents.add(path.dirname(dir))
783
- }),
796
+ const dirs = [...level].filter(
797
+ (dir) => dir !== rootResolved && dir.startsWith(rootResolved + path.sep),
784
798
  )
799
+ const gone = (err: NodeJS.ErrnoException): boolean => err.code === 'ENOENT'
800
+ if (dirs.length <= SYNC_CLEAN_MAX) {
801
+ for (const dir of dirs) {
802
+ try {
803
+ rmdirSync(dir)
804
+ parents.add(path.dirname(dir))
805
+ } catch (err) {
806
+ if (gone(err as NodeJS.ErrnoException)) parents.add(path.dirname(dir))
807
+ }
808
+ }
809
+ } else {
810
+ await Promise.all(
811
+ dirs.map(async (dir) => {
812
+ if (await rmdir(dir).then(() => true, gone)) parents.add(path.dirname(dir))
813
+ }),
814
+ )
815
+ }
785
816
  level = parents
786
817
  }
787
818
  }