From f618a44b9e845e4cd15efe697b1a426ccc839b36 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Sat, 15 Feb 2014 10:05:04 -0500 Subject: [PATCH 01/21] Begin work on FileSystemShell and touch command --- src/fs.js | 5 ++++ src/index.js | 5 ++-- src/shell.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/shell.js diff --git a/src/fs.js b/src/fs.js index 5a8e223..122ce21 100644 --- a/src/fs.js +++ b/src/fs.js @@ -53,6 +53,7 @@ define(function(require) { var providers = require('src/providers/providers'); var adapters = require('src/adapters/adapters'); + var Shell = require('src/shell'); /* * DirectoryEntry @@ -2473,6 +2474,10 @@ define(function(require) { callback(error); } }; + FileSystem.prototype.Shell = function(options) { + return new Shell(this, options); + }; + return FileSystem; }); diff --git a/src/index.js b/src/index.js index eeebb53..f343bd6 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,8 @@ define(function(require) { return { FileSystem: require('src/fs'), + FileSystemShell: require('src/shell'), Path: require('src/path') - } + }; -}); \ No newline at end of file +}); diff --git a/src/shell.js b/src/shell.js new file mode 100644 index 0000000..48eb925 --- /dev/null +++ b/src/shell.js @@ -0,0 +1,78 @@ +define(function(require) { + + function Shell(fs, options) { + options = options || {}; + + var cwd = options.cwd || '/'; + + Object.defineProperty(this, 'fs', { + get: function() { return fs; }, + enumerable: true + }); + + Object.defineProperty(this, 'cwd', { + get: function() { return cwd; }, + enumerable: true + }); + + // We include `cd` on the this vs. proto so that + // we can access cwd without exposing it externally. + this.cd = function(path) { + + }; + + } + + Shell.prototype.ls = function(path) { + + }; + + Shell.prototype.rm = function(path, options, callback) { + + }; + + Shell.prototype.mv = function(path) { + + }; + + Shell.prototype.cp = function(path) { + + }; + + Shell.prototype.mkdir = function(path) { + + }; + + Shell.prototype.touch = function(path, options, callback) { + var fs = this.fs; + path = Path.resolve(this.cwd, path); + + function createFile(path) { + fs.writeFile(path, '', function(error) { + callback(error); + }); + } + + function updateTimes(path) { + var now = Date.now(); + fs.utimes(path, now, now, function(error) { + callback(error); + }); + } + + fs.stat(path, function(error, stats) { + if(error) { + createFile(path); + } else { + updateTimes(path); + } + }); + }; + + Shell.prototype.ln = function(path) { + + }; + + return Shell; + +}); From e43b1ba551f7e74e4a7c078b3a90edd7a3a2b41b Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Sat, 15 Feb 2014 10:54:54 -0500 Subject: [PATCH 02/21] Working touch command with tests --- src/shell.js | 27 ++++++++- tests/lib/test-utils.js | 5 ++ tests/spec/shell/touch.spec.js | 104 +++++++++++++++++++++++++++++++++ tests/test-manifest.js | 3 + 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 tests/spec/shell/touch.spec.js diff --git a/src/shell.js b/src/shell.js index 48eb925..c1f18b5 100644 --- a/src/shell.js +++ b/src/shell.js @@ -1,5 +1,9 @@ define(function(require) { + var Path = require('src/path'); + + + function Shell(fs, options) { options = options || {}; @@ -43,8 +47,19 @@ define(function(require) { }; + /** + * Create a file if it does not exist, or update access and + * modified times if it does. Valid options include: + * + * * create - whether to create the file if missing (defaults to true) + * * date - use the provided Date value instead of current date/time + */ Shell.prototype.touch = function(path, options, callback) { var fs = this.fs; + if(typeof options === 'function') { + callback = options; + options = {}; + } path = Path.resolve(this.cwd, path); function createFile(path) { @@ -55,14 +70,22 @@ define(function(require) { function updateTimes(path) { var now = Date.now(); - fs.utimes(path, now, now, function(error) { + var atime = options.date || now; + var mtime = options.date || now; + + fs.utimes(path, atime, mtime, function(error) { callback(error); }); } fs.stat(path, function(error, stats) { if(error) { - createFile(path); + // Skip file creation if create is `false` + if(options.create === false) { + callback(); + } else { + createFile(path); + } } else { updateTimes(path); } diff --git a/tests/lib/test-utils.js b/tests/lib/test-utils.js index 666a2f2..bca7658 100644 --- a/tests/lib/test-utils.js +++ b/tests/lib/test-utils.js @@ -68,6 +68,10 @@ function(Filer, IndexedDBTestProvider, WebSQLTestProvider, MemoryTestProvider) { return _provider; } + function shell() { + return fs().Shell(); + } + function cleanup(callback) { if(!_provider) { return; @@ -100,6 +104,7 @@ function(Filer, IndexedDBTestProvider, WebSQLTestProvider, MemoryTestProvider) { uniqueName: uniqueName, setup: setup, fs: fs, + shell: shell, provider: provider, providers: { IndexedDB: IndexedDBTestProvider, diff --git a/tests/spec/shell/touch.spec.js b/tests/spec/shell/touch.spec.js new file mode 100644 index 0000000..ba86827 --- /dev/null +++ b/tests/spec/shell/touch.spec.js @@ -0,0 +1,104 @@ +define(["Filer", "util"], function(Filer, util) { + + function getTimes(fs, path, callback) { + fs.stat(path, function(error, stats) { + if(error) throw error; + callback({mtime: stats.mtime, atime: stats.atime}); + }); + } + + describe('FileSystemShell.touch', function() { + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should be a function', function() { + var shell = util.shell(); + expect(shell.touch).to.be.a('function'); + }); + + it('should create a new file if path does not exist', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + shell.touch('/newfile', function(error) { + if(error) throw error; + + fs.stat('/newfile', function(error, stats) { + expect(error).not.to.exist; + expect(stats.type).to.equal('FILE'); + done(); + }); + }); + }); + + it('should skip creating a new file if options.create is false', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + shell.touch('/newfile', { create: false }, function(error) { + if(error) throw error; + + fs.stat('/newfile', function(error, stats) { + expect(error).to.exist; + done(); + }); + }); + }); + + it('should update times if path does exist', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var atime = Date.parse('1 Oct 2000 15:33:22'); + var mtime = Date.parse('30 Sep 2000 06:43:54'); + + fs.open('/newfile', 'w', function (error, fd) { + if (error) throw error; + + fs.futimes(fd, atime, mtime, function (error) { + if(error) throw error; + + fs.close(fd, function(error) { + if(error) throw error; + + getTimes(fs, '/newfile', function(times1) { + shell.touch('/newfile', function(error) { + expect(error).not.to.exist; + + getTimes(fs, '/newfile', function(times2) { + expect(times2.mtime).to.be.above(times1.mtime); + expect(times2.atime).to.be.above(times1.atime); + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should update times to specified date if path does exist', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var date = Date.parse('1 Oct 2001 15:33:22'); + + fs.open('/newfile', 'w', function (error, fd) { + if (error) throw error; + + fs.close(fd, function(error) { + if(error) throw error; + + shell.touch('/newfile', { date: date }, function(error) { + expect(error).not.to.exist; + + getTimes(fs, '/newfile', function(times) { + expect(times.mtime).to.equal(date); + expect(times.atime).to.equal(date); + done(); + }); + }); + }); + }); + }); + }); + +}); diff --git a/tests/test-manifest.js b/tests/test-manifest.js index 73e8ae5..f7f77e4 100644 --- a/tests/test-manifest.js +++ b/tests/test-manifest.js @@ -42,6 +42,9 @@ define([ "spec/adapters/adapters.spec", "spec/adapters/adapters.general.spec", + // Filer.FileSystemShell.* + "spec/shell/touch.spec", + // Ported node.js tests (filenames match names in https://github.com/joyent/node/tree/master/test) "spec/node-js/simple/test-fs-mkdir", "spec/node-js/simple/test-fs-null-bytes", From 263b9cd9b0a6f776d76b674ab5cf441bb8e70a96 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Sat, 15 Feb 2014 15:28:00 -0500 Subject: [PATCH 03/21] Add shell.exec with test --- src/shell.js | 68 ++++++++++++++++++++-------------- tests/spec/shell/exec.spec.js | 33 +++++++++++++++++ tests/spec/shell/touch.spec.js | 4 +- tests/test-manifest.js | 1 + 4 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 tests/spec/shell/exec.spec.js diff --git a/src/shell.js b/src/shell.js index c1f18b5..06957ea 100644 --- a/src/shell.js +++ b/src/shell.js @@ -2,8 +2,6 @@ define(function(require) { var Path = require('src/path'); - - function Shell(fs, options) { options = options || {}; @@ -22,36 +20,55 @@ define(function(require) { // We include `cd` on the this vs. proto so that // we can access cwd without exposing it externally. this.cd = function(path) { - + this.cwd = Path.resolve(this.cwd, path); }; } - Shell.prototype.ls = function(path) { - - }; - - Shell.prototype.rm = function(path, options, callback) { - - }; - - Shell.prototype.mv = function(path) { - - }; - - Shell.prototype.cp = function(path) { - - }; - - Shell.prototype.mkdir = function(path) { + /** + * Execute the .js command located at `path`. Such commands + * should assume the existence of 3 arguments, which will be + * defined at runtime: + * + * * options - an object containing any arguments, data, etc. + * * callback - a callback function(error, result) to call when done. + * + * The .js command's contents should be the body of a function + * that looks like this: + * + * function(fs, options, callback) { + * // .js code here + * } + */ + Shell.prototype.exec = function(path, options, callback) { + var fs = this.fs; + if(typeof options === 'function') { + callback = options; + options = {}; + } + options = options || {}; + callback = callback || function(){}; + path = Path.resolve(this.cwd, path); + fs.readFile(path, "utf8", function(error, data) { + if(error) { + callback(error); + return; + } + try { + var cmd = new Function('fs', 'options', 'callback', data); + cmd(fs, options, callback); + } catch(e) { + callback(e); + } + }); }; /** * Create a file if it does not exist, or update access and * modified times if it does. Valid options include: * - * * create - whether to create the file if missing (defaults to true) + * * updateOnly - whether to create the file if missing (defaults to false) * * date - use the provided Date value instead of current date/time */ Shell.prototype.touch = function(path, options, callback) { @@ -60,6 +77,8 @@ define(function(require) { callback = options; options = {}; } + options = options || {}; + callback = callback || function(){}; path = Path.resolve(this.cwd, path); function createFile(path) { @@ -80,8 +99,7 @@ define(function(require) { fs.stat(path, function(error, stats) { if(error) { - // Skip file creation if create is `false` - if(options.create === false) { + if(options.updateOnly === true) { callback(); } else { createFile(path); @@ -92,10 +110,6 @@ define(function(require) { }); }; - Shell.prototype.ln = function(path) { - - }; - return Shell; }); diff --git a/tests/spec/shell/exec.spec.js b/tests/spec/shell/exec.spec.js new file mode 100644 index 0000000..6f325a0 --- /dev/null +++ b/tests/spec/shell/exec.spec.js @@ -0,0 +1,33 @@ +define(["Filer", "util"], function(Filer, util) { + + describe('FileSystemShell.exec', function() { + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should be a function', function() { + var shell = util.shell(); + expect(shell.exec).to.be.a('function'); + }); + + it('should be able to execute a command .js file from the filesystem', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var cmdString = "fs.writeFile(options.path, options.data, callback);"; + + fs.writeFile('/cmd.js', cmdString, function(error) { + if(error) throw error; + + shell.exec('/cmd.js', {path: '/test', data: 'hello world'}, function(error, result) { + if(error) throw error; + + fs.readFile('/test', 'utf8', function(error, data) { + if(error) throw error; + expect(data).to.equal('hello world'); + done(); + }); + }); + }); + }); + }); + +}); diff --git a/tests/spec/shell/touch.spec.js b/tests/spec/shell/touch.spec.js index ba86827..3b3a12a 100644 --- a/tests/spec/shell/touch.spec.js +++ b/tests/spec/shell/touch.spec.js @@ -31,11 +31,11 @@ define(["Filer", "util"], function(Filer, util) { }); }); - it('should skip creating a new file if options.create is false', function(done) { + it('should skip creating a new file if options.updateOnly is true', function(done) { var fs = util.fs(); var shell = fs.Shell(); - shell.touch('/newfile', { create: false }, function(error) { + shell.touch('/newfile', { updateOnly: true }, function(error) { if(error) throw error; fs.stat('/newfile', function(error, stats) { diff --git a/tests/test-manifest.js b/tests/test-manifest.js index f7f77e4..3faf91d 100644 --- a/tests/test-manifest.js +++ b/tests/test-manifest.js @@ -44,6 +44,7 @@ define([ // Filer.FileSystemShell.* "spec/shell/touch.spec", + "spec/shell/exec.spec", // Ported node.js tests (filenames match names in https://github.com/joyent/node/tree/master/test) "spec/node-js/simple/test-fs-mkdir", From 2d3b15d31074a61626c91e359bc45ff03044a4d8 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Sat, 15 Feb 2014 15:43:38 -0500 Subject: [PATCH 04/21] Deal with jshint hating the Function constructor --- gruntfile.js | 19 +++++++++++-------- src/shell.js | 1 + 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/gruntfile.js b/gruntfile.js index 1b627ad..13fa88e 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -18,14 +18,17 @@ module.exports = function(grunt) { jshint: { // Don't bother with src/path.js - all: ['gruntfile.js', - 'src/constants.js', - 'src/error.js', - 'src/fs.js', - 'src/shared.js', - 'src/providers/**/*.js', - 'src/adapters/**/*.js' - ] + all: [ + 'gruntfile.js', + 'src/constants.js', + 'src/error.js', + 'src/fs.js', + 'src/index.js', + 'src/shared.js', + 'src/shell.js', + 'src/providers/**/*.js', + 'src/adapters/**/*.js' + ] }, mocha: { diff --git a/src/shell.js b/src/shell.js index 06957ea..c53699a 100644 --- a/src/shell.js +++ b/src/shell.js @@ -1,3 +1,4 @@ +/* jshint evil:true */ define(function(require) { var Path = require('src/path'); From d7cce709f965f2bf16c6d8f5be4d451330ccc3e7 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Mon, 17 Feb 2014 10:47:47 -0500 Subject: [PATCH 05/21] Fix shell.cd and add tests --- src/shell.js | 29 ++++++---- tests/spec/shell/cd.spec.js | 104 ++++++++++++++++++++++++++++++++++++ tests/test-manifest.js | 1 + 3 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 tests/spec/shell/cd.spec.js diff --git a/src/shell.js b/src/shell.js index c53699a..4b31b81 100644 --- a/src/shell.js +++ b/src/shell.js @@ -2,11 +2,12 @@ define(function(require) { var Path = require('src/path'); + var Error = require('src/error'); function Shell(fs, options) { options = options || {}; - var cwd = options.cwd || '/'; + var cwd = '/'; Object.defineProperty(this, 'fs', { get: function() { return fs; }, @@ -20,10 +21,22 @@ define(function(require) { // We include `cd` on the this vs. proto so that // we can access cwd without exposing it externally. - this.cd = function(path) { - this.cwd = Path.resolve(this.cwd, path); + this.cd = function(path, callback) { + path = Path.resolve(this.cwd, path); + // Make sure the path actually exists, and is a dir + fs.stat(path, function(err, stats) { + if(err) { + callback(new Error.ENotDirectory()); + return; + } + if(stats.type === 'DIRECTORY') { + cwd = path; + callback(); + } else { + callback(new Error.ENotDirectory()); + } + }); }; - } /** @@ -83,9 +96,7 @@ define(function(require) { path = Path.resolve(this.cwd, path); function createFile(path) { - fs.writeFile(path, '', function(error) { - callback(error); - }); + fs.writeFile(path, '', callback); } function updateTimes(path) { @@ -93,9 +104,7 @@ define(function(require) { var atime = options.date || now; var mtime = options.date || now; - fs.utimes(path, atime, mtime, function(error) { - callback(error); - }); + fs.utimes(path, atime, mtime, callback); } fs.stat(path, function(error, stats) { diff --git a/tests/spec/shell/cd.spec.js b/tests/spec/shell/cd.spec.js new file mode 100644 index 0000000..8f01264 --- /dev/null +++ b/tests/spec/shell/cd.spec.js @@ -0,0 +1,104 @@ +define(["Filer", "util"], function(Filer, util) { + + describe('FileSystemShell.cd', function() { + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should be a function', function() { + var shell = util.shell(); + expect(shell.cd).to.be.a('function'); + }); + + it('should default to a cwd of /', function() { + var shell = util.shell(); + expect(shell.cwd).to.equal('/'); + }); + + it('should allow changing the path to a valid dir', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(err) { + if(err) throw err; + + expect(shell.cwd).to.equal('/'); + shell.cd('/dir', function(err) { + expect(err).not.to.exist; + expect(shell.cwd).to.equal('/dir'); + done(); + }); + }); + }); + + it('should fail when changing the path to an invalid dir', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(err) { + if(err) throw err; + + expect(shell.cwd).to.equal('/'); + shell.cd('/nodir', function(err) { + expect(err).to.exist; + expect(err.name).to.equal('ENotDirectory'); + expect(shell.cwd).to.equal('/'); + done(); + }); + }); + }); + + it('should fail when changing the path to a file', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.writeFile('/file', 'file', function(err) { + if(err) throw err; + + expect(shell.cwd).to.equal('/'); + shell.cd('/file', function(err) { + expect(err).to.exist; + expect(err.name).to.equal('ENotDirectory'); + expect(shell.cwd).to.equal('/'); + done(); + }); + }); + }); + + it('should allow relative paths for a valid dir', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(err) { + if(err) throw err; + + expect(shell.cwd).to.equal('/'); + shell.cd('./dir', function(err) { + expect(err).not.to.exist; + expect(shell.cwd).to.equal('/dir'); + done(); + }); + }); + }); + + it('should allow .. in paths for a valid dir', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(err) { + if(err) throw err; + + expect(shell.cwd).to.equal('/'); + shell.cd('./dir', function(err) { + expect(err).not.to.exist; + expect(shell.cwd).to.equal('/dir'); + shell.cd('..', function(err) { + expect(err).not.to.exist; + expect(shell.cwd).to.equal('/'); + done(); + }); + }); + }); + }); + + }); +}); diff --git a/tests/test-manifest.js b/tests/test-manifest.js index 3faf91d..2fa4f63 100644 --- a/tests/test-manifest.js +++ b/tests/test-manifest.js @@ -43,6 +43,7 @@ define([ "spec/adapters/adapters.general.spec", // Filer.FileSystemShell.* + "spec/shell/cd.spec", "spec/shell/touch.spec", "spec/shell/exec.spec", From ef095267402327eddc89d218fe9bedd6bfd3b880 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Mon, 17 Feb 2014 16:55:36 -0500 Subject: [PATCH 06/21] Add cd test for symlink'ed dir --- tests/spec/shell/cd.spec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/spec/shell/cd.spec.js b/tests/spec/shell/cd.spec.js index 8f01264..151b703 100644 --- a/tests/spec/shell/cd.spec.js +++ b/tests/spec/shell/cd.spec.js @@ -100,5 +100,24 @@ define(["Filer", "util"], function(Filer, util) { }); }); + it('should follow symlinks to dirs', function(done) { + var fs = util.fs(); + + fs.mkdir('/dir', function(error) { + if(error) throw error; + + fs.symlink('/dir', '/link', function(error) { + if(error) throw error; + + var shell = fs.Shell(); + shell.cd('link', function(error) { + expect(error).not.to.exist; + expect(shell.cwd).to.equal('/link'); + done(); + }); + }); + }); + }); + }); }); From 2829079bd318a4ef7943b287efe4eb070bf3d0d5 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Tue, 18 Feb 2014 14:51:06 -0500 Subject: [PATCH 07/21] Add Shell.cat() with tests --- lib/async.js | 964 +++++++++++++++++++++++++++++++++++ src/shell.js | 45 +- tests/spec/shell/cat.spec.js | 82 +++ tests/test-manifest.js | 1 + 4 files changed, 1089 insertions(+), 3 deletions(-) create mode 100644 lib/async.js create mode 100644 tests/spec/shell/cat.spec.js diff --git a/lib/async.js b/lib/async.js new file mode 100644 index 0000000..2f52b6c --- /dev/null +++ b/lib/async.js @@ -0,0 +1,964 @@ +/*global setImmediate: false, setTimeout: false, console: false */ + +/** + * https://raw.github.com/caolan/async/master/lib/async.js Feb 18, 2014 + * Used under MIT - https://github.com/caolan/async/blob/master/LICENSE + */ + +(function () { + + var async = {}; + + // global on the server, window in the browser + var root, previous_async; + + root = this; + if (root != null) { + previous_async = root.async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + function only_once(fn) { + var called = false; + return function() { + if (called) throw new Error("Callback was already called."); + called = true; + fn.apply(root, arguments); + } + } + + //// cross-browser compatiblity functions //// + + var _each = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _each(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _each(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + if (typeof setImmediate === 'function') { + async.nextTick = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + async.setImmediate = async.nextTick; + } + else { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + async.setImmediate = async.nextTick; + } + } + else { + async.nextTick = process.nextTick; + if (typeof setImmediate !== 'undefined') { + async.setImmediate = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + } + else { + async.setImmediate = async.nextTick; + } + } + + async.each = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + _each(arr, function (x) { + iterator(x, only_once(function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + } + })); + }); + }; + async.forEach = async.each; + + async.eachSeries = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + async.forEachSeries = async.eachSeries; + + async.eachLimit = function (arr, limit, iterator, callback) { + var fn = _eachLimit(limit); + fn.apply(null, [arr, iterator, callback]); + }; + async.forEachLimit = async.eachLimit; + + var _eachLimit = function (limit) { + + return function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed >= arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + started += 1; + running += 1; + iterator(arr[started - 1], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed >= arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + } + })(); + }; + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.each].concat(args)); + }; + }; + var doParallelLimit = function(limit, fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [_eachLimit(limit)].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.eachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + async.mapLimit = function (arr, limit, iterator, callback) { + return _mapLimit(limit)(arr, iterator, callback); + }; + + var _mapLimit = function(limit) { + return doParallelLimit(limit, _asyncMap); + }; + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.eachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + if (!keys.length) { + return callback(null); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + _each(listeners.slice(0), function (fn) { + fn(); + }); + }; + + addListener(function () { + if (_keys(results).length === keys.length) { + callback(null, results); + callback = function () {}; + } + }); + + _each(keys, function (k) { + var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k]; + var taskCallback = function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + if (err) { + var safeResults = {}; + _each(_keys(results), function(rkey) { + safeResults[rkey] = results[rkey]; + }); + safeResults[k] = args; + callback(err, safeResults); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + results[k] = args; + async.setImmediate(taskComplete); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true) && !results.hasOwnProperty(k); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.waterfall = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor !== Array) { + var err = new Error('First argument to waterfall must be an array of functions'); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback.apply(null, arguments); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.setImmediate(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + var _parallel = function(eachfn, tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + eachfn.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + eachfn.each(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.parallel = function (tasks, callback) { + _parallel({ map: async.map, each: async.each }, tasks, callback); + }; + + async.parallelLimit = function(tasks, limit, callback) { + _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.eachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doWhilst = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (test()) { + async.doWhilst(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doUntil = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (!test()) { + async.doUntil(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.queue = function (worker, concurrency) { + if (concurrency === undefined) { + concurrency = 1; + } + function _insert(q, data, pos, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + var item = { + data: task, + callback: typeof callback === 'function' ? callback : null + }; + + if (pos) { + q.tasks.unshift(item); + } else { + q.tasks.push(item); + } + + if (q.saturated && q.tasks.length === concurrency) { + q.saturated(); + } + async.setImmediate(q.process); + }); + } + + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + _insert(q, data, false, callback); + }, + unshift: function (data, callback) { + _insert(q, data, true, callback); + }, + process: function () { + if (workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if (q.empty && q.tasks.length === 0) { + q.empty(); + } + workers += 1; + var next = function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if (q.drain && q.tasks.length + workers === 0) { + q.drain(); + } + q.process(); + }; + var cb = only_once(next); + worker(task.data, cb); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + } + }; + return q; + }; + + async.cargo = function (worker, payload) { + var working = false, + tasks = []; + + var cargo = { + tasks: tasks, + payload: payload, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + tasks.push({ + data: task, + callback: typeof callback === 'function' ? callback : null + }); + if (cargo.saturated && tasks.length === payload) { + cargo.saturated(); + } + }); + async.setImmediate(cargo.process); + }, + process: function process() { + if (working) return; + if (tasks.length === 0) { + if(cargo.drain) cargo.drain(); + return; + } + + var ts = typeof payload === 'number' + ? tasks.splice(0, payload) + : tasks.splice(0); + + var ds = _map(ts, function (task) { + return task.data; + }); + + if(cargo.empty) cargo.empty(); + working = true; + worker(ds, function () { + working = false; + + var args = arguments; + _each(ts, function (data) { + if (data.callback) { + data.callback.apply(null, args); + } + }); + + process(); + }); + }, + length: function () { + return tasks.length; + }, + running: function () { + return working; + } + }; + return cargo; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _each(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + callback.apply(null, memo[key]); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.memo = memo; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + }; + }; + + async.times = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.map(counter, iterator, callback); + }; + + async.timesSeries = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.mapSeries(counter, iterator, callback); + }; + + async.compose = function (/* functions... */) { + var fns = Array.prototype.reverse.call(arguments); + return function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + async.reduce(fns, args, function (newargs, fn, cb) { + fn.apply(that, newargs.concat([function () { + var err = arguments[0]; + var nextargs = Array.prototype.slice.call(arguments, 1); + cb(err, nextargs); + }])) + }, + function (err, results) { + callback.apply(that, [err].concat(results)); + }); + }; + }; + + var _applyEach = function (eachfn, fns /*args...*/) { + var go = function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + return eachfn(fns, function (fn, cb) { + fn.apply(that, args.concat([cb])); + }, + callback); + }; + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + return go.apply(this, args); + } + else { + return go; + } + }; + async.applyEach = doParallel(_applyEach); + async.applyEachSeries = doSeries(_applyEach); + + async.forever = function (fn, callback) { + function next(err) { + if (err) { + if (callback) { + return callback(err); + } + throw err; + } + fn(next); + } + next(); + }; + + // AMD / RequireJS + if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return async; + }); + } + // Node.js + else if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + // included directly via