Why does Node.js exit before completing a non-flowing stream copy

I'm just learning node.js and wanted to write a simple test program that copied a file from a source folder to a destination folder. I piped a fs.ReadStream to a fs.WriteStream and that worked perfectly. I next tried to use non-flowing mode but the following program fails 99% of the time on larger files (anything over 1MB.) I'm assuming that given certain timing the event queue becomes empty and so exits. Should the following program work?

var sourcePath = "./source/test.txt";
var destinationPath = "./destination/test.txt";

// Display number of times 'readable' callback fired
var callbackCount = 0;
process.on('exit', function() {
    console.log('Readable callback fired %d times', callbackCount);
})

var fs = require('fs');
var sourceStream = fs.createReadStream(sourcePath);
var destinationStream = fs.createWriteStream(destinationPath);

copyStream(sourceStream, destinationStream);

function copyStream(src, dst) {
    var drained = true;

    // read chunk of data when ready
    src.on('readable', function () {
        ++callbackCount;
        if (!drained) {
            dst.once('drain', function () {
                writeToDestination();
            });
        } else {
            writeToDestination();
        }

        function writeToDestination() {
            var chunk = src.read();

            if (chunk !== null) {
                drained = dst.write(chunk);
            }
        }
    });

    src.on('end', function () {
        dst.end();
    });
}

NOTE: If I remove the drain related code the program always works but the node.js documentation indicates that you should wait on a drain event if the write function returns false.

So should the above program work as is? If it shouldn't how should I reorganize it to work with both readable and drain events?

It looks like you're most of the way there; there are just a couple of things you need to change.

  1. When writing to dst you need to keep reading from src until either you get a null chunk, or dst.write() returns false.
  2. Instead of listening for all readable events on src, you should only be listening for those events when it's ok to write to dst and you currently have nothing to write.

Something like this:

function copyStream(src, dst) {
    function writeToDestination() {
        var chunk = src.read(),
            drained = true;

        // write until dst is saturated or there's no more data available in src
        while (drained && (chunk !== null)) {
            if (drained = dst.write(chunk)) {
                chunk = src.read();
            }
        }

        if (!drained) {
            // if dst is saturated, wait for it to drain and then write again
            dst.once('drain', function() {
                writeToDestination();
            });
        } else {
            // if we ran out of data in src, wait for more and then write again
            src.once('readable', function() {
                ++callbackCount;
                writeToDestination();
            });
        }
    }

    // trigger the initial write when data is available in src
    src.once('readable', function() {
        ++callbackCount;
        writeToDestination();
    });

    src.on('end', function () {
        dst.end();
    });
}