3
\$\begingroup\$

The purpose of the code is to allow the user to select directories that will be crawled recursively in order to find particular files or file types to analyze en masse. The thing to bear in mind is that if C:\ is already selected, then any child path of C:\ that the user tries to select will be discarded. The entire process of the script can be broken down into the following steps:

  1. The user selects a directory or directories via a dialog box.
  2. The script then compares the selection to whatever directories might already be stored in an array in the configuration file, which is a JSON object.
  3. If the selection is invalid, the selection is discarded. / If the selection is valid, the configuration file is modified accordingly.

Obviously promises are ideal, so I'm using Node's fs.promises. However, the tricky bit is that, although they're asynchronous, parts of the code must behave synchronously. For example, it would be a waste of resources to retrieve the array from the configuration file if the dialog box is cancelled by the user.

I'm completely new to promises and everything else that entails, so I'm wondering whether or not there's an even better way to structure the asynchronous code or the code in general? The code works fine the way it is, but can its structure/readability and or efficiency be improved?

async function userSelectsArchives()
{
  try {
    const r1 = await dialog.showOpenDialog(remote.getCurrentWindow(), {
      title: 'Select Archives to Analyze',
      buttonLabel: 'Select Archives',
      defaultPath: 'c:\\',
      properties: [ 'openDirectory', 'multiSelections' ]
    })

    if (r1.canceled) throw new Error('Dialog cancelled.')

    const
      r2 = await fsp.readFile('./config.json', 'utf8'),
      config = JSON.parse(r2),
      archivesOG = config.archives

    for (const dir of r1.filePaths){
      // Disregard user input if parent path or exact match already exists.
      if (
        config.archives.reduce((a, v) => {
          return a || dir.indexOf(v.slice(-1) === '\\' ? v : (v + '\\')) === 0
        }, false)
        || config.archives.includes(dir)
      ) continue
      // Remove child paths of user input in config.
      config.archives = config.archives.filter(
        v => v.indexOf(dir.slice(-1) === '\\' ? dir : (dir + '\\')) !== 0
      )
      // Add user input to archive.
      config.archives.push(dir)
    }

    // Verify that changes have occurred or throw error.
    if (archivesOG === config.archives) throw new Error('Invalid selection.')

    await fsp.writeFile('./config.json', JSON.stringify(config, null, 2), 'utf8')

    displayDirs(config.archives)
  }
  catch (error) {
    console.error(error)
  }
}
\$\endgroup\$
  • 2
    \$\begingroup\$ Welcome to CR! Please edit your post to tell reviewers about the purpose of this code, and make the title summarize that. What we want to avoid, is a home page that reads "how can I improve this code?" 50 times over. Thanks! \$\endgroup\$ – Mathieu Guindon Oct 30 at 0:01
  • \$\begingroup\$ Is this code working as expected? It seems as if (archivesOG === config.archives) will always evaluate to true and throw as you as these reference the same object (you are not cloning this array). \$\endgroup\$ – Mike Brant Oct 30 at 2:22
  • \$\begingroup\$ @MathieuGuindon Thanks. I will do that as soon as i get a free minute sometime in the next 24 hours. \$\endgroup\$ – oldboy Oct 30 at 3:49
  • 2
    \$\begingroup\$ @MikeBrant yes, code is working as expected. no, archivesOG === config.archives evaluates to false if or rather when config.archives is modified, which is typically the case, since archivesOG is simply a cached version of config.archives before potential modifications are acted out \$\endgroup\$ – oldboy Oct 30 at 3:52
  • 1
    \$\begingroup\$ It's fine as is, but you should use .some() instead of .reduce() here. \$\endgroup\$ – wOxxOm Oct 30 at 5:50
3
\$\begingroup\$

Tips for JS code optimization:

Variable names

Give your variables meaningful names:

  • const r1 = await dialog.showOpenDialog(...) can be renamed to dirs or dirList (representing selected directories)
  • const r2 = await fsp.readFile('./config.json', 'utf8'), config = JSON.parse(r2), where r2 is supposed to be a config file path. Let's rename it to say
    const config_file = await fsp.readFile('./config.json', 'utf8'), config = JSON.parse(config_file)

Extract function:

Repeated expression <string>.slice(-1) === '\\' ? <string> : (<string> + '\\') can be extracted into a reusable routine (named or anonymous function):

var asDirPath = function(path) {
    return path.slice(-1) === '\\' ? path : (path + '\\');
};

Substitute algorithm:

The whole condition (combination of array.reduce and array.includes) can be easily substituted with a more efficient Array.some() approach (to check for any match between current directory path dir and config.archives file/dir paths):

for (const dir of dirList.filePaths){
    // Disregard user input if parent path or exact match already exists.
    if (config.archives.some(path => {
            return dir.indexOf(asDirPath(path)) === 0 || path === dir
    })) continue
\$\endgroup\$
  • 1
    \$\begingroup\$ thanks for your recommendations! \$\endgroup\$ – oldboy Oct 30 at 7:19
2
\$\begingroup\$

Some suggestions:

  1. Since this is an async function, it seems likely the caller may want to know when it's done and if it succeeded. To do that, you need to reject upon error. That means rethrow after logging inside your catch. This allows the error to get back to the caller as a rejected promise.

  2. Simplify by changing config.archives.reduce() to config.archives.some(). This will also be faster in cases where there is a match because .some() will short-circuit the iteration as soon as a match is found.

  3. Change return dir.indexOf(v.slice(-1) === '\\' ? v : (v + '\\')) === 0; to return dir.indexOf(v) === 0;. It appears the point of this code is to allow partial matches where v is a subset of dir, but starts matching at the beginning. If that's the case and if I understood what you're trying to do here, then you don't need to make sure that v has a trailing slash on it as that won't change the outcome.

  4. Remove || config.archives.includes(dir). It appears that is redundant since a full match will have already been found in the previous iteration through config.archives.

  5. Coding style. I prefer if (condition) { do something } rather than if (condition) continue as I just think it's easier to read and follow.

  6. You have a lot of references to config.archives. I would suggest either just using archivesOG instead or make a shorter named local variable such as just archives that you can refer to everywhere except when assigning back to it.

  7. r1 and r2 could have much more meaningful names.

  8. The code dir.indexOf(v.slice(-1) === '\\' ? v : (v + '\\')) === 0; begs to be put in a function with a meaningful name. It takes a bit of a pause when reading this code to figure out what that's doing. If it has a meaningful function name such as normalizePathEnd() or something like that, the code will be a lot easier to read without having to follow the detail of the string manipulation. Also, your code as you show it has two copies of this concept which also begs to be in a utility function.

\$\endgroup\$
  • \$\begingroup\$ unfortunately 3. isn't possible due to comparisons between similar paths like C:\\User and C:\\Users, thus 4. is also out of the question. thanks for your suggestions nonetheless \$\endgroup\$ – oldboy Nov 1 at 23:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.