From f1b7c96686670487bc4f5d2ba9e220b342112657 Mon Sep 17 00:00:00 2001 From: Louis Chatriot Date: Thu, 11 Jul 2013 12:14:42 +0200 Subject: [PATCH] Now with support for regular expressions --- lib/model.js | 13 +- out.js | 6466 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- test/model.test.js | 27 + 4 files changed, 6505 insertions(+), 3 deletions(-) create mode 100644 out.js diff --git a/lib/model.js b/lib/model.js index ba70bf2..a29a0dc 100644 --- a/lib/model.js +++ b/lib/model.js @@ -415,6 +415,15 @@ function getDotValue (obj, field) { function areThingsEqual (a, b) { var aKeys , bKeys , i; + // String against regexp + if (util.isRegExp(b)) { + if (typeof a !== 'string') { + return false + } else { + return b.test(a); + } + } + // Strings, booleans, numbers, null if (a === null || typeof a === 'string' || typeof a === 'boolean' || typeof a === 'number' || b === null || typeof b === 'string' || typeof b === 'boolean' || typeof b === 'number') { return a === b; } @@ -426,7 +435,7 @@ function areThingsEqual (a, b) { // undefined (no match since they mean field doesn't exist and can't be serialized) if (util.isArray(a) || util.isArray(b) || a === undefined || b === undefined) { return false; } - // Objects (check for deep equality) + // General objects (check for deep equality) // a and b should be objects at this point try { aKeys = Object.keys(a); @@ -609,7 +618,7 @@ function matchQueryPart (obj, queryKey, queryValue) { // queryValue is an actual object. Determine whether it contains comparison operators // or only normal fields. Mixed objects are not allowed - if (queryValue !== null && typeof queryValue === 'object') { + if (queryValue !== null && typeof queryValue === 'object' && !util.isRegExp(queryValue)) { keys = Object.keys(queryValue); firstChars = _.map(keys, function (item) { return item[0]; }); dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; }); diff --git a/out.js b/out.js new file mode 100644 index 0000000..2946fa4 --- /dev/null +++ b/out.js @@ -0,0 +1,6466 @@ +;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s 0) { + var fn = queue.shift(); + fn(); + } + } + }, true); + + return function nextTick(fn) { + queue.push(fn); + window.postMessage('process-tick', '*'); + }; + } + + return function nextTick(fn) { + setTimeout(fn, 0); + }; +})(); + +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +} + +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; + +},{}],5:[function(require,module,exports){ +(function(process){function filter (xs, fn) { + var res = []; + for (var i = 0; i < xs.length; i++) { + if (fn(xs[i], i, xs)) res.push(xs[i]); + } + return res; +} + +// resolves . and .. elements in a path array with directory names there +// must be no slashes, empty elements, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish +// relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length; i >= 0; i--) { + var last = parts[i]; + if (last == '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + +// Regex to split a filename into [*, dir, basename, ext] +// posix version +var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/; + +// path.resolve([from ...], to) +// posix version +exports.resolve = function() { +var resolvedPath = '', + resolvedAbsolute = false; + +for (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) + ? arguments[i] + : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string' || !path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; +} + +// At this point the path should be resolved to a full absolute path, but +// handle relative paths to be safe (might happen when process.cwd() fails) + +// Normalize the path +resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; +}; + +// path.normalize(path) +// posix version +exports.normalize = function(path) { +var isAbsolute = path.charAt(0) === '/', + trailingSlash = path.slice(-1) === '/'; + +// Normalize the path +path = normalizeArray(filter(path.split('/'), function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; +}; + + +// posix version +exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(filter(paths, function(p, index) { + return p && typeof p === 'string'; + }).join('/')); +}; + + +exports.dirname = function(path) { + var dir = splitPathRe.exec(path)[1] || ''; + var isWindows = false; + if (!dir) { + // No dirname + return '.'; + } else if (dir.length === 1 || + (isWindows && dir.length <= 3 && dir.charAt(1) === ':')) { + // It is just a slash or a drive letter with a slash + return dir; + } else { + // It is a full dirname, strip trailing slash + return dir.substring(0, dir.length - 1); + } +}; + + +exports.basename = function(path, ext) { + var f = splitPathRe.exec(path)[2] || ''; + // TODO: make this comparison case-insensitive on windows? + if (ext && f.substr(-1 * ext.length) === ext) { + f = f.substr(0, f.length - ext.length); + } + return f; +}; + + +exports.extname = function(path) { + return splitPathRe.exec(path)[3] || ''; +}; + +exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); +}; + +})(require("__browserify_process")) +},{"__browserify_process":4}],6:[function(require,module,exports){ +var events = require('events'); + +exports.isArray = isArray; +exports.isDate = function(obj){return Object.prototype.toString.call(obj) === '[object Date]'}; +exports.isRegExp = function(obj){return Object.prototype.toString.call(obj) === '[object RegExp]'}; + + +exports.print = function () {}; +exports.puts = function () {}; +exports.debug = function() {}; + +exports.inspect = function(obj, showHidden, depth, colors) { + var seen = []; + + var stylize = function(str, styleType) { + // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics + var styles = + { 'bold' : [1, 22], + 'italic' : [3, 23], + 'underline' : [4, 24], + 'inverse' : [7, 27], + 'white' : [37, 39], + 'grey' : [90, 39], + 'black' : [30, 39], + 'blue' : [34, 39], + 'cyan' : [36, 39], + 'green' : [32, 39], + 'magenta' : [35, 39], + 'red' : [31, 39], + 'yellow' : [33, 39] }; + + var style = + { 'special': 'cyan', + 'number': 'blue', + 'boolean': 'yellow', + 'undefined': 'grey', + 'null': 'bold', + 'string': 'green', + 'date': 'magenta', + // "name": intentionally not styling + 'regexp': 'red' }[styleType]; + + if (style) { + return '\033[' + styles[style][0] + 'm' + str + + '\033[' + styles[style][1] + 'm'; + } else { + return str; + } + }; + if (! colors) { + stylize = function(str, styleType) { return str; }; + } + + function format(value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + if (value && typeof value.inspect === 'function' && + // Filter out the util module, it's inspect function is special + value !== exports && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + return value.inspect(recurseTimes); + } + + // Primitive types cannot have properties + switch (typeof value) { + case 'undefined': + return stylize('undefined', 'undefined'); + + case 'string': + var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return stylize(simple, 'string'); + + case 'number': + return stylize('' + value, 'number'); + + case 'boolean': + return stylize('' + value, 'boolean'); + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return stylize('null', 'null'); + } + + // Look up the keys of the object. + var visible_keys = Object_keys(value); + var keys = showHidden ? Object_getOwnPropertyNames(value) : visible_keys; + + // Functions without properties can be shortcutted. + if (typeof value === 'function' && keys.length === 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + var name = value.name ? ': ' + value.name : ''; + return stylize('[Function' + name + ']', 'special'); + } + } + + // Dates without properties can be shortcutted + if (isDate(value) && keys.length === 0) { + return stylize(value.toUTCString(), 'date'); + } + + var base, type, braces; + // Determine the object type + if (isArray(value)) { + type = 'Array'; + braces = ['[', ']']; + } else { + type = 'Object'; + braces = ['{', '}']; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + var n = value.name ? ': ' + value.name : ''; + base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; + } else { + base = ''; + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ' ' + value.toUTCString(); + } + + if (keys.length === 0) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + return stylize('[Object]', 'special'); + } + } + + seen.push(value); + + var output = keys.map(function(key) { + var name, str; + if (value.__lookupGetter__) { + if (value.__lookupGetter__(key)) { + if (value.__lookupSetter__(key)) { + str = stylize('[Getter/Setter]', 'special'); + } else { + str = stylize('[Getter]', 'special'); + } + } else { + if (value.__lookupSetter__(key)) { + str = stylize('[Setter]', 'special'); + } + } + } + if (visible_keys.indexOf(key) < 0) { + name = '[' + key + ']'; + } + if (!str) { + if (seen.indexOf(value[key]) < 0) { + if (recurseTimes === null) { + str = format(value[key]); + } else { + str = format(value[key], recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (isArray(value)) { + str = str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n').substr(2); + } else { + str = '\n' + str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n'); + } + } + } else { + str = stylize('[Circular]', 'special'); + } + } + if (typeof name === 'undefined') { + if (type === 'Array' && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = stylize(name, 'string'); + } + } + + return name + ': ' + str; + }); + + seen.pop(); + + var numLinesEst = 0; + var length = output.reduce(function(prev, cur) { + numLinesEst++; + if (cur.indexOf('\n') >= 0) numLinesEst++; + return prev + cur.length + 1; + }, 0); + + if (length > 50) { + output = braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + + } else { + output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; + } + + return output; + } + return format(obj, (typeof depth === 'undefined' ? 2 : depth)); +}; + + +function isArray(ar) { + return ar instanceof Array || + Array.isArray(ar) || + (ar && ar !== Object.prototype && isArray(ar.__proto__)); +} + + +function isRegExp(re) { + return re instanceof RegExp || + (typeof re === 'object' && Object.prototype.toString.call(re) === '[object RegExp]'); +} + + +function isDate(d) { + if (d instanceof Date) return true; + if (typeof d !== 'object') return false; + var properties = Date.prototype && Object_getOwnPropertyNames(Date.prototype); + var proto = d.__proto__ && Object_getOwnPropertyNames(d.__proto__); + return JSON.stringify(proto) === JSON.stringify(properties); +} + +function pad(n) { + return n < 10 ? '0' + n.toString(10) : n.toString(10); +} + +var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; + +// 26 Feb 16:19:34 +function timestamp() { + var d = new Date(); + var time = [pad(d.getHours()), + pad(d.getMinutes()), + pad(d.getSeconds())].join(':'); + return [d.getDate(), months[d.getMonth()], time].join(' '); +} + +exports.log = function (msg) {}; + +exports.pump = null; + +var Object_keys = Object.keys || function (obj) { + var res = []; + for (var key in obj) res.push(key); + return res; +}; + +var Object_getOwnPropertyNames = Object.getOwnPropertyNames || function (obj) { + var res = []; + for (var key in obj) { + if (Object.hasOwnProperty.call(obj, key)) res.push(key); + } + return res; +}; + +var Object_create = Object.create || function (prototype, properties) { + // from es5-shim + var object; + if (prototype === null) { + object = { '__proto__' : null }; + } + else { + if (typeof prototype !== 'object') { + throw new TypeError( + 'typeof prototype[' + (typeof prototype) + '] != \'object\'' + ); + } + var Type = function () {}; + Type.prototype = prototype; + object = new Type(); + object.__proto__ = prototype; + } + if (typeof properties !== 'undefined' && Object.defineProperties) { + Object.defineProperties(object, properties); + } + return object; +}; + +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object_create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +var formatRegExp = /%[sdj%]/g; +exports.format = function(f) { + if (typeof f !== 'string') { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(exports.inspect(arguments[i])); + } + return objects.join(' '); + } + + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function(x) { + if (x === '%%') return '%'; + if (i >= len) return x; + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': return JSON.stringify(args[i++]); + default: + return x; + } + }); + for(var x = args[i]; i < len; x = args[++i]){ + if (x === null || typeof x !== 'object') { + str += ' ' + x; + } else { + str += ' ' + exports.inspect(x); + } + } + return str; +}; + +},{"events":7}],7:[function(require,module,exports){ +(function(process){if (!process.EventEmitter) process.EventEmitter = function () {}; + +var EventEmitter = exports.EventEmitter = process.EventEmitter; +var isArray = typeof Array.isArray === 'function' + ? Array.isArray + : function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]' + } +; +function indexOf (xs, x) { + if (xs.indexOf) return xs.indexOf(x); + for (var i = 0; i < xs.length; i++) { + if (x === xs[i]) return i; + } + return -1; +} + +// By default EventEmitters will print a warning if more than +// 10 listeners are added to it. This is a useful default which +// helps finding memory leaks. +// +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +var defaultMaxListeners = 10; +EventEmitter.prototype.setMaxListeners = function(n) { + if (!this._events) this._events = {}; + this._events.maxListeners = n; +}; + + +EventEmitter.prototype.emit = function(type) { + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events || !this._events.error || + (isArray(this._events.error) && !this._events.error.length)) + { + if (arguments[1] instanceof Error) { + throw arguments[1]; // Unhandled 'error' event + } else { + throw new Error("Uncaught, unspecified 'error' event."); + } + return false; + } + } + + if (!this._events) return false; + var handler = this._events[type]; + if (!handler) return false; + + if (typeof handler == 'function') { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + var args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + return true; + + } else if (isArray(handler)) { + var args = Array.prototype.slice.call(arguments, 1); + + var listeners = handler.slice(); + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + return true; + + } else { + return false; + } +}; + +// EventEmitter is defined in src/node_events.cc +// EventEmitter.prototype.emit() is also defined there. +EventEmitter.prototype.addListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('addListener only takes instances of Function'); + } + + if (!this._events) this._events = {}; + + // To avoid recursion in the case that type == "newListeners"! Before + // adding it to the listeners, first emit "newListeners". + this.emit('newListener', type, listener); + + if (!this._events[type]) { + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + } else if (isArray(this._events[type])) { + + // Check for listener leak + if (!this._events[type].warned) { + var m; + if (this._events.maxListeners !== undefined) { + m = this._events.maxListeners; + } else { + m = defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + console.trace(); + } + } + + // If we've already got an array, just append. + this._events[type].push(listener); + } else { + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + var self = this; + self.on(type, function g() { + self.removeListener(type, g); + listener.apply(this, arguments); + }); + + return this; +}; + +EventEmitter.prototype.removeListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('removeListener only takes instances of Function'); + } + + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events || !this._events[type]) return this; + + var list = this._events[type]; + + if (isArray(list)) { + var i = indexOf(list, listener); + if (i < 0) return this; + list.splice(i, 1); + if (list.length == 0) + delete this._events[type]; + } else if (this._events[type] === listener) { + delete this._events[type]; + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + if (arguments.length === 0) { + this._events = {}; + return this; + } + + // does not use listeners(), so no side effect of creating _events[type] + if (type && this._events && this._events[type]) this._events[type] = null; + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + if (!this._events) this._events = {}; + if (!this._events[type]) this._events[type] = []; + if (!isArray(this._events[type])) { + this._events[type] = [this._events[type]]; + } + return this._events[type]; +}; + +})(require("__browserify_process")) +},{"__browserify_process":4}],8:[function(require,module,exports){ +/** + * Handle models (i.e. docs) + * Serialization/deserialization + * Copying + */ + +var dateToJSON = function () { return { $$date: this.getTime() }; } + , originalDateToJSON = Date.prototype.toJSON + , util = require('util') + //, _ = require('underscore') + , _ = {} + , modifierFunctions = {} + , lastStepModifierFunctions = {} + , comparisonFunctions = {} + , logicalOperators = {} + ; + + +/** + * Check a key, throw an error if the key is non valid + * @param {String} k key + * @param {Model} v value, needed to treat the Date edge case + * Non-treatable edge cases here: if part of the object if of the form { $$date: number } or { $$deleted: true } + * Its serialized-then-deserialized version it will transformed into a Date object + * But you really need to want it to trigger such behaviour, even when warned not to use '$' at the beginning of the field names... + */ +function checkKey (k, v) { + if (k[0] === '$' && !(k === '$$date' && typeof v === 'number') && !(k === '$$deleted' && v === true)) { + throw 'Field names cannot begin with the $ character'; + } + + if (k.indexOf('.') !== -1) { + throw 'Field names cannot contain a .'; + } +} + + +/** + * Check a DB object and throw an error if it's not valid + * Works by applying the above checkKey function to all fields recursively + */ +function checkObject (obj) { + if (util.isArray(obj)) { + obj.forEach(function (o) { + checkObject(o); + }); + } + + if (typeof obj === 'object' && obj !== null) { + Object.keys(obj).forEach(function (k) { + checkKey(k, obj[k]); + checkObject(obj[k]); + }); + } +} + + +/** + * Serialize an object to be persisted to a one-line string + * For serialization/deserialization, we use the native JSON parser and not eval or Function + * That gives us less freedom but data entered in the database may come from users + * so eval and the like are not safe + * Accepted primitive types: Number, String, Boolean, Date, null + * Accepted secondary types: Objects, Arrays + */ +function serialize (obj) { + var res; + + // Keep track of the fact that this is a Date object + Date.prototype.toJSON = dateToJSON; + + res = JSON.stringify(obj, function (k, v) { + checkKey(k, v); + + if (typeof v === undefined) { return null; } + if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) { return v; } + + return v; + }); + + // Return Date to its original state + Date.prototype.toJSON = originalDateToJSON; + + return res; +} + + +/** + * From a one-line representation of an object generate by the serialize function + * Return the object itself + */ +function deserialize (rawData) { + return JSON.parse(rawData, function (k, v) { + if (k === '$$date') { return new Date(v); } + if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) { return v; } + if (v && v.$$date) { return v.$$date; } + + return v; + }); +} + + +/** + * Deep copy a DB object + */ +function deepCopy (obj) { + var res; + + if ( typeof obj === 'boolean' || + typeof obj === 'number' || + typeof obj === 'string' || + obj === null || + (util.isDate(obj)) ) { + return obj; + } + + if (util.isArray(obj)) { + res = []; + obj.forEach(function (o) { res.push(o); }); + return res; + } + + if (typeof obj === 'object') { + res = {}; + Object.keys(obj).forEach(function (k) { + res[k] = deepCopy(obj[k]); + }); + return res; + } + + return undefined; // For now everything else is undefined. We should probably throw an error instead +} + + +/** + * Utility functions for comparing things + * Assumes type checking was already done (a and b already have the same type) + * compareNSB works for numbers, strings and booleans + */ +function compareNSB (a, b) { + if (a < b) { return -1; } + if (a > b) { return 1; } + return 0; +} + +function compareArrays (a, b) { + var i, comp; + + for (i = 0; i < Math.min(a.length, b.length); i += 1) { + comp = compareThings(a[i], b[i]); + + if (comp !== 0) { return comp; } + } + + // Common section was identical, longest one wins + return compareNSB(a.length, b.length); +} + + +/** + * Compare { things U undefined } + * Things are defined as any native types (string, number, boolean, null, date) and objects + * We need to compare with undefined as it will be used in indexes + * In the case of objects and arrays, we compare the serialized versions + * If two objects dont have the same type, the (arbitrary) type hierarchy is: undefined, null, number, strings, boolean, dates, arrays, objects + * Return -1 if a < b, 1 if a > b and 0 if a = b (note that equality here is NOT the same as defined in areThingsEqual!) + */ +function compareThings (a, b) { + var aKeys, bKeys, comp, i; + + // undefined + if (a === undefined) { return b === undefined ? 0 : -1; } + if (b === undefined) { return a === undefined ? 0 : 1; } + + // null + if (a === null) { return b === null ? 0 : -1; } + if (b === null) { return a === null ? 0 : 1; } + + // Numbers + if (typeof a === 'number') { return typeof b === 'number' ? compareNSB(a, b) : -1; } + if (typeof b === 'number') { return typeof a === 'number' ? compareNSB(a, b) : 1; } + + // Strings + if (typeof a === 'string') { return typeof b === 'string' ? compareNSB(a, b) : -1; } + if (typeof b === 'string') { return typeof a === 'string' ? compareNSB(a, b) : 1; } + + // Booleans + if (typeof a === 'boolean') { return typeof b === 'boolean' ? compareNSB(a, b) : -1; } + if (typeof b === 'boolean') { return typeof a === 'boolean' ? compareNSB(a, b) : 1; } + + // Dates + if (util.isDate(a)) { return util.isDate(b) ? compareNSB(a.getTime(), b.getTime()) : -1; } + if (util.isDate(b)) { return util.isDate(a) ? compareNSB(a.getTime(), b.getTime()) : 1; } + + // Arrays (first element is most significant and so on) + if (util.isArray(a)) { return util.isArray(b) ? compareArrays(a, b) : -1; } + if (util.isArray(b)) { return util.isArray(a) ? compareArrays(a, b) : 1; } + + // Objects + aKeys = Object.keys(a).sort(); + bKeys = Object.keys(b).sort(); + + for (i = 0; i < Math.min(aKeys.length, bKeys.length); i += 1) { + comp = compareThings(a[aKeys[i]], b[bKeys[i]]); + + if (comp !== 0) { return comp; } + } + + return compareNSB(aKeys.length, bKeys.length); +} + + + +// ============================================================== +// Updating documents +// ============================================================== + +/** + * The signature of modifier functions is as follows + * Their structure is always the same: recursively follow the dot notation while creating + * the nested documents if needed, then apply the "last step modifier" + * @param {Object} obj The model to modify + * @param {String} field Can contain dots, in that case that means we will set a subfield recursively + * @param {Model} value + */ + +/** + * Set a field to a new value + */ +lastStepModifierFunctions.$set = function (obj, field, value) { + obj[field] = value; +}; + + +/** + * Push an element to the end of an array field + */ +lastStepModifierFunctions.$push = function (obj, field, value) { + // Create the array if it doesn't exist + if (!obj.hasOwnProperty(field)) { obj[field] = []; } + + if (!util.isArray(obj[field])) { throw "Can't $push an element on non-array values"; } + + if (value !== null && typeof value === 'object' && value.$each) { + if (Object.keys(value).length > 1) { throw "Can't use another field in conjunction with $each"; } + if (!util.isArray(value.$each)) { throw "$each requires an array value"; } + + value.$each.forEach(function (v) { + obj[field].push(v); + }); + } else { + obj[field].push(value); + } +}; + + +/** + * Add an element to an array field only if it is not already in it + * No modification if the element is already in the array + * Note that it doesn't check whether the original array contains duplicates + */ +lastStepModifierFunctions.$addToSet = function (obj, field, value) { + var addToSet = true; + + // Create the array if it doesn't exist + if (!obj.hasOwnProperty(field)) { obj[field] = []; } + + if (!util.isArray(obj[field])) { throw "Can't $addToSet an element on non-array values"; } + + if (value !== null && typeof value === 'object' && value.$each) { + if (Object.keys(value).length > 1) { throw "Can't use another field in conjunction with $each"; } + if (!util.isArray(value.$each)) { throw "$each requires an array value"; } + + value.$each.forEach(function (v) { + lastStepModifierFunctions.$addToSet(obj, field, v); + }); + } else { + obj[field].forEach(function (v) { + if (compareThings(v, value) === 0) { addToSet = false; } + }); + if (addToSet) { obj[field].push(value); } + } +}; + + +/** + * Remove the first or last element of an array + */ +lastStepModifierFunctions.$pop = function (obj, field, value) { + if (!util.isArray(obj[field])) { throw "Can't $pop an element from non-array values"; } + if (typeof value !== 'number') { throw value + " isn't an integer, can't use it with $pop"; } + if (value === 0) { return; } + + if (value > 0) { + obj[field] = obj[field].slice(0, obj[field].length - 1); + } else { + obj[field] = obj[field].slice(1); + } +}; + + +/** + * Increment a numeric field's value + */ +lastStepModifierFunctions.$inc = function (obj, field, value) { + if (typeof value !== 'number') { throw value + " must be a number"; } + + if (typeof obj[field] !== 'number') { + if (!_.has(obj, field)) { + obj[field] = value; + } else { + throw "Don't use the $inc modifier on non-number fields"; + } + } else { + obj[field] += value; + } +}; + +// Given its name, create the complete modifier function +function createModifierFunction (modifier) { + return function (obj, field, value) { + var fieldParts = typeof field === 'string' ? field.split('.') : field; + + if (fieldParts.length === 1) { + lastStepModifierFunctions[modifier](obj, field, value); + } else { + obj[fieldParts[0]] = obj[fieldParts[0]] || {}; + modifierFunctions[modifier](obj[fieldParts[0]], fieldParts.slice(1), value); + } + }; +} + +// Actually create all modifier functions +Object.keys(lastStepModifierFunctions).forEach(function (modifier) { + modifierFunctions[modifier] = createModifierFunction(modifier); +}); + + +/** + * Modify a DB object according to an update query + * For now the updateQuery only replaces the object + */ +function modify (obj, updateQuery) { + var keys = Object.keys(updateQuery) + , firstChars = _.map(keys, function (item) { return item[0]; }) + , dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; }) + , newDoc, modifiers + ; + + if (keys.indexOf('_id') !== -1 && updateQuery._id !== obj._id) { throw "You cannot change a document's _id"; } + + if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) { + throw "You cannot mix modifiers and normal fields"; + } + + if (dollarFirstChars.length === 0) { + // Simply replace the object with the update query contents + newDoc = deepCopy(updateQuery); + newDoc._id = obj._id; + } else { + // Apply modifiers + modifiers = _.uniq(keys); + newDoc = deepCopy(obj); + modifiers.forEach(function (m) { + var keys; + + if (!modifierFunctions[m]) { throw "Unknown modifier " + m; } + + try { + keys = Object.keys(updateQuery[m]); + } catch (e) { + throw "Modifier " + m + "'s argument must be an object"; + } + + keys.forEach(function (k) { + modifierFunctions[m](newDoc, k, updateQuery[m][k]); + }); + }); + } + + // Check result is valid and return it + checkObject(newDoc); + if (obj._id !== newDoc._id) { throw "You can't change a document's _id"; } + return newDoc; +}; + + +// ============================================================== +// Finding documents +// ============================================================== + +/** + * Get a value from object with dot notation + * @param {Object} obj + * @param {String} field + */ +function getDotValue (obj, field) { + var fieldParts = typeof field === 'string' ? field.split('.') : field; + + if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match + + if (fieldParts.length === 1) { + return obj[fieldParts[0]]; + } else { + return getDotValue(obj[fieldParts[0]], fieldParts.slice(1)); + } +} + + +/** + * Check whether 'things' are equal + * Things are defined as any native types (string, number, boolean, null, date) and objects + * In the case of object, we check deep equality + * Returns true if they are, false otherwise + */ +function areThingsEqual (a, b) { + var aKeys , bKeys , i; + + // Strings, booleans, numbers, null + if (a === null || typeof a === 'string' || typeof a === 'boolean' || typeof a === 'number' || + b === null || typeof b === 'string' || typeof b === 'boolean' || typeof b === 'number') { return a === b; } + + // Dates + if (util.isDate(a) || util.isDate(b)) { return util.isDate(a) && util.isDate(b) && a.getTime() === b.getTime(); } + + // Arrays (no match since arrays are used as a $in) + // undefined (no match since they mean field doesn't exist and can't be serialized) + if (util.isArray(a) || util.isArray(b) || a === undefined || b === undefined) { return false; } + + // Objects (check for deep equality) + // a and b should be objects at this point + try { + aKeys = Object.keys(a); + bKeys = Object.keys(b); + } catch (e) { + return false; + } + + if (aKeys.length !== bKeys.length) { return false; } + for (i = 0; i < aKeys.length; i += 1) { + if (bKeys.indexOf(aKeys[i]) === -1) { return false; } + if (!areThingsEqual(a[aKeys[i]], b[aKeys[i]])) { return false; } + } + return true; +} + + +/** + * Check that two values are comparable + */ +function areComparable (a, b) { + if (typeof a !== 'string' && typeof a !== 'number' && !util.isDate(a) && + typeof b !== 'string' && typeof b !== 'number' && !util.isDate(b)) { + return false; + } + + if (typeof a !== typeof b) { return false; } + + return true; +} + + +/** + * Arithmetic and comparison operators + * @param {Native value} a Value in the object + * @param {Native value} b Value in the query + */ +comparisonFunctions.$lt = function (a, b) { + return areComparable(a, b) && a < b; +}; + +comparisonFunctions.$lte = function (a, b) { + return areComparable(a, b) && a <= b; +}; + +comparisonFunctions.$gt = function (a, b) { + return areComparable(a, b) && a > b; +}; + +comparisonFunctions.$gte = function (a, b) { + return areComparable(a, b) && a >= b; +}; + +comparisonFunctions.$ne = function (a, b) { + if (!a) { return true; } + return !areThingsEqual(a, b); +}; + +comparisonFunctions.$in = function (a, b) { + var i; + + if (!util.isArray(b)) { throw "$in operator called with a non-array"; } + + for (i = 0; i < b.length; i += 1) { + if (areThingsEqual(a, b[i])) { return true; } + } + + return false; +}; + +comparisonFunctions.$nin = function (a, b) { + if (!util.isArray(b)) { throw "$nin operator called with a non-array"; } + + return !comparisonFunctions.$in(a, b); +}; + +comparisonFunctions.$exists = function (value, exists) { + if (exists || exists === '') { // This will be true for all values of exists except false, null, undefined and 0 + exists = true; // That's strange behaviour (we should only use true/false) but that's the way Mongo does it... + } else { + exists = false; + } + + if (value === undefined) { + return !exists + } else { + return exists; + } +}; + + +/** + * Match any of the subqueries + * @param {Model} obj + * @param {Array of Queries} query + */ +logicalOperators.$or = function (obj, query) { + var i; + + if (!util.isArray(query)) { throw "$or operator used without an array"; } + + for (i = 0; i < query.length; i += 1) { + if (match(obj, query[i])) { return true; } + } + + return false; +}; + + +/** + * Match all of the subqueries + * @param {Model} obj + * @param {Array of Queries} query + */ +logicalOperators.$and = function (obj, query) { + var i; + + if (!util.isArray(query)) { throw "$and operator used without an array"; } + + for (i = 0; i < query.length; i += 1) { + if (!match(obj, query[i])) { return false; } + } + + return true; +}; + + +/** + * Inverted match of the query + * @param {Model} obj + * @param {Query} query + */ +logicalOperators.$not = function (obj, query) { + return !match(obj, query); +}; + + +/** + * Tell if a given document matches a query + * @param {Object} obj Document to check + * @param {Object} query + */ +function match (obj, query) { + var queryKeys = Object.keys(query) + , i + ; + + for (i = 0; i < queryKeys.length; i += 1) { + if (!matchQueryPart(obj, queryKeys[i], query[queryKeys[i]])) { return false; } + } + + return true; +}; + + +/** + * Match an object against a specific { key: value } part of a query + */ +function matchQueryPart (obj, queryKey, queryValue) { + var objValue = getDotValue(obj, queryKey) + , i + , keys, firstChars, dollarFirstChars + ; + + // Query part begins with a logical operator: apply it + if (queryKey[0] === '$') { + if (!logicalOperators[queryKey]) { throw "Unknown logical operator " + queryKey; } + + return logicalOperators[queryKey](obj, queryValue); + } + + // Check if the object value is an array treat it as an array of { obj, query } + // Where there needs to be at least one match + if (util.isArray(objValue)) { + for (i = 0; i < objValue.length; i += 1) { + if (matchQueryPart({ k: objValue[i] }, 'k', queryValue)) { return true; } // k here could be any string + } + return false; + } + + // queryValue is an actual object. Determine whether it contains comparison operators + // or only normal fields. Mixed objects are not allowed + if (queryValue !== null && typeof queryValue === 'object') { + keys = Object.keys(queryValue); + firstChars = _.map(keys, function (item) { return item[0]; }); + dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; }); + + if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) { + throw "You cannot mix operators and normal fields"; + } + + // queryValue is an object of this form: { $comparisonOperator1: value1, ... } + if (dollarFirstChars.length > 0) { + for (i = 0; i < keys.length; i += 1) { + if (!comparisonFunctions[keys[i]]) { throw "Unknown comparison function " + keys[i]; } + + if (!comparisonFunctions[keys[i]](objValue, queryValue[keys[i]])) { return false; } + } + return true; + } + } + + // queryValue is either a native value or a normal object + // Simple matching is possible + if (!areThingsEqual(objValue, queryValue)) { return false; } + + return true; +} + + +// Interface +module.exports.serialize = serialize; +module.exports.deserialize = deserialize; +module.exports.deepCopy = deepCopy; +module.exports.checkObject = checkObject; +module.exports.modify = modify; +module.exports.getDotValue = getDotValue; +module.exports.match = match; +module.exports.areThingsEqual = areThingsEqual; +module.exports.compareThings = compareThings; + +},{"util":6}],2:[function(require,module,exports){ +var fs = require('fs') + , path = require('path') + , customUtils = require('./customUtils') + , model = require('./model') + , async = require('async') + , Executor = require('./executor') + , Index = require('./indexes') + , util = require('util') + //, _ = require('underscore') + , _ = {} + ; + + +/** + * Create a new collection + * @param {String} options.filename Optional, datastore will be in-memory only if not provided + * @param {Boolean} options.inMemoryOnly Optional, default to false + * @param {Boolean} options.autoload Optional, defaults to false + * @param {Boolean} options.pipeline DEPRECATED, doesn't have any effect anymore + * @param {Boolean} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where + * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion) + */ +function Datastore (options) { + var filename; + + // Retrocompatibility with v0.6 and before + if (typeof options === 'string') { + filename = options; + this.inMemoryOnly = false; // Default + } else { + options = options || {}; + filename = options.filename; + this.inMemoryOnly = options.inMemoryOnly || false; + this.autoload = options.autoload || false; + } + + // Determine whether in memory or persistent + if (!filename || typeof filename !== 'string' || filename.length === 0) { + this.filename = null; + this.inMemoryOnly = true; + } else { + this.filename = filename; + } + + // For NW apps, store data in the same directory where NW stores application data + if (this.filename && options.nodeWebkitAppName) { + this.filename = customUtils.getNWAppFilename(options.nodeWebkitAppName, this.filename); + } + + // This new executor is ready if we don't use persistence + // If we do, it will only be ready once loadDatabase is called + this.executor = new Executor(); + if (this.inMemoryOnly) { this.executor.ready = true; } + + // We keep internally the number of lines in the datafile + // This will be used when/if I implement autocompacting when the datafile grows too big + // For now it is not urgent as autocompaction happens upon every restart + this.datafileSize = 0; + + // Indexed by field name, dot notation can be used + // _id is always indexed and since _ids are generated randomly the underlying + // binary is always well-balanced + this.indexes = {}; + this.indexes._id = new Index({ fieldName: '_id', unique: true }); + + if (this.autoload) { this.loadDatabase(); } +} + + +/** + * Get an array of all the data in the database + */ +Datastore.prototype.getAllData = function () { + return this.indexes._id.getAll(); +}; + + +/** + * Reset all currently defined indexes + */ +Datastore.prototype.resetIndexes = function (newData) { + var self = this; + + Object.keys(this.indexes).forEach(function (i) { + self.indexes[i].reset(newData); + }); +}; + + +/** + * Ensure an index is kept for this field. Same parameters as lib/indexes + * For now this function is synchronous, we need to test how much time it takes + * We use an async API for consistency with the rest of the code + * @param {String} options.fieldName + * @param {Boolean} options.unique + * @param {Boolean} options.sparse + * @param {Function} cb Optional callback, signature: err + */ +Datastore.prototype.ensureIndex = function (options, cb) { + var callback = cb || function () {}; + + options = options || {}; + + if (!options.fieldName) { return callback({ missingFieldName: true }); } + if (this.indexes[options.fieldName]) { return callback(null); } + + this.indexes[options.fieldName] = new Index(options); + + try { + this.indexes[options.fieldName].insert(this.getAllData()); + } catch (e) { + delete this.indexes[options.fieldName]; + return callback(e); + } + + return callback(null); +}; + + +/** + * Add one or several document(s) to all indexes + */ +Datastore.prototype.addToIndexes = function (doc) { + var i, failingIndex, error + , keys = Object.keys(this.indexes) + ; + + for (i = 0; i < keys.length; i += 1) { + try { + this.indexes[keys[i]].insert(doc); + } catch (e) { + failingIndex = i; + error = e; + break; + } + } + + // If an error happened, we need to rollback the insert on all other indexes + if (error) { + for (i = 0; i < failingIndex; i += 1) { + this.indexes[keys[i]].remove(doc); + } + + throw error; + } +}; + + +/** + * Remove one or several document(s) from all indexes + */ +Datastore.prototype.removeFromIndexes = function (doc) { + var self = this; + + Object.keys(this.indexes).forEach(function (i) { + self.indexes[i].remove(doc); + }); +}; + + +/** + * Update one or several documents in all indexes + * If one update violates a constraint, all changes are rolled back + */ +Datastore.prototype.updateIndexes = function (oldDoc, newDoc) { + var i, failingIndex, error + , keys = Object.keys(this.indexes) + ; + + for (i = 0; i < keys.length; i += 1) { + try { + this.indexes[keys[i]].update(oldDoc, newDoc); + } catch (e) { + failingIndex = i; + error = e; + break; + } + } + + // If an error happened, we need to rollback the insert on all other indexes + if (error) { + for (i = 0; i < failingIndex; i += 1) { + this.indexes[keys[i]].revertUpdate(oldDoc, newDoc); + } + + throw error; + } +}; + + +/** + * Return the list of candidates for a given query + * Crude implementation for now, we return the candidates given by the first usable index if any + * We try the following query types, in this order: basic match, $in match, comparison match + * One way to make it better would be to enable the use of multiple indexes if the first usable index + * returns too much data. I may do it in the future. + */ +Datastore.prototype.getCandidates = function (query) { + var indexNames = Object.keys(this.indexes) + , usableQueryKeys; + + // For a basic match + usableQueryKeys = []; + Object.keys(query).forEach(function (k) { + if (typeof query[k] === 'string' || typeof query[k] === 'number' || typeof query[k] === 'boolean' || util.isDate(query[k]) || query[k] === null) { + usableQueryKeys.push(k); + } + }); + usableQueryKeys = _.intersection(usableQueryKeys, indexNames); + if (usableQueryKeys.length > 0) { + return this.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]]); + } + + // For a $in match + usableQueryKeys = []; + Object.keys(query).forEach(function (k) { + if (query[k] && query[k].hasOwnProperty('$in')) { + usableQueryKeys.push(k); + } + }); + usableQueryKeys = _.intersection(usableQueryKeys, indexNames); + if (usableQueryKeys.length > 0) { + return this.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]].$in); + } + + // For a comparison match + usableQueryKeys = []; + Object.keys(query).forEach(function (k) { + if (query[k] && (query[k].hasOwnProperty('$lt') || query[k].hasOwnProperty('$lte') || query[k].hasOwnProperty('$gt') || query[k].hasOwnProperty('$gte'))) { + usableQueryKeys.push(k); + } + }); + usableQueryKeys = _.intersection(usableQueryKeys, indexNames); + if (usableQueryKeys.length > 0) { + return this.indexes[usableQueryKeys[0]].getBetweenBounds(query[usableQueryKeys[0]]); + } + + // By default, return all the DB data + return this.getAllData(); +}; + + +/** + * Load the database + * This means pulling data out of the data file or creating it if it doesn't exist + * Also, all data is persisted right away, which has the effect of compacting the database file + * This operation is very quick at startup for a big collection (60ms for ~10k docs) + * @param {Function} cb Optional callback, signature: err + * + * @api private Use loadDatabase + */ +Datastore.prototype._loadDatabase = function (cb) { + var callback = cb || function () {} + , self = this + ; + + self.resetIndexes(); + self.datafileSize = 0; + + // In-memory only datastore + if (self.inMemoryOnly) { return callback(null); } + + async.waterfall([ + function (cb) { + customUtils.ensureDirectoryExists(path.dirname(self.filename), function (err) { + fs.exists(self.filename, function (exists) { + if (!exists) { return fs.writeFile(self.filename, '', 'utf8', function (err) { cb(err); }); } + + fs.readFile(self.filename, 'utf8', function (err, rawData) { + if (err) { return cb(err); } + var treatedData = Datastore.treatRawData(rawData); + + try { + self.resetIndexes(treatedData); + } catch (e) { + self.resetIndexes(); // Rollback any index which didn't fail + self.datafileSize = 0; + return cb(e); + } + + self.datafileSize = treatedData.length; + self.persistCachedDatabase(cb); + }); + }); + }); + } + ], function (err) { + if (err) { return callback(err); } + + self.executor.processBuffer(); + return callback(null); + }); +}; + +Datastore.prototype.loadDatabase = function () { + this.executor.push({ this: this, fn: this._loadDatabase, arguments: arguments }, true); +}; + + +/** + * From a database's raw data, return the corresponding + * machine understandable collection + */ +Datastore.treatRawData = function (rawData) { + var data = rawData.split('\n') + , dataById = {} + , res = [] + , i; + + for (i = 0; i < data.length; i += 1) { + var doc; + + try { + doc = model.deserialize(data[i]); + if (doc._id) { + if (doc.$$deleted === true) { + delete dataById[doc._id]; + } else { + dataById[doc._id] = doc; + } + } + } catch (e) { + } + } + + Object.keys(dataById).forEach(function (k) { + res.push(dataById[k]); + }); + + return res; +}; + + +/** + * Persist cached database + * This serves as a compaction function since the cache always contains only the number of documents in the collection + * while the data file is append-only so it may grow larger + * @param {Function} cb Optional callback, signature: err + */ +Datastore.prototype.persistCachedDatabase = function (cb) { + var callback = cb || function () {} + , toPersist = '' + ; + + this.getAllData().forEach(function (doc) { + toPersist += model.serialize(doc) + '\n'; + }); + + if (toPersist.length === 0) { return callback(null); } + + fs.writeFile(this.filename, toPersist, function (err) { return callback(err); }); +}; + + +/** + * Persist new state for the given newDocs (can be insertion, update or removal) + * Use an append-only format + * @param {Array} newDocs Can be empty if no doc was updated/removed + * @param {Function} cb Optional, signature: err + */ +Datastore.prototype._persistNewState = function (newDocs, cb) { + var self = this + , toPersist = '' + , callback = cb || function () {} + ; + + // In-memory only datastore + if (self.inMemoryOnly) { return callback(null); } + + self.datafileSize += newDocs.length; + + newDocs.forEach(function (doc) { + toPersist += model.serialize(doc) + '\n'; + }); + + if (toPersist.length === 0) { return callback(null); } + + fs.appendFile(self.filename, toPersist, 'utf8', function (err) { + return callback(err); + }); +}; +Datastore.prototype.persistNewState = function (newDocs, cb) { + if (this.inMemoryOnly) { + cb(); + } else { + this._persistNewState(newDocs, cb); + } +}; + + +/** + * Insert a new document + * @param {Function} cb Optional callback, signature: err, insertedDoc + * + * @api private Use Datastore.insert which has the same signature + */ +Datastore.prototype._insert = function (newDoc, cb) { + var callback = cb || function () {} + , self = this + , insertedDoc + ; + + // Ensure the document has the right format + try { + newDoc._id = customUtils.uid(16); + model.checkObject(newDoc); + insertedDoc = model.deepCopy(newDoc); + } catch (e) { + return callback(e); + } + + // Insert in all indexes (also serves to ensure uniqueness) + try { self.addToIndexes(insertedDoc); } catch (e) { return callback(e); } + + this.persistNewState([newDoc], function (err) { + if (err) { return callback(err); } + return callback(null, newDoc); + }); +}; + +Datastore.prototype.insert = function () { + this.executor.push({ this: this, fn: this._insert, arguments: arguments }); +}; + + +/** + * Find all documents matching the query + * @param {Object} query MongoDB-style query + * + * @api private Use find + */ +Datastore.prototype._find = function (query, callback) { + var res = [] + , self = this + , candidates = this.getCandidates(query) + , i + ; + + try { + for (i = 0; i < candidates.length; i += 1) { + if (model.match(candidates[i], query)) { + res.push(model.deepCopy(candidates[i])); + } + } + } catch (err) { + return callback(err); + } + + return callback(null, res); +}; + +Datastore.prototype.find = function () { + this.executor.push({ this: this, fn: this._find, arguments: arguments }); +}; + + +/** + * Find one document matching the query + * @param {Object} query MongoDB-style query + * + * @api private Use findOne + */ +Datastore.prototype._findOne = function (query, callback) { + var self = this + , candidates = this.getCandidates(query) + , i + ; + + try { + for (i = 0; i < candidates.length; i += 1) { + if (model.match(candidates[i], query)) { + return callback(null, model.deepCopy(candidates[i])); + } + } + } catch (err) { + return callback(err); + } + + return callback(null, null); +}; + +Datastore.prototype.findOne = function () { + this.executor.push({ this: this, fn: this._findOne, arguments: arguments }); +}; + + +/** + * Update all docs matching query + * For now, very naive implementation (recalculating the whole database) + * @param {Object} query + * @param {Object} updateQuery + * @param {Object} options Optional options + * options.multi If true, can update multiple documents (defaults to false) + * options.upsert If true, document is inserted if the query doesn't match anything + * @param {Function} cb Optional callback, signature: err, numReplaced, upsert (set to true if the update was in fact an upsert) + * + * @api private Use Datastore.update which has the same signature + */ +Datastore.prototype._update = function (query, updateQuery, options, cb) { + var callback + , self = this + , numReplaced = 0 + , multi, upsert + , updatedDocs = [] + , candidates + , i + ; + + if (typeof options === 'function') { cb = options; options = {}; } + callback = cb || function () {}; + multi = options.multi !== undefined ? options.multi : false; + upsert = options.upsert !== undefined ? options.upsert : false; + + async.waterfall([ + function (cb) { // If upsert option is set, check whether we need to insert the doc + if (!upsert) { return cb(); } + + self._findOne(query, function (err, doc) { + if (err) { return callback(err); } + if (doc) { + return cb(); + } else { + // The upserted document is the query (since for now queries have the same structure as + // documents), modified by the updateQuery + return self._insert(model.modify(query, updateQuery), function (err) { + if (err) { return callback(err); } + return callback(null, 1, true); + }); + } + }); + } + , function () { // Perform the update + var modifiedDoc; + + candidates = self.getCandidates(query); + + try { + for (i = 0; i < candidates.length; i += 1) { + if (model.match(candidates[i], query) && (multi || numReplaced === 0)) { + numReplaced += 1; + modifiedDoc = model.modify(candidates[i], updateQuery); + self.updateIndexes(candidates[i], modifiedDoc); + updatedDocs.push(modifiedDoc); + } + } + } catch (err) { + return callback(err); + } + + self.persistNewState(updatedDocs, function (err) { + if (err) { return callback(err); } + return callback(null, numReplaced); + }); + } + ]); +}; +Datastore.prototype.update = function () { + this.executor.push({ this: this, fn: this._update, arguments: arguments }); +}; + + +/** + * Remove all docs matching the query + * For now very naive implementation (similar to update) + * @param {Object} query + * @param {Object} options Optional options + * options.multi If true, can update multiple documents (defaults to false) + * @param {Function} cb Optional callback, signature: err, numRemoved + * + * @api private Use Datastore.remove which has the same signature + */ +Datastore.prototype._remove = function (query, options, cb) { + var callback + , self = this + , numRemoved = 0 + , multi + , removedDocs = [] + , candidates = this.getCandidates(query) + ; + + if (typeof options === 'function') { cb = options; options = {}; } + callback = cb || function () {}; + multi = options.multi !== undefined ? options.multi : false; + + try { + candidates.forEach(function (d) { + if (model.match(d, query) && (multi || numRemoved === 0)) { + numRemoved += 1; + removedDocs.push({ $$deleted: true, _id: d._id }); + self.removeFromIndexes(d); + } + }); + } catch (err) { return callback(err); } + + self.persistNewState(removedDocs, function (err) { + if (err) { return callback(err); } + return callback(null, numRemoved); + }); +}; +Datastore.prototype.remove = function () { + this.executor.push({ this: this, fn: this._remove, arguments: arguments }); +}; + + + + +module.exports = Datastore; + +},{"fs":3,"path":5,"util":6,"./customUtils":9,"./model":8,"./executor":10,"./indexes":11,"async":12}],13:[function(require,module,exports){ +var sha = require('./sha') +var rng = require('./rng') +var md5 = require('./md5') + +var algorithms = { + sha1: { + hex: sha.hex_sha1, + binary: sha.b64_sha1, + ascii: sha.str_sha1 + }, + md5: { + hex: md5.hex_md5, + binary: md5.b64_md5, + ascii: md5.any_md5 + } +} + +function error () { + var m = [].slice.call(arguments).join(' ') + throw new Error([ + m, + 'we accept pull requests', + 'http://github.com/dominictarr/crypto-browserify' + ].join('\n')) +} + +exports.createHash = function (alg) { + alg = alg || 'sha1' + if(!algorithms[alg]) + error('algorithm:', alg, 'is not yet supported') + var s = '' + var _alg = algorithms[alg] + return { + update: function (data) { + s += data + return this + }, + digest: function (enc) { + enc = enc || 'binary' + var fn + if(!(fn = _alg[enc])) + error('encoding:', enc , 'is not yet supported for algorithm', alg) + var r = fn(s) + s = null //not meant to use the hash after you've called digest. + return r + } + } +} + +exports.randomBytes = function(size, callback) { + if (callback && callback.call) { + try { + callback.call(this, undefined, rng(size)); + } catch (err) { callback(err); } + } else { + return rng(size); + } +} + +// the least I can do is make error messages for the rest of the node.js/crypto api. +;['createCredentials' +, 'createHmac' +, 'createCypher' +, 'createCypheriv' +, 'createDecipher' +, 'createDecipheriv' +, 'createSign' +, 'createVerify' +, 'createDeffieHellman' +, 'pbkdf2'].forEach(function (name) { + exports[name] = function () { + error('sorry,', name, 'is not implemented yet') + } +}) + +},{"./sha":14,"./rng":15,"./md5":16}],12:[function(require,module,exports){ +(function(process){/*global setImmediate: false, setTimeout: false, console: false */ +(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.setImmediate = setImmediate; + async.nextTick = setImmediate; + } + else { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + async.setImmediate = async.nextTick; + } + } + else { + async.nextTick = process.nextTick; + if (typeof setImmediate !== 'undefined') { + async.setImmediate = setImmediate; + } + 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