mpen
mpen

Reputation: 283013

Nice way to do recursion with ES6 promises?

Here's what I've got:

function nextAvailableFilename(path) {
    return new Promise(function (resolve, reject) {
        FileSystem.exists(path, function (exists) {
            if (!exists) return resolve(path);

            var ext = Path.extname(path);
            var pathWithoutExt = path.slice(0, -ext.length);

            var match = /\d+$/.exec(pathWithoutExt);
            var number = 1;

            if (match) {
                number = parseInt(match[0]);
                pathWithoutExt = pathWithoutExt.slice(0, -match[0].length);
            }

            ++number;

            nextAvailableFilename(pathWithoutExt + number + ext).then(function () {
                return resolve.apply(undefined, arguments);
            }, function () {
                return reject.apply(undefined, arguments);
            });
        });
    });
}

But I don't like that block at the end -- isn't there a way to 'replace' the current promise with the next one in the stack rather than having one promise resolve the next like I've done here?

Upvotes: 1

Views: 856

Answers (2)

mpen
mpen

Reputation: 283013

I came up with a solution too that doesn't depend on bluebird.promisify. It should handle the case where file creation fails for a reason other than it already exists.

function createFile(path) {
    return new Promise(function (resolve, reject) {
        FileSystem.open(path, 'wx', function (err, fd) {
            if (err) return reject(err);
            FileSystem.close(fd, function (err) {
                if (err) return reject(err);
                return resolve();
            });
        });
    });
}

// todo: make more efficient by multiplying numbers by 2 or something like http://stackoverflow.com/a/1078898/65387

function nextFile(path) {
    return createFile(path).then(function () {
        return path;
    }, function (err) {
        if (err.code !== 'EEXIST') throw err; // error other than "file exists"

        var ext = Path.extname(path);
        var pathWithoutExt = path.slice(0, -ext.length);

        var match = /\d+$/.exec(pathWithoutExt);
        var number = 2;

        if (match) {
            number = parseInt(match[0]) + 1;
            pathWithoutExt = pathWithoutExt.slice(0, -match[0].length);
        }

        return nextFile(pathWithoutExt + number + ext);
    });
}

Upvotes: 0

jfriend00
jfriend00

Reputation: 707666

Here's a version that uses promise chaining and file create to avoid the race condition. I used the bluebird promise library so I can use promises with the fs library just to simplify the code and error handling:

var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));
var path = require('path');

// Creates next available xxx/yyy/foo4.txt numeric sequenced file that does
// not yet exist.  Returns the new filename in the promise
// Calling this function will create a new empty file.
function nextAvailableFilename(filename) {
    return fs.openAsync(filename, "wx+").then(function(fd) {
        return fs.closeAsync(fd).then(function() {
            return filename;
        });
    }, function(err) {
        if (err.code !== 'EEXIST') {
            // unexpected file system error
            // to avoid possible looping forever, we must bail 
            // and cause rejected promise to be returned
            throw err;
        }
        // Previous file exists so reate a new filename
        // xxx/yyy/foo4.txt becomes xxx/yyy/foo5.txt
        var ext = path.extname(filename);
        var filenameWithoutExt = filename.slice(0, -ext.length);
        var number = 0;
        var match = filenameWithoutExt.match(/\d+$/);
        if (match) {
            number = parseInt(match[0], 10);
            filenameWithoutExt = filenameWithoutExt.slice(0, -match[0].length);
        }
        ++number;
        // call this function again, returning the promise 
        // which will cause it to chain onto previous promise
        return nextAvailableFilename(filenameWithoutExt + number + ext);
    });
}

Upvotes: 1

Related Questions