mirror of https://github.com/seald/nedb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
9278 lines
286 KiB
9278 lines
286 KiB
(function(e){if("function"==typeof bootstrap)bootstrap("nedb",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeNedb=e}else"undefined"!=typeof window?window.Nedb=e():global.Nedb=e()})(function(){var define,ses,bootstrap,module,exports;
|
|
return (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<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
|
|
var process=require("__browserify_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];
|
|
};
|
|
|
|
EventEmitter.listenerCount = function(emitter, type) {
|
|
var ret;
|
|
if (!emitter._events || !emitter._events[type])
|
|
ret = 0;
|
|
else if (typeof emitter._events[type] === 'function')
|
|
ret = 1;
|
|
else
|
|
ret = emitter._events[type].length;
|
|
return ret;
|
|
};
|
|
|
|
},{"__browserify_process":4}],2:[function(require,module,exports){
|
|
var process=require("__browserify_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('/');
|
|
};
|
|
|
|
exports.sep = '/';
|
|
|
|
},{"__browserify_process":4}],3:[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 '\u001b[' + styles[style][0] + 'm' + str +
|
|
'\u001b[' + 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 Array.isArray(ar) ||
|
|
(typeof ar === 'object' && Object.prototype.toString.call(ar) === '[object Array]');
|
|
}
|
|
|
|
|
|
function isRegExp(re) {
|
|
typeof re === 'object' && Object.prototype.toString.call(re) === '[object RegExp]';
|
|
}
|
|
|
|
|
|
function isDate(d) {
|
|
return typeof d === 'object' && Object.prototype.toString.call(d) === '[object Date]';
|
|
}
|
|
|
|
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":1}],4:[function(require,module,exports){
|
|
// shim for using process in browser
|
|
|
|
var process = module.exports = {};
|
|
|
|
process.nextTick = (function () {
|
|
var canSetImmediate = typeof window !== 'undefined'
|
|
&& window.setImmediate;
|
|
var canPost = typeof window !== 'undefined'
|
|
&& window.postMessage && window.addEventListener
|
|
;
|
|
|
|
if (canSetImmediate) {
|
|
return function (f) { return window.setImmediate(f) };
|
|
}
|
|
|
|
if (canPost) {
|
|
var queue = [];
|
|
window.addEventListener('message', function (ev) {
|
|
var source = ev.source;
|
|
if ((source === window || source === null) && ev.data === 'process-tick') {
|
|
ev.stopPropagation();
|
|
if (queue.length > 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){
|
|
/**
|
|
* Manage access to data, be it to find, update or remove it
|
|
*/
|
|
var model = require('./model')
|
|
, _ = require('underscore')
|
|
;
|
|
|
|
|
|
|
|
/**
|
|
* Create a new cursor for this collection
|
|
* @param {Datastore} db - The datastore this cursor is bound to
|
|
* @param {Query} query - The query this cursor will operate on
|
|
* @param {Function} execDn - Handler to be executed after cursor has found the results and before the callback passed to find/findOne/update/remove
|
|
*/
|
|
function Cursor (db, query, execFn) {
|
|
this.db = db;
|
|
this.query = query || {};
|
|
if (execFn) { this.execFn = execFn; }
|
|
}
|
|
|
|
|
|
/**
|
|
* Set a limit to the number of results
|
|
*/
|
|
Cursor.prototype.limit = function(limit) {
|
|
this._limit = limit;
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Skip a the number of results
|
|
*/
|
|
Cursor.prototype.skip = function(skip) {
|
|
this._skip = skip;
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sort results of the query
|
|
* @param {SortQuery} sortQuery - SortQuery is { field: order }, field can use the dot-notation, order is 1 for ascending and -1 for descending
|
|
*/
|
|
Cursor.prototype.sort = function(sortQuery) {
|
|
this._sort = sortQuery;
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Add the use of a projection
|
|
* @param {Object} projection - MongoDB-style projection. {} means take all fields. Then it's { key1: 1, key2: 1 } to take only key1 and key2
|
|
* { key1: 0, key2: 0 } to omit only key1 and key2. Except _id, you can't mix takes and omits
|
|
*/
|
|
Cursor.prototype.projection = function(projection) {
|
|
this._projection = projection;
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Apply the projection
|
|
*/
|
|
Cursor.prototype.project = function (candidates) {
|
|
var res = [], self = this
|
|
, keepId, action, keys
|
|
;
|
|
|
|
if (this._projection === undefined || Object.keys(this._projection).length === 0) {
|
|
return candidates;
|
|
}
|
|
|
|
keepId = this._projection._id === 0 ? false : true;
|
|
this._projection = _.omit(this._projection, '_id');
|
|
|
|
// Check for consistency
|
|
keys = Object.keys(this._projection);
|
|
keys.forEach(function (k) {
|
|
if (action !== undefined && self._projection[k] !== action) { throw new Error("Can't both keep and omit fields except for _id"); }
|
|
action = self._projection[k];
|
|
});
|
|
|
|
// Do the actual projection
|
|
candidates.forEach(function (candidate) {
|
|
var toPush = action === 1 ? _.pick(candidate, keys) : _.omit(candidate, keys);
|
|
if (keepId) {
|
|
toPush._id = candidate._id;
|
|
} else {
|
|
delete toPush._id;
|
|
}
|
|
res.push(toPush);
|
|
});
|
|
|
|
return res;
|
|
};
|
|
|
|
|
|
/**
|
|
* Get all matching elements
|
|
* Will return pointers to matched elements (shallow copies), returning full copies is the role of find or findOne
|
|
* This is an internal function, use exec which uses the executor
|
|
*
|
|
* @param {Function} callback - Signature: err, results
|
|
*/
|
|
Cursor.prototype._exec = function(callback) {
|
|
var candidates = this.db.getCandidates(this.query)
|
|
, res = [], added = 0, skipped = 0, self = this
|
|
, error = null
|
|
, i, keys, key
|
|
;
|
|
|
|
try {
|
|
for (i = 0; i < candidates.length; i += 1) {
|
|
if (model.match(candidates[i], this.query)) {
|
|
// If a sort is defined, wait for the results to be sorted before applying limit and skip
|
|
if (!this._sort) {
|
|
if (this._skip && this._skip > skipped) {
|
|
skipped += 1;
|
|
} else {
|
|
res.push(candidates[i]);
|
|
added += 1;
|
|
if (this._limit && this._limit <= added) { break; }
|
|
}
|
|
} else {
|
|
res.push(candidates[i]);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
// Apply all sorts
|
|
if (this._sort) {
|
|
keys = Object.keys(this._sort);
|
|
|
|
// Sorting
|
|
var criteria = [];
|
|
for (i = 0; i < keys.length; i++) {
|
|
key = keys[i];
|
|
criteria.push({ key: key, direction: self._sort[key] });
|
|
}
|
|
res.sort(function(a, b) {
|
|
var criterion, compare, i;
|
|
for (i = 0; i < criteria.length; i++) {
|
|
criterion = criteria[i];
|
|
compare = criterion.direction * model.compareThings(model.getDotValue(a, criterion.key), model.getDotValue(b, criterion.key), self.db.compareStrings);
|
|
if (compare !== 0) {
|
|
return compare;
|
|
}
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
// Applying limit and skip
|
|
var limit = this._limit || res.length
|
|
, skip = this._skip || 0;
|
|
|
|
res = res.slice(skip, skip + limit);
|
|
}
|
|
|
|
// Apply projection
|
|
try {
|
|
res = this.project(res);
|
|
} catch (e) {
|
|
error = e;
|
|
res = undefined;
|
|
}
|
|
|
|
if (this.execFn) {
|
|
return this.execFn(error, res, callback);
|
|
} else {
|
|
return callback(error, res);
|
|
}
|
|
};
|
|
|
|
Cursor.prototype.exec = function () {
|
|
this.db.executor.push({ this: this, fn: this._exec, arguments: arguments });
|
|
};
|
|
|
|
|
|
|
|
// Interface
|
|
module.exports = Cursor;
|
|
|
|
},{"./model":10,"underscore":19}],6:[function(require,module,exports){
|
|
/**
|
|
* Specific customUtils for the browser, where we don't have access to the Crypto and Buffer modules
|
|
*/
|
|
|
|
/**
|
|
* Taken from the crypto-browserify module
|
|
* https://github.com/dominictarr/crypto-browserify
|
|
* NOTE: Math.random() does not guarantee "cryptographic quality" but we actually don't need it
|
|
*/
|
|
function randomBytes (size) {
|
|
var bytes = new Array(size);
|
|
var r;
|
|
|
|
for (var i = 0, r; i < size; i++) {
|
|
if ((i & 0x03) == 0) r = Math.random() * 0x100000000;
|
|
bytes[i] = r >>> ((i & 0x03) << 3) & 0xff;
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
|
|
/**
|
|
* Taken from the base64-js module
|
|
* https://github.com/beatgammit/base64-js/
|
|
*/
|
|
function byteArrayToBase64 (uint8) {
|
|
var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
, extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
|
|
, output = ""
|
|
, temp, length, i;
|
|
|
|
function tripletToBase64 (num) {
|
|
return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F];
|
|
};
|
|
|
|
// go through the array every three bytes, we'll deal with trailing stuff later
|
|
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
|
|
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]);
|
|
output += tripletToBase64(temp);
|
|
}
|
|
|
|
// pad the end with zeros, but make sure to not forget the extra bytes
|
|
switch (extraBytes) {
|
|
case 1:
|
|
temp = uint8[uint8.length - 1];
|
|
output += lookup[temp >> 2];
|
|
output += lookup[(temp << 4) & 0x3F];
|
|
output += '==';
|
|
break;
|
|
case 2:
|
|
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1]);
|
|
output += lookup[temp >> 10];
|
|
output += lookup[(temp >> 4) & 0x3F];
|
|
output += lookup[(temp << 2) & 0x3F];
|
|
output += '=';
|
|
break;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return a random alphanumerical string of length len
|
|
* There is a very small probability (less than 1/1,000,000) for the length to be less than len
|
|
* (il the base64 conversion yields too many pluses and slashes) but
|
|
* that's not an issue here
|
|
* The probability of a collision is extremely small (need 3*10^12 documents to have one chance in a million of a collision)
|
|
* See http://en.wikipedia.org/wiki/Birthday_problem
|
|
*/
|
|
function uid (len) {
|
|
return byteArrayToBase64(randomBytes(Math.ceil(Math.max(8, len * 2)))).replace(/[+\/]/g, '').slice(0, len);
|
|
}
|
|
|
|
|
|
|
|
module.exports.uid = uid;
|
|
|
|
},{}],7:[function(require,module,exports){
|
|
var customUtils = require('./customUtils')
|
|
, model = require('./model')
|
|
, async = require('async')
|
|
, Executor = require('./executor')
|
|
, Index = require('./indexes')
|
|
, util = require('util')
|
|
, _ = require('underscore')
|
|
, Persistence = require('./persistence')
|
|
, Cursor = require('./cursor')
|
|
;
|
|
|
|
|
|
/**
|
|
* Create a new collection
|
|
* @param {String} options.filename Optional, datastore will be in-memory only if not provided
|
|
* @param {Boolean} options.timestampData Optional, defaults to false. If set to true, createdAt and updatedAt will be created and populated automatically (if not specified by user)
|
|
* @param {Boolean} options.inMemoryOnly Optional, defaults to false
|
|
* @param {String} 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)
|
|
* @param {Boolean} options.autoload Optional, defaults to false
|
|
* @param {Function} options.onload Optional, if autoload is used this will be called after the load database with the error object as parameter. If you don't pass it the error will be thrown
|
|
* @param {Function} options.afterSerialization/options.beforeDeserialization Optional, serialization hooks
|
|
* @param {Number} options.corruptAlertThreshold Optional, threshold after which an alert is thrown if too much data is corrupt
|
|
* @param {Function} options.compareStrings Optional, string comparison function that overrides default for sorting
|
|
*
|
|
* Event Emitter - Events
|
|
* * compaction.done - Fired whenever a compaction operation was finished
|
|
*/
|
|
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;
|
|
this.timestampData = options.timestampData || 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;
|
|
}
|
|
|
|
// String comparison function
|
|
this.compareStrings = options.compareStrings;
|
|
|
|
// Persistence handling
|
|
this.persistence = new Persistence({ db: this, nodeWebkitAppName: options.nodeWebkitAppName
|
|
, afterSerialization: options.afterSerialization
|
|
, beforeDeserialization: options.beforeDeserialization
|
|
, corruptAlertThreshold: options.corruptAlertThreshold
|
|
});
|
|
|
|
// 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; }
|
|
|
|
// 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 });
|
|
|
|
// Queue a load of the database right away and call the onload handler
|
|
// By default (no onload handler), if there is an error there, no operation will be possible so warn the user by throwing an exception
|
|
if (this.autoload) { this.loadDatabase(options.onload || function (err) {
|
|
if (err) { throw err; }
|
|
}); }
|
|
}
|
|
|
|
util.inherits(Datastore, require('events'));
|
|
|
|
|
|
/**
|
|
* Load the database from the datafile, and trigger the execution of buffered commands if any
|
|
*/
|
|
Datastore.prototype.loadDatabase = function () {
|
|
this.executor.push({ this: this.persistence, fn: this.persistence.loadDatabase, arguments: arguments }, true);
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 err
|
|
, callback = cb || function () {};
|
|
|
|
options = options || {};
|
|
|
|
if (!options.fieldName) {
|
|
err = new Error("Cannot create an index without a fieldName");
|
|
err.missingFieldName = true;
|
|
return callback(err);
|
|
}
|
|
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);
|
|
}
|
|
|
|
// We may want to force all options to be persisted including defaults, not just the ones passed the index creation function
|
|
this.persistence.persistNewState([{ $$indexCreated: options }], function (err) {
|
|
if (err) { return callback(err); }
|
|
return callback(null);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Remove an index
|
|
* @param {String} fieldName
|
|
* @param {Function} cb Optional callback, signature: err
|
|
*/
|
|
Datastore.prototype.removeIndex = function (fieldName, cb) {
|
|
var callback = cb || function () {};
|
|
|
|
delete this.indexes[fieldName];
|
|
|
|
this.persistence.persistNewState([{ $$indexRemoved: fieldName }], function (err) {
|
|
if (err) { return callback(err); }
|
|
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
|
|
* To update multiple documents, oldDoc must be an array of { oldDoc, newDoc } pairs
|
|
* 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 update 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.
|
|
*
|
|
* TODO: needs to be moved to the Cursor module
|
|
*/
|
|
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();
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 () {}
|
|
, preparedDoc
|
|
;
|
|
|
|
try {
|
|
preparedDoc = this.prepareDocumentForInsertion(newDoc)
|
|
this._insertInCache(preparedDoc);
|
|
} catch (e) {
|
|
return callback(e);
|
|
}
|
|
|
|
this.persistence.persistNewState(util.isArray(preparedDoc) ? preparedDoc : [preparedDoc], function (err) {
|
|
if (err) { return callback(err); }
|
|
return callback(null, model.deepCopy(preparedDoc));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Create a new _id that's not already in use
|
|
*/
|
|
Datastore.prototype.createNewId = function () {
|
|
var tentativeId = customUtils.uid(16);
|
|
// Try as many times as needed to get an unused _id. As explained in customUtils, the probability of this ever happening is extremely small, so this is O(1)
|
|
if (this.indexes._id.getMatching(tentativeId).length > 0) {
|
|
tentativeId = this.createNewId();
|
|
}
|
|
return tentativeId;
|
|
};
|
|
|
|
/**
|
|
* Prepare a document (or array of documents) to be inserted in a database
|
|
* Meaning adds _id and timestamps if necessary on a copy of newDoc to avoid any side effect on user input
|
|
* @api private
|
|
*/
|
|
Datastore.prototype.prepareDocumentForInsertion = function (newDoc) {
|
|
var preparedDoc, self = this;
|
|
|
|
if (util.isArray(newDoc)) {
|
|
preparedDoc = [];
|
|
newDoc.forEach(function (doc) { preparedDoc.push(self.prepareDocumentForInsertion(doc)); });
|
|
} else {
|
|
preparedDoc = model.deepCopy(newDoc);
|
|
if (preparedDoc._id === undefined) { preparedDoc._id = this.createNewId(); }
|
|
var now = new Date();
|
|
if (this.timestampData && preparedDoc.createdAt === undefined) { preparedDoc.createdAt = now; }
|
|
if (this.timestampData && preparedDoc.updatedAt === undefined) { preparedDoc.updatedAt = now; }
|
|
model.checkObject(preparedDoc);
|
|
}
|
|
|
|
return preparedDoc;
|
|
};
|
|
|
|
/**
|
|
* If newDoc is an array of documents, this will insert all documents in the cache
|
|
* @api private
|
|
*/
|
|
Datastore.prototype._insertInCache = function (preparedDoc) {
|
|
if (util.isArray(preparedDoc)) {
|
|
this._insertMultipleDocsInCache(preparedDoc);
|
|
} else {
|
|
this.addToIndexes(preparedDoc);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* If one insertion fails (e.g. because of a unique constraint), roll back all previous
|
|
* inserts and throws the error
|
|
* @api private
|
|
*/
|
|
Datastore.prototype._insertMultipleDocsInCache = function (preparedDocs) {
|
|
var i, failingI, error;
|
|
|
|
for (i = 0; i < preparedDocs.length; i += 1) {
|
|
try {
|
|
this.addToIndexes(preparedDocs[i]);
|
|
} catch (e) {
|
|
error = e;
|
|
failingI = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
for (i = 0; i < failingI; i += 1) {
|
|
this.removeFromIndexes(preparedDocs[i]);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
Datastore.prototype.insert = function () {
|
|
this.executor.push({ this: this, fn: this._insert, arguments: arguments });
|
|
};
|
|
|
|
|
|
/**
|
|
* Count all documents matching the query
|
|
* @param {Object} query MongoDB-style query
|
|
*/
|
|
Datastore.prototype.count = function(query, callback) {
|
|
var cursor = new Cursor(this, query, function(err, docs, callback) {
|
|
if (err) { return callback(err); }
|
|
return callback(null, docs.length);
|
|
});
|
|
|
|
if (typeof callback === 'function') {
|
|
cursor.exec(callback);
|
|
} else {
|
|
return cursor;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Find all documents matching the query
|
|
* If no callback is passed, we return the cursor so that user can limit, skip and finally exec
|
|
* @param {Object} query MongoDB-style query
|
|
* @param {Object} projection MongoDB-style projection
|
|
*/
|
|
Datastore.prototype.find = function (query, projection, callback) {
|
|
switch (arguments.length) {
|
|
case 1:
|
|
projection = {};
|
|
// callback is undefined, will return a cursor
|
|
break;
|
|
case 2:
|
|
if (typeof projection === 'function') {
|
|
callback = projection;
|
|
projection = {};
|
|
} // If not assume projection is an object and callback undefined
|
|
break;
|
|
}
|
|
|
|
var cursor = new Cursor(this, query, function(err, docs, callback) {
|
|
var res = [], i;
|
|
|
|
if (err) { return callback(err); }
|
|
|
|
for (i = 0; i < docs.length; i += 1) {
|
|
res.push(model.deepCopy(docs[i]));
|
|
}
|
|
return callback(null, res);
|
|
});
|
|
|
|
cursor.projection(projection);
|
|
if (typeof callback === 'function') {
|
|
cursor.exec(callback);
|
|
} else {
|
|
return cursor;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Find one document matching the query
|
|
* @param {Object} query MongoDB-style query
|
|
* @param {Object} projection MongoDB-style projection
|
|
*/
|
|
Datastore.prototype.findOne = function (query, projection, callback) {
|
|
switch (arguments.length) {
|
|
case 1:
|
|
projection = {};
|
|
// callback is undefined, will return a cursor
|
|
break;
|
|
case 2:
|
|
if (typeof projection === 'function') {
|
|
callback = projection;
|
|
projection = {};
|
|
} // If not assume projection is an object and callback undefined
|
|
break;
|
|
}
|
|
|
|
var cursor = new Cursor(this, query, function(err, docs, callback) {
|
|
if (err) { return callback(err); }
|
|
if (docs.length === 1) {
|
|
return callback(null, model.deepCopy(docs[0]));
|
|
} else {
|
|
return callback(null, null);
|
|
}
|
|
});
|
|
|
|
cursor.projection(projection).limit(1);
|
|
if (typeof callback === 'function') {
|
|
cursor.exec(callback);
|
|
} else {
|
|
return cursor;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Update all docs matching query
|
|
* @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
|
|
* options.returnUpdatedDocs Defaults to false, if true return as third argument the array of updated matched documents (even if no change actually took place)
|
|
* @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
|
|
, 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(); }
|
|
|
|
// Need to use an internal function not tied to the executor to avoid deadlock
|
|
var cursor = new Cursor(self, query);
|
|
cursor.limit(1)._exec(function (err, docs) {
|
|
if (err) { return callback(err); }
|
|
if (docs.length === 1) {
|
|
return cb();
|
|
} else {
|
|
var toBeInserted;
|
|
|
|
try {
|
|
model.checkObject(updateQuery);
|
|
// updateQuery is a simple object with no modifier, use it as the document to insert
|
|
toBeInserted = updateQuery;
|
|
} catch (e) {
|
|
// updateQuery contains modifiers, use the find query as the base,
|
|
// strip it from all operators and update it according to updateQuery
|
|
try {
|
|
toBeInserted = model.modify(model.deepCopy(query, true), updateQuery);
|
|
} catch (err) {
|
|
return callback(err);
|
|
}
|
|
}
|
|
|
|
return self._insert(toBeInserted, function (err, newDoc) {
|
|
if (err) { return callback(err); }
|
|
return callback(null, 1, newDoc);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
, function () { // Perform the update
|
|
var modifiedDoc
|
|
, candidates = self.getCandidates(query)
|
|
, modifications = []
|
|
;
|
|
|
|
// Preparing update (if an error is thrown here neither the datafile nor
|
|
// the in-memory indexes are affected)
|
|
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);
|
|
if (self.timestampData) { modifiedDoc.updatedAt = new Date(); }
|
|
modifications.push({ oldDoc: candidates[i], newDoc: modifiedDoc });
|
|
}
|
|
}
|
|
} catch (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
// Change the docs in memory
|
|
try {
|
|
self.updateIndexes(modifications);
|
|
} catch (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
// Update the datafile
|
|
var updatedDocs = _.pluck(modifications, 'newDoc');
|
|
self.persistence.persistNewState(updatedDocs, function (err) {
|
|
if (err) { return callback(err); }
|
|
if (!options.returnUpdatedDocs) {
|
|
return callback(null, numReplaced);
|
|
} else {
|
|
var updatedDocsDC = [];
|
|
updatedDocs.forEach(function (doc) { updatedDocsDC.push(model.deepCopy(doc)); });
|
|
return callback(null, numReplaced, updatedDocsDC);
|
|
}
|
|
});
|
|
}
|
|
]);
|
|
};
|
|
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.persistence.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;
|
|
|
|
},{"./cursor":5,"./customUtils":6,"./executor":8,"./indexes":9,"./model":10,"./persistence":11,"async":13,"events":1,"underscore":19,"util":3}],8:[function(require,module,exports){
|
|
var process=require("__browserify_process");/**
|
|
* Responsible for sequentially executing actions on the database
|
|
*/
|
|
|
|
var async = require('async')
|
|
;
|
|
|
|
function Executor () {
|
|
this.buffer = [];
|
|
this.ready = false;
|
|
|
|
// This queue will execute all commands, one-by-one in order
|
|
this.queue = async.queue(function (task, cb) {
|
|
var callback
|
|
, lastArg = task.arguments[task.arguments.length - 1]
|
|
, i, newArguments = []
|
|
;
|
|
|
|
// task.arguments is an array-like object on which adding a new field doesn't work, so we transform it into a real array
|
|
for (i = 0; i < task.arguments.length; i += 1) { newArguments.push(task.arguments[i]); }
|
|
|
|
// Always tell the queue task is complete. Execute callback if any was given.
|
|
if (typeof lastArg === 'function') {
|
|
callback = function () {
|
|
if (typeof setImmediate === 'function') {
|
|
setImmediate(cb);
|
|
} else {
|
|
process.nextTick(cb);
|
|
}
|
|
lastArg.apply(null, arguments);
|
|
};
|
|
|
|
newArguments[newArguments.length - 1] = callback;
|
|
} else {
|
|
callback = function () { cb(); };
|
|
newArguments.push(callback);
|
|
}
|
|
|
|
|
|
task.fn.apply(task.this, newArguments);
|
|
}, 1);
|
|
}
|
|
|
|
|
|
/**
|
|
* If executor is ready, queue task (and process it immediately if executor was idle)
|
|
* If not, buffer task for later processing
|
|
* @param {Object} task
|
|
* task.this - Object to use as this
|
|
* task.fn - Function to execute
|
|
* task.arguments - Array of arguments
|
|
* @param {Boolean} forceQueuing Optional (defaults to false) force executor to queue task even if it is not ready
|
|
*/
|
|
Executor.prototype.push = function (task, forceQueuing) {
|
|
if (this.ready || forceQueuing) {
|
|
this.queue.push(task);
|
|
} else {
|
|
this.buffer.push(task);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Queue all tasks in buffer (in the same order they came in)
|
|
* Automatically sets executor as ready
|
|
*/
|
|
Executor.prototype.processBuffer = function () {
|
|
var i;
|
|
this.ready = true;
|
|
for (i = 0; i < this.buffer.length; i += 1) { this.queue.push(this.buffer[i]); }
|
|
this.buffer = [];
|
|
};
|
|
|
|
|
|
|
|
// Interface
|
|
module.exports = Executor;
|
|
|
|
},{"__browserify_process":4,"async":13}],9:[function(require,module,exports){
|
|
var BinarySearchTree = require('binary-search-tree').AVLTree
|
|
, model = require('./model')
|
|
, _ = require('underscore')
|
|
, util = require('util')
|
|
;
|
|
|
|
/**
|
|
* Two indexed pointers are equal iif they point to the same place
|
|
*/
|
|
function checkValueEquality (a, b) {
|
|
return a === b;
|
|
}
|
|
|
|
/**
|
|
* Type-aware projection
|
|
*/
|
|
function projectForUnique (elt) {
|
|
if (elt === null) { return '$null'; }
|
|
if (typeof elt === 'string') { return '$string' + elt; }
|
|
if (typeof elt === 'boolean') { return '$boolean' + elt; }
|
|
if (typeof elt === 'number') { return '$number' + elt; }
|
|
if (util.isArray(elt)) { return '$date' + elt.getTime(); }
|
|
|
|
return elt; // Arrays and objects, will check for pointer equality
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a new index
|
|
* All methods on an index guarantee that either the whole operation was successful and the index changed
|
|
* or the operation was unsuccessful and an error is thrown while the index is unchanged
|
|
* @param {String} options.fieldName On which field should the index apply (can use dot notation to index on sub fields)
|
|
* @param {Boolean} options.unique Optional, enforce a unique constraint (default: false)
|
|
* @param {Boolean} options.sparse Optional, allow a sparse index (we can have documents for which fieldName is undefined) (default: false)
|
|
*/
|
|
function Index (options) {
|
|
this.fieldName = options.fieldName;
|
|
this.unique = options.unique || false;
|
|
this.sparse = options.sparse || false;
|
|
|
|
this.treeOptions = { unique: this.unique, compareKeys: model.compareThings, checkValueEquality: checkValueEquality };
|
|
|
|
this.reset(); // No data in the beginning
|
|
}
|
|
|
|
|
|
/**
|
|
* Reset an index
|
|
* @param {Document or Array of documents} newData Optional, data to initialize the index with
|
|
* If an error is thrown during insertion, the index is not modified
|
|
*/
|
|
Index.prototype.reset = function (newData) {
|
|
this.tree = new BinarySearchTree(this.treeOptions);
|
|
|
|
if (newData) { this.insert(newData); }
|
|
};
|
|
|
|
|
|
/**
|
|
* Insert a new document in the index
|
|
* If an array is passed, we insert all its elements (if one insertion fails the index is not modified)
|
|
* O(log(n))
|
|
*/
|
|
Index.prototype.insert = function (doc) {
|
|
var key, self = this
|
|
, keys, i, failingI, error
|
|
;
|
|
|
|
if (util.isArray(doc)) { this.insertMultipleDocs(doc); return; }
|
|
|
|
key = model.getDotValue(doc, this.fieldName);
|
|
|
|
// We don't index documents that don't contain the field if the index is sparse
|
|
if (key === undefined && this.sparse) { return; }
|
|
|
|
if (!util.isArray(key)) {
|
|
this.tree.insert(key, doc);
|
|
} else {
|
|
// If an insert fails due to a unique constraint, roll back all inserts before it
|
|
keys = _.uniq(key, projectForUnique);
|
|
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
try {
|
|
this.tree.insert(keys[i], doc);
|
|
} catch (e) {
|
|
error = e;
|
|
failingI = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
for (i = 0; i < failingI; i += 1) {
|
|
this.tree.delete(keys[i], doc);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Insert an array of documents in the index
|
|
* If a constraint is violated, the changes should be rolled back and an error thrown
|
|
*
|
|
* @API private
|
|
*/
|
|
Index.prototype.insertMultipleDocs = function (docs) {
|
|
var i, error, failingI;
|
|
|
|
for (i = 0; i < docs.length; i += 1) {
|
|
try {
|
|
this.insert(docs[i]);
|
|
} catch (e) {
|
|
error = e;
|
|
failingI = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
for (i = 0; i < failingI; i += 1) {
|
|
this.remove(docs[i]);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Remove a document from the index
|
|
* If an array is passed, we remove all its elements
|
|
* The remove operation is safe with regards to the 'unique' constraint
|
|
* O(log(n))
|
|
*/
|
|
Index.prototype.remove = function (doc) {
|
|
var key, self = this;
|
|
|
|
if (util.isArray(doc)) { doc.forEach(function (d) { self.remove(d); }); return; }
|
|
|
|
key = model.getDotValue(doc, this.fieldName);
|
|
|
|
if (key === undefined && this.sparse) { return; }
|
|
|
|
if (!util.isArray(key)) {
|
|
this.tree.delete(key, doc);
|
|
} else {
|
|
_.uniq(key, projectForUnique).forEach(function (_key) {
|
|
self.tree.delete(_key, doc);
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Update a document in the index
|
|
* If a constraint is violated, changes are rolled back and an error thrown
|
|
* Naive implementation, still in O(log(n))
|
|
*/
|
|
Index.prototype.update = function (oldDoc, newDoc) {
|
|
if (util.isArray(oldDoc)) { this.updateMultipleDocs(oldDoc); return; }
|
|
|
|
this.remove(oldDoc);
|
|
|
|
try {
|
|
this.insert(newDoc);
|
|
} catch (e) {
|
|
this.insert(oldDoc);
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Update multiple documents in the index
|
|
* If a constraint is violated, the changes need to be rolled back
|
|
* and an error thrown
|
|
* @param {Array of oldDoc, newDoc pairs} pairs
|
|
*
|
|
* @API private
|
|
*/
|
|
Index.prototype.updateMultipleDocs = function (pairs) {
|
|
var i, failingI, error;
|
|
|
|
for (i = 0; i < pairs.length; i += 1) {
|
|
this.remove(pairs[i].oldDoc);
|
|
}
|
|
|
|
for (i = 0; i < pairs.length; i += 1) {
|
|
try {
|
|
this.insert(pairs[i].newDoc);
|
|
} catch (e) {
|
|
error = e;
|
|
failingI = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If an error was raised, roll back changes in the inverse order
|
|
if (error) {
|
|
for (i = 0; i < failingI; i += 1) {
|
|
this.remove(pairs[i].newDoc);
|
|
}
|
|
|
|
for (i = 0; i < pairs.length; i += 1) {
|
|
this.insert(pairs[i].oldDoc);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Revert an update
|
|
*/
|
|
Index.prototype.revertUpdate = function (oldDoc, newDoc) {
|
|
var revert = [];
|
|
|
|
if (!util.isArray(oldDoc)) {
|
|
this.update(newDoc, oldDoc);
|
|
} else {
|
|
oldDoc.forEach(function (pair) {
|
|
revert.push({ oldDoc: pair.newDoc, newDoc: pair.oldDoc });
|
|
});
|
|
this.update(revert);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Get all documents in index whose key match value (if it is a Thing) or one of the elements of value (if it is an array of Things)
|
|
* @param {Thing} value Value to match the key against
|
|
* @return {Array of documents}
|
|
*/
|
|
Index.prototype.getMatching = function (value) {
|
|
var self = this;
|
|
|
|
if (!util.isArray(value)) {
|
|
return self.tree.search(value);
|
|
} else {
|
|
var _res = {}, res = [];
|
|
|
|
value.forEach(function (v) {
|
|
self.getMatching(v).forEach(function (doc) {
|
|
_res[doc._id] = doc;
|
|
});
|
|
});
|
|
|
|
Object.keys(_res).forEach(function (_id) {
|
|
res.push(_res[_id]);
|
|
});
|
|
|
|
return res;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Get all documents in index whose key is between bounds are they are defined by query
|
|
* Documents are sorted by key
|
|
* @param {Query} query
|
|
* @return {Array of documents}
|
|
*/
|
|
Index.prototype.getBetweenBounds = function (query) {
|
|
return this.tree.betweenBounds(query);
|
|
};
|
|
|
|
|
|
/**
|
|
* Get all elements in the index
|
|
* @return {Array of documents}
|
|
*/
|
|
Index.prototype.getAll = function () {
|
|
var res = [];
|
|
|
|
this.tree.executeOnEveryNode(function (node) {
|
|
var i;
|
|
|
|
for (i = 0; i < node.data.length; i += 1) {
|
|
res.push(node.data[i]);
|
|
}
|
|
});
|
|
|
|
return res;
|
|
};
|
|
|
|
|
|
|
|
|
|
// Interface
|
|
module.exports = Index;
|
|
|
|
},{"./model":10,"binary-search-tree":14,"underscore":19,"util":3}],10:[function(require,module,exports){
|
|
/**
|
|
* Handle models (i.e. docs)
|
|
* Serialization/deserialization
|
|
* Copying
|
|
* Querying, update
|
|
*/
|
|
|
|
var util = require('util')
|
|
, _ = require('underscore')
|
|
, modifierFunctions = {}
|
|
, lastStepModifierFunctions = {}
|
|
, comparisonFunctions = {}
|
|
, logicalOperators = {}
|
|
, arrayComparisonFunctions = {}
|
|
;
|
|
|
|
|
|
/**
|
|
* 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 (typeof k === 'number') {
|
|
k = k.toString();
|
|
}
|
|
|
|
if (k[0] === '$' && !(k === '$$date' && typeof v === 'number') && !(k === '$$deleted' && v === true) && !(k === '$$indexCreated') && !(k === '$$indexRemoved')) {
|
|
throw new Error('Field names cannot begin with the $ character');
|
|
}
|
|
|
|
if (k.indexOf('.') !== -1) {
|
|
throw new Error('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;
|
|
|
|
res = JSON.stringify(obj, function (k, v) {
|
|
checkKey(k, v);
|
|
|
|
if (v === undefined) { return undefined; }
|
|
if (v === null) { return null; }
|
|
|
|
// Hackish way of checking if object is Date (this way it works between execution contexts in node-webkit).
|
|
// We can't use value directly because for dates it is already string in this function (date.toJSON was already called), so we use this
|
|
if (typeof this[k].getTime === 'function') { return { $$date: this[k].getTime() }; }
|
|
|
|
return v;
|
|
});
|
|
|
|
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
|
|
* The optional strictKeys flag (defaulting to false) indicates whether to copy everything or only fields
|
|
* where the keys are valid, i.e. don't begin with $ and don't contain a .
|
|
*/
|
|
function deepCopy (obj, strictKeys) {
|
|
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(deepCopy(o, strictKeys)); });
|
|
return res;
|
|
}
|
|
|
|
if (typeof obj === 'object') {
|
|
res = {};
|
|
Object.keys(obj).forEach(function (k) {
|
|
if (!strictKeys || (k[0] !== '$' && k.indexOf('.') === -1)) {
|
|
res[k] = deepCopy(obj[k], strictKeys);
|
|
}
|
|
});
|
|
return res;
|
|
}
|
|
|
|
return undefined; // For now everything else is undefined. We should probably throw an error instead
|
|
}
|
|
|
|
|
|
/**
|
|
* Tells if an object is a primitive type or a "real" object
|
|
* Arrays are considered primitive
|
|
*/
|
|
function isPrimitiveType (obj) {
|
|
return ( typeof obj === 'boolean' ||
|
|
typeof obj === 'number' ||
|
|
typeof obj === 'string' ||
|
|
obj === null ||
|
|
util.isDate(obj) ||
|
|
util.isArray(obj));
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 deep-compare
|
|
* 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!)
|
|
*
|
|
* @param {Function} _compareStrings String comparing function, returning -1, 0 or 1, overriding default string comparison (useful for languages with accented letters)
|
|
*/
|
|
function compareThings (a, b, _compareStrings) {
|
|
var aKeys, bKeys, comp, i
|
|
, compareStrings = _compareStrings || compareNSB;
|
|
|
|
// 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' ? compareStrings(a, b) : -1; }
|
|
if (typeof b === 'string') { return typeof a === 'string' ? compareStrings(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;
|
|
};
|
|
|
|
|
|
/**
|
|
* Unset a field
|
|
*/
|
|
lastStepModifierFunctions.$unset = function (obj, field, value) {
|
|
delete obj[field];
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 new Error("Can't $push an element on non-array values"); }
|
|
|
|
if (value !== null && typeof value === 'object' && value.$each) {
|
|
if (Object.keys(value).length > 1) { throw new Error("Can't use another field in conjunction with $each"); }
|
|
if (!util.isArray(value.$each)) { throw new Error("$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 new Error("Can't $addToSet an element on non-array values"); }
|
|
|
|
if (value !== null && typeof value === 'object' && value.$each) {
|
|
if (Object.keys(value).length > 1) { throw new Error("Can't use another field in conjunction with $each"); }
|
|
if (!util.isArray(value.$each)) { throw new Error("$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 new Error("Can't $pop an element from non-array values"); }
|
|
if (typeof value !== 'number') { throw new Error(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);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes all instances of a value from an existing array
|
|
*/
|
|
lastStepModifierFunctions.$pull = function (obj, field, value) {
|
|
var arr, i;
|
|
|
|
if (!util.isArray(obj[field])) { throw new Error("Can't $pull an element from non-array values"); }
|
|
|
|
arr = obj[field];
|
|
for (i = arr.length - 1; i >= 0; i -= 1) {
|
|
if (match(arr[i], value)) {
|
|
arr.splice(i, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Increment a numeric field's value
|
|
*/
|
|
lastStepModifierFunctions.$inc = function (obj, field, value) {
|
|
if (typeof value !== 'number') { throw new Error(value + " must be a number"); }
|
|
|
|
if (typeof obj[field] !== 'number') {
|
|
if (!_.has(obj, field)) {
|
|
obj[field] = value;
|
|
} else {
|
|
throw new Error("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
|
|
*/
|
|
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 new Error("You cannot change a document's _id"); }
|
|
|
|
if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) {
|
|
throw new Error("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 new Error("Unknown modifier " + m); }
|
|
|
|
// Can't rely on Object.keys throwing on non objects since ES6{
|
|
// Not 100% satisfying as non objects can be interpreted as objects but no false negatives so we can live with it
|
|
if (typeof updateQuery[m] !== 'object') {
|
|
throw new Error("Modifier " + m + "'s argument must be an object");
|
|
}
|
|
|
|
keys = Object.keys(updateQuery[m]);
|
|
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 new Error("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
|
|
, i, objs;
|
|
|
|
if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match
|
|
|
|
if (fieldParts.length === 0) { return obj; }
|
|
|
|
if (fieldParts.length === 1) { return obj[fieldParts[0]]; }
|
|
|
|
if (util.isArray(obj[fieldParts[0]])) {
|
|
// If the next field is an integer, return only this item of the array
|
|
i = parseInt(fieldParts[1], 10);
|
|
if (typeof i === 'number' && !isNaN(i)) {
|
|
return getDotValue(obj[fieldParts[0]][i], fieldParts.slice(2))
|
|
}
|
|
|
|
// Return the array of values
|
|
objs = new Array();
|
|
for (i = 0; i < obj[fieldParts[0]].length; i += 1) {
|
|
objs.push(getDotValue(obj[fieldParts[0]][i], fieldParts.slice(1)));
|
|
}
|
|
return objs;
|
|
} 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)) && (util.isArray(a) || util.isArray(b))) || a === undefined || b === undefined) { return false; }
|
|
|
|
// General 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 === undefined) { return true; }
|
|
return !areThingsEqual(a, b);
|
|
};
|
|
|
|
comparisonFunctions.$in = function (a, b) {
|
|
var i;
|
|
|
|
if (!util.isArray(b)) { throw new Error("$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 new Error("$nin operator called with a non-array"); }
|
|
|
|
return !comparisonFunctions.$in(a, b);
|
|
};
|
|
|
|
comparisonFunctions.$regex = function (a, b) {
|
|
if (!util.isRegExp(b)) { throw new Error("$regex operator called with non regular expression"); }
|
|
|
|
if (typeof a !== 'string') {
|
|
return false
|
|
} else {
|
|
return b.test(a);
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
// Specific to arrays
|
|
comparisonFunctions.$size = function (obj, value) {
|
|
if (!util.isArray(obj)) { return false; }
|
|
if (value % 1 !== 0) { throw new Error("$size operator called without an integer"); }
|
|
|
|
return (obj.length == value);
|
|
};
|
|
arrayComparisonFunctions.$size = true;
|
|
|
|
|
|
/**
|
|
* 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 new Error("$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 new Error("$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);
|
|
};
|
|
|
|
|
|
/**
|
|
* Use a function to match
|
|
* @param {Model} obj
|
|
* @param {Query} query
|
|
*/
|
|
logicalOperators.$where = function (obj, fn) {
|
|
var result;
|
|
|
|
if (!_.isFunction(fn)) { throw new Error("$where operator used without a function"); }
|
|
|
|
result = fn.call(obj);
|
|
if (!_.isBoolean(result)) { throw new Error("$where function must return boolean"); }
|
|
|
|
return result;
|
|
};
|
|
|
|
|
|
/**
|
|
* Tell if a given document matches a query
|
|
* @param {Object} obj Document to check
|
|
* @param {Object} query
|
|
*/
|
|
function match (obj, query) {
|
|
var queryKeys, queryKey, queryValue, i;
|
|
|
|
// Primitive query against a primitive type
|
|
// This is a bit of a hack since we construct an object with an arbitrary key only to dereference it later
|
|
// But I don't have time for a cleaner implementation now
|
|
if (isPrimitiveType(obj) || isPrimitiveType(query)) {
|
|
return matchQueryPart({ needAKey: obj }, 'needAKey', query);
|
|
}
|
|
|
|
// Normal query
|
|
queryKeys = Object.keys(query);
|
|
for (i = 0; i < queryKeys.length; i += 1) {
|
|
queryKey = queryKeys[i];
|
|
queryValue = query[queryKey];
|
|
|
|
if (queryKey[0] === '$') {
|
|
if (!logicalOperators[queryKey]) { throw new Error("Unknown logical operator " + queryKey); }
|
|
if (!logicalOperators[queryKey](obj, queryValue)) { return false; }
|
|
} else {
|
|
if (!matchQueryPart(obj, queryKey, queryValue)) { return false; }
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Match an object against a specific { key: value } part of a query
|
|
* if the treatObjAsValue flag is set, don't try to match every part separately, but the array as a whole
|
|
*/
|
|
function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) {
|
|
var objValue = getDotValue(obj, queryKey)
|
|
, i, keys, firstChars, dollarFirstChars;
|
|
|
|
// Check if the value is an array if we don't force a treatment as value
|
|
if (util.isArray(objValue) && !treatObjAsValue) {
|
|
// If the queryValue is an array, try to perform an exact match
|
|
if (util.isArray(queryValue)) {
|
|
return matchQueryPart(obj, queryKey, queryValue, true);
|
|
}
|
|
|
|
// Check if we are using an array-specific comparison function
|
|
if (queryValue !== null && typeof queryValue === 'object' && !util.isRegExp(queryValue)) {
|
|
keys = Object.keys(queryValue);
|
|
for (i = 0; i < keys.length; i += 1) {
|
|
if (arrayComparisonFunctions[keys[i]]) { return matchQueryPart(obj, queryKey, queryValue, true); }
|
|
}
|
|
}
|
|
|
|
// If not, treat it as an array of { obj, query } where there needs to be at least one match
|
|
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' && !util.isRegExp(queryValue) && !util.isArray(queryValue)) {
|
|
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 new Error("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 new Error("Unknown comparison function " + keys[i]); }
|
|
|
|
if (!comparisonFunctions[keys[i]](objValue, queryValue[keys[i]])) { return false; }
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Using regular expressions with basic querying
|
|
if (util.isRegExp(queryValue)) { return comparisonFunctions.$regex(objValue, queryValue); }
|
|
|
|
// queryValue is either a native value or a normal object
|
|
// Basic 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.isPrimitiveType = isPrimitiveType;
|
|
module.exports.modify = modify;
|
|
module.exports.getDotValue = getDotValue;
|
|
module.exports.match = match;
|
|
module.exports.areThingsEqual = areThingsEqual;
|
|
module.exports.compareThings = compareThings;
|
|
|
|
},{"underscore":19,"util":3}],11:[function(require,module,exports){
|
|
var process=require("__browserify_process");/**
|
|
* Handle every persistence-related task
|
|
* The interface Datastore expects to be implemented is
|
|
* * Persistence.loadDatabase(callback) and callback has signature err
|
|
* * Persistence.persistNewState(newDocs, callback) where newDocs is an array of documents and callback has signature err
|
|
*/
|
|
|
|
var storage = require('./storage')
|
|
, path = require('path')
|
|
, model = require('./model')
|
|
, async = require('async')
|
|
, customUtils = require('./customUtils')
|
|
, Index = require('./indexes')
|
|
;
|
|
|
|
|
|
/**
|
|
* Create a new Persistence object for database options.db
|
|
* @param {Datastore} options.db
|
|
* @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 Persistence (options) {
|
|
var i, j, randomString;
|
|
|
|
this.db = options.db;
|
|
this.inMemoryOnly = this.db.inMemoryOnly;
|
|
this.filename = this.db.filename;
|
|
this.corruptAlertThreshold = options.corruptAlertThreshold !== undefined ? options.corruptAlertThreshold : 0.1;
|
|
|
|
if (!this.inMemoryOnly && this.filename && this.filename.charAt(this.filename.length - 1) === '~') {
|
|
throw new Error("The datafile name can't end with a ~, which is reserved for crash safe backup files");
|
|
}
|
|
|
|
// After serialization and before deserialization hooks with some basic sanity checks
|
|
if (options.afterSerialization && !options.beforeDeserialization) {
|
|
throw new Error("Serialization hook defined but deserialization hook undefined, cautiously refusing to start NeDB to prevent dataloss");
|
|
}
|
|
if (!options.afterSerialization && options.beforeDeserialization) {
|
|
throw new Error("Serialization hook undefined but deserialization hook defined, cautiously refusing to start NeDB to prevent dataloss");
|
|
}
|
|
this.afterSerialization = options.afterSerialization || function (s) { return s; };
|
|
this.beforeDeserialization = options.beforeDeserialization || function (s) { return s; };
|
|
for (i = 1; i < 30; i += 1) {
|
|
for (j = 0; j < 10; j += 1) {
|
|
randomString = customUtils.uid(i);
|
|
if (this.beforeDeserialization(this.afterSerialization(randomString)) !== randomString) {
|
|
throw new Error("beforeDeserialization is not the reverse of afterSerialization, cautiously refusing to start NeDB to prevent dataloss");
|
|
}
|
|
}
|
|
}
|
|
|
|
// For NW apps, store data in the same directory where NW stores application data
|
|
if (this.filename && options.nodeWebkitAppName) {
|
|
console.log("==================================================================");
|
|
console.log("WARNING: The nodeWebkitAppName option is deprecated");
|
|
console.log("To get the path to the directory where Node Webkit stores the data");
|
|
console.log("for your app, use the internal nw.gui module like this");
|
|
console.log("require('nw.gui').App.dataPath");
|
|
console.log("See https://github.com/rogerwang/node-webkit/issues/500");
|
|
console.log("==================================================================");
|
|
this.filename = Persistence.getNWAppFilename(options.nodeWebkitAppName, this.filename);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if a directory exists and create it on the fly if it is not the case
|
|
* cb is optional, signature: err
|
|
*/
|
|
Persistence.ensureDirectoryExists = function (dir, cb) {
|
|
var callback = cb || function () {}
|
|
;
|
|
|
|
storage.mkdirp(dir, function (err) { return callback(err); });
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Return the path the datafile if the given filename is relative to the directory where Node Webkit stores
|
|
* data for this application. Probably the best place to store data
|
|
*/
|
|
Persistence.getNWAppFilename = function (appName, relativeFilename) {
|
|
var home;
|
|
|
|
switch (process.platform) {
|
|
case 'win32':
|
|
case 'win64':
|
|
home = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
if (!home) { throw new Error("Couldn't find the base application data folder"); }
|
|
home = path.join(home, appName);
|
|
break;
|
|
case 'darwin':
|
|
home = process.env.HOME;
|
|
if (!home) { throw new Error("Couldn't find the base application data directory"); }
|
|
home = path.join(home, 'Library', 'Application Support', appName);
|
|
break;
|
|
case 'linux':
|
|
home = process.env.HOME;
|
|
if (!home) { throw new Error("Couldn't find the base application data directory"); }
|
|
home = path.join(home, '.config', appName);
|
|
break;
|
|
default:
|
|
throw new Error("Can't use the Node Webkit relative path for platform " + process.platform);
|
|
break;
|
|
}
|
|
|
|
return path.join(home, 'nedb-data', relativeFilename);
|
|
}
|
|
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
Persistence.prototype.persistCachedDatabase = function (cb) {
|
|
var callback = cb || function () {}
|
|
, toPersist = ''
|
|
, self = this
|
|
;
|
|
|
|
if (this.inMemoryOnly) { return callback(null); }
|
|
|
|
this.db.getAllData().forEach(function (doc) {
|
|
toPersist += self.afterSerialization(model.serialize(doc)) + '\n';
|
|
});
|
|
Object.keys(this.db.indexes).forEach(function (fieldName) {
|
|
if (fieldName != "_id") { // The special _id index is managed by datastore.js, the others need to be persisted
|
|
toPersist += self.afterSerialization(model.serialize({ $$indexCreated: { fieldName: fieldName, unique: self.db.indexes[fieldName].unique, sparse: self.db.indexes[fieldName].sparse }})) + '\n';
|
|
}
|
|
});
|
|
|
|
storage.crashSafeWriteFile(this.filename, toPersist, function (err) {
|
|
if (err) { return callback(err); }
|
|
self.db.emit('compaction.done');
|
|
return callback(null);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Queue a rewrite of the datafile
|
|
*/
|
|
Persistence.prototype.compactDatafile = function () {
|
|
this.db.executor.push({ this: this, fn: this.persistCachedDatabase, arguments: [] });
|
|
};
|
|
|
|
|
|
/**
|
|
* Set automatic compaction every interval ms
|
|
* @param {Number} interval in milliseconds, with an enforced minimum of 5 seconds
|
|
*/
|
|
Persistence.prototype.setAutocompactionInterval = function (interval) {
|
|
var self = this
|
|
, minInterval = 5000
|
|
, realInterval = Math.max(interval || 0, minInterval)
|
|
;
|
|
|
|
this.stopAutocompaction();
|
|
|
|
this.autocompactionIntervalId = setInterval(function () {
|
|
self.compactDatafile();
|
|
}, realInterval);
|
|
};
|
|
|
|
|
|
/**
|
|
* Stop autocompaction (do nothing if autocompaction was not running)
|
|
*/
|
|
Persistence.prototype.stopAutocompaction = function () {
|
|
if (this.autocompactionIntervalId) { clearInterval(this.autocompactionIntervalId); }
|
|
};
|
|
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
Persistence.prototype.persistNewState = function (newDocs, cb) {
|
|
var self = this
|
|
, toPersist = ''
|
|
, callback = cb || function () {}
|
|
;
|
|
|
|
// In-memory only datastore
|
|
if (self.inMemoryOnly) { return callback(null); }
|
|
|
|
newDocs.forEach(function (doc) {
|
|
toPersist += self.afterSerialization(model.serialize(doc)) + '\n';
|
|
});
|
|
|
|
if (toPersist.length === 0) { return callback(null); }
|
|
|
|
storage.appendFile(self.filename, toPersist, 'utf8', function (err) {
|
|
return callback(err);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* From a database's raw data, return the corresponding
|
|
* machine understandable collection
|
|
*/
|
|
Persistence.prototype.treatRawData = function (rawData) {
|
|
var data = rawData.split('\n')
|
|
, dataById = {}
|
|
, tdata = []
|
|
, i
|
|
, indexes = {}
|
|
, corruptItems = -1 // Last line of every data file is usually blank so not really corrupt
|
|
;
|
|
|
|
for (i = 0; i < data.length; i += 1) {
|
|
var doc;
|
|
|
|
try {
|
|
doc = model.deserialize(this.beforeDeserialization(data[i]));
|
|
if (doc._id) {
|
|
if (doc.$$deleted === true) {
|
|
delete dataById[doc._id];
|
|
} else {
|
|
dataById[doc._id] = doc;
|
|
}
|
|
} else if (doc.$$indexCreated && doc.$$indexCreated.fieldName != undefined) {
|
|
indexes[doc.$$indexCreated.fieldName] = doc.$$indexCreated;
|
|
} else if (typeof doc.$$indexRemoved === "string") {
|
|
delete indexes[doc.$$indexRemoved];
|
|
}
|
|
} catch (e) {
|
|
corruptItems += 1;
|
|
}
|
|
}
|
|
|
|
// A bit lenient on corruption
|
|
if (data.length > 0 && corruptItems / data.length > this.corruptAlertThreshold) {
|
|
throw new Error("More than " + Math.floor(100 * this.corruptAlertThreshold) + "% of the data file is corrupt, the wrong beforeDeserialization hook may be used. Cautiously refusing to start NeDB to prevent dataloss");
|
|
}
|
|
|
|
Object.keys(dataById).forEach(function (k) {
|
|
tdata.push(dataById[k]);
|
|
});
|
|
|
|
return { data: tdata, indexes: indexes };
|
|
};
|
|
|
|
|
|
/**
|
|
* Load the database
|
|
* 1) Create all indexes
|
|
* 2) Insert all data
|
|
* 3) Compact 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
|
|
*/
|
|
Persistence.prototype.loadDatabase = function (cb) {
|
|
var callback = cb || function () {}
|
|
, self = this
|
|
;
|
|
|
|
self.db.resetIndexes();
|
|
|
|
// In-memory only datastore
|
|
if (self.inMemoryOnly) { return callback(null); }
|
|
|
|
async.waterfall([
|
|
function (cb) {
|
|
Persistence.ensureDirectoryExists(path.dirname(self.filename), function (err) {
|
|
storage.ensureDatafileIntegrity(self.filename, function (err) {
|
|
storage.readFile(self.filename, 'utf8', function (err, rawData) {
|
|
if (err) { return cb(err); }
|
|
|
|
try {
|
|
var treatedData = self.treatRawData(rawData);
|
|
} catch (e) {
|
|
return cb(e);
|
|
}
|
|
|
|
// Recreate all indexes in the datafile
|
|
Object.keys(treatedData.indexes).forEach(function (key) {
|
|
self.db.indexes[key] = new Index(treatedData.indexes[key]);
|
|
});
|
|
|
|
// Fill cached database (i.e. all indexes) with data
|
|
try {
|
|
self.db.resetIndexes(treatedData.data);
|
|
} catch (e) {
|
|
self.db.resetIndexes(); // Rollback any index which didn't fail
|
|
return cb(e);
|
|
}
|
|
|
|
self.db.persistence.persistCachedDatabase(cb);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
], function (err) {
|
|
if (err) { return callback(err); }
|
|
|
|
self.db.executor.processBuffer();
|
|
return callback(null);
|
|
});
|
|
};
|
|
|
|
|
|
// Interface
|
|
module.exports = Persistence;
|
|
|
|
},{"./customUtils":6,"./indexes":9,"./model":10,"./storage":12,"__browserify_process":4,"async":13,"path":2}],12:[function(require,module,exports){
|
|
/**
|
|
* Way data is stored for this database
|
|
* For a Node.js/Node Webkit database it's the file system
|
|
* For a browser-side database it's localforage, which uses the best backend available (IndexedDB then WebSQL then localStorage)
|
|
*
|
|
* This version is the browser version
|
|
*/
|
|
|
|
var localforage = require('localforage')
|
|
|
|
// Configure localforage to display NeDB name for now. Would be a good idea to let user use his own app name
|
|
localforage.config({
|
|
name: 'NeDB'
|
|
, storeName: 'nedbdata'
|
|
});
|
|
|
|
|
|
function exists (filename, callback) {
|
|
localforage.getItem(filename, function (err, value) {
|
|
if (value !== null) { // Even if value is undefined, localforage returns null
|
|
return callback(true);
|
|
} else {
|
|
return callback(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function rename (filename, newFilename, callback) {
|
|
localforage.getItem(filename, function (err, value) {
|
|
if (value === null) {
|
|
localforage.removeItem(newFilename, function () { return callback(); });
|
|
} else {
|
|
localforage.setItem(newFilename, value, function () {
|
|
localforage.removeItem(filename, function () { return callback(); });
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function writeFile (filename, contents, options, callback) {
|
|
// Options do not matter in browser setup
|
|
if (typeof options === 'function') { callback = options; }
|
|
localforage.setItem(filename, contents, function () { return callback(); });
|
|
}
|
|
|
|
|
|
function appendFile (filename, toAppend, options, callback) {
|
|
// Options do not matter in browser setup
|
|
if (typeof options === 'function') { callback = options; }
|
|
|
|
localforage.getItem(filename, function (err, contents) {
|
|
contents = contents || '';
|
|
contents += toAppend;
|
|
localforage.setItem(filename, contents, function () { return callback(); });
|
|
});
|
|
}
|
|
|
|
|
|
function readFile (filename, options, callback) {
|
|
// Options do not matter in browser setup
|
|
if (typeof options === 'function') { callback = options; }
|
|
localforage.getItem(filename, function (err, contents) { return callback(null, contents || ''); });
|
|
}
|
|
|
|
|
|
function unlink (filename, callback) {
|
|
localforage.removeItem(filename, function () { return callback(); });
|
|
}
|
|
|
|
|
|
// Nothing to do, no directories will be used on the browser
|
|
function mkdirp (dir, callback) {
|
|
return callback();
|
|
}
|
|
|
|
|
|
// Nothing to do, no data corruption possible in the brower
|
|
function ensureDatafileIntegrity (filename, callback) {
|
|
return callback(null);
|
|
}
|
|
|
|
|
|
// Interface
|
|
module.exports.exists = exists;
|
|
module.exports.rename = rename;
|
|
module.exports.writeFile = writeFile;
|
|
module.exports.crashSafeWriteFile = writeFile; // No need for a crash safe function in the browser
|
|
module.exports.appendFile = appendFile;
|
|
module.exports.readFile = readFile;
|
|
module.exports.unlink = unlink;
|
|
module.exports.mkdirp = mkdirp;
|
|
module.exports.ensureDatafileIntegrity = ensureDatafileIntegrity;
|
|
|
|
|
|
},{"localforage":18}],13:[function(require,module,exports){
|
|
var process=require("__browserify_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.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 <script> tag
|
|
else {
|
|
root.async = async;
|
|
}
|
|
|
|
}());
|
|
|
|
},{"__browserify_process":4}],14:[function(require,module,exports){
|
|
module.exports.BinarySearchTree = require('./lib/bst');
|
|
module.exports.AVLTree = require('./lib/avltree');
|
|
|
|
},{"./lib/avltree":15,"./lib/bst":16}],15:[function(require,module,exports){
|
|
/**
|
|
* Self-balancing binary search tree using the AVL implementation
|
|
*/
|
|
var BinarySearchTree = require('./bst')
|
|
, customUtils = require('./customUtils')
|
|
, util = require('util')
|
|
, _ = require('underscore')
|
|
;
|
|
|
|
|
|
/**
|
|
* Constructor
|
|
* We can't use a direct pointer to the root node (as in the simple binary search tree)
|
|
* as the root will change during tree rotations
|
|
* @param {Boolean} options.unique Whether to enforce a 'unique' constraint on the key or not
|
|
* @param {Function} options.compareKeys Initialize this BST's compareKeys
|
|
*/
|
|
function AVLTree (options) {
|
|
this.tree = new _AVLTree(options);
|
|
}
|
|
|
|
|
|
/**
|
|
* Constructor of the internal AVLTree
|
|
* @param {Object} options Optional
|
|
* @param {Boolean} options.unique Whether to enforce a 'unique' constraint on the key or not
|
|
* @param {Key} options.key Initialize this BST's key with key
|
|
* @param {Value} options.value Initialize this BST's data with [value]
|
|
* @param {Function} options.compareKeys Initialize this BST's compareKeys
|
|
*/
|
|
function _AVLTree (options) {
|
|
options = options || {};
|
|
|
|
this.left = null;
|
|
this.right = null;
|
|
this.parent = options.parent !== undefined ? options.parent : null;
|
|
if (options.hasOwnProperty('key')) { this.key = options.key; }
|
|
this.data = options.hasOwnProperty('value') ? [options.value] : [];
|
|
this.unique = options.unique || false;
|
|
|
|
this.compareKeys = options.compareKeys || customUtils.defaultCompareKeysFunction;
|
|
this.checkValueEquality = options.checkValueEquality || customUtils.defaultCheckValueEquality;
|
|
}
|
|
|
|
|
|
/**
|
|
* Inherit basic functions from the basic binary search tree
|
|
*/
|
|
util.inherits(_AVLTree, BinarySearchTree);
|
|
|
|
/**
|
|
* Keep a pointer to the internal tree constructor for testing purposes
|
|
*/
|
|
AVLTree._AVLTree = _AVLTree;
|
|
|
|
|
|
/**
|
|
* Check the recorded height is correct for every node
|
|
* Throws if one height doesn't match
|
|
*/
|
|
_AVLTree.prototype.checkHeightCorrect = function () {
|
|
var leftH, rightH;
|
|
|
|
if (!this.hasOwnProperty('key')) { return; } // Empty tree
|
|
|
|
if (this.left && this.left.height === undefined) { throw new Error("Undefined height for node " + this.left.key); }
|
|
if (this.right && this.right.height === undefined) { throw new Error("Undefined height for node " + this.right.key); }
|
|
if (this.height === undefined) { throw new Error("Undefined height for node " + this.key); }
|
|
|
|
leftH = this.left ? this.left.height : 0;
|
|
rightH = this.right ? this.right.height : 0;
|
|
|
|
if (this.height !== 1 + Math.max(leftH, rightH)) { throw new Error("Height constraint failed for node " + this.key); }
|
|
if (this.left) { this.left.checkHeightCorrect(); }
|
|
if (this.right) { this.right.checkHeightCorrect(); }
|
|
};
|
|
|
|
|
|
/**
|
|
* Return the balance factor
|
|
*/
|
|
_AVLTree.prototype.balanceFactor = function () {
|
|
var leftH = this.left ? this.left.height : 0
|
|
, rightH = this.right ? this.right.height : 0
|
|
;
|
|
return leftH - rightH;
|
|
};
|
|
|
|
|
|
/**
|
|
* Check that the balance factors are all between -1 and 1
|
|
*/
|
|
_AVLTree.prototype.checkBalanceFactors = function () {
|
|
if (Math.abs(this.balanceFactor()) > 1) { throw new Error('Tree is unbalanced at node ' + this.key); }
|
|
|
|
if (this.left) { this.left.checkBalanceFactors(); }
|
|
if (this.right) { this.right.checkBalanceFactors(); }
|
|
};
|
|
|
|
|
|
/**
|
|
* When checking if the BST conditions are met, also check that the heights are correct
|
|
* and the tree is balanced
|
|
*/
|
|
_AVLTree.prototype.checkIsAVLT = function () {
|
|
_AVLTree.super_.prototype.checkIsBST.call(this);
|
|
this.checkHeightCorrect();
|
|
this.checkBalanceFactors();
|
|
};
|
|
AVLTree.prototype.checkIsAVLT = function () { this.tree.checkIsAVLT(); };
|
|
|
|
|
|
/**
|
|
* Perform a right rotation of the tree if possible
|
|
* and return the root of the resulting tree
|
|
* The resulting tree's nodes' heights are also updated
|
|
*/
|
|
_AVLTree.prototype.rightRotation = function () {
|
|
var q = this
|
|
, p = this.left
|
|
, b
|
|
, ah, bh, ch;
|
|
|
|
if (!p) { return this; } // No change
|
|
|
|
b = p.right;
|
|
|
|
// Alter tree structure
|
|
if (q.parent) {
|
|
p.parent = q.parent;
|
|
if (q.parent.left === q) { q.parent.left = p; } else { q.parent.right = p; }
|
|
} else {
|
|
p.parent = null;
|
|
}
|
|
p.right = q;
|
|
q.parent = p;
|
|
q.left = b;
|
|
if (b) { b.parent = q; }
|
|
|
|
// Update heights
|
|
ah = p.left ? p.left.height : 0;
|
|
bh = b ? b.height : 0;
|
|
ch = q.right ? q.right.height : 0;
|
|
q.height = Math.max(bh, ch) + 1;
|
|
p.height = Math.max(ah, q.height) + 1;
|
|
|
|
return p;
|
|
};
|
|
|
|
|
|
/**
|
|
* Perform a left rotation of the tree if possible
|
|
* and return the root of the resulting tree
|
|
* The resulting tree's nodes' heights are also updated
|
|
*/
|
|
_AVLTree.prototype.leftRotation = function () {
|
|
var p = this
|
|
, q = this.right
|
|
, b
|
|
, ah, bh, ch;
|
|
|
|
if (!q) { return this; } // No change
|
|
|
|
b = q.left;
|
|
|
|
// Alter tree structure
|
|
if (p.parent) {
|
|
q.parent = p.parent;
|
|
if (p.parent.left === p) { p.parent.left = q; } else { p.parent.right = q; }
|
|
} else {
|
|
q.parent = null;
|
|
}
|
|
q.left = p;
|
|
p.parent = q;
|
|
p.right = b;
|
|
if (b) { b.parent = p; }
|
|
|
|
// Update heights
|
|
ah = p.left ? p.left.height : 0;
|
|
bh = b ? b.height : 0;
|
|
ch = q.right ? q.right.height : 0;
|
|
p.height = Math.max(ah, bh) + 1;
|
|
q.height = Math.max(ch, p.height) + 1;
|
|
|
|
return q;
|
|
};
|
|
|
|
|
|
/**
|
|
* Modify the tree if its right subtree is too small compared to the left
|
|
* Return the new root if any
|
|
*/
|
|
_AVLTree.prototype.rightTooSmall = function () {
|
|
if (this.balanceFactor() <= 1) { return this; } // Right is not too small, don't change
|
|
|
|
if (this.left.balanceFactor() < 0) {
|
|
this.left.leftRotation();
|
|
}
|
|
|
|
return this.rightRotation();
|
|
};
|
|
|
|
|
|
/**
|
|
* Modify the tree if its left subtree is too small compared to the right
|
|
* Return the new root if any
|
|
*/
|
|
_AVLTree.prototype.leftTooSmall = function () {
|
|
if (this.balanceFactor() >= -1) { return this; } // Left is not too small, don't change
|
|
|
|
if (this.right.balanceFactor() > 0) {
|
|
this.right.rightRotation();
|
|
}
|
|
|
|
return this.leftRotation();
|
|
};
|
|
|
|
|
|
/**
|
|
* Rebalance the tree along the given path. The path is given reversed (as he was calculated
|
|
* in the insert and delete functions).
|
|
* Returns the new root of the tree
|
|
* Of course, the first element of the path must be the root of the tree
|
|
*/
|
|
_AVLTree.prototype.rebalanceAlongPath = function (path) {
|
|
var newRoot = this
|
|
, rotated
|
|
, i;
|
|
|
|
if (!this.hasOwnProperty('key')) { delete this.height; return this; } // Empty tree
|
|
|
|
// Rebalance the tree and update all heights
|
|
for (i = path.length - 1; i >= 0; i -= 1) {
|
|
path[i].height = 1 + Math.max(path[i].left ? path[i].left.height : 0, path[i].right ? path[i].right.height : 0);
|
|
|
|
if (path[i].balanceFactor() > 1) {
|
|
rotated = path[i].rightTooSmall();
|
|
if (i === 0) { newRoot = rotated; }
|
|
}
|
|
|
|
if (path[i].balanceFactor() < -1) {
|
|
rotated = path[i].leftTooSmall();
|
|
if (i === 0) { newRoot = rotated; }
|
|
}
|
|
}
|
|
|
|
return newRoot;
|
|
};
|
|
|
|
|
|
/**
|
|
* Insert a key, value pair in the tree while maintaining the AVL tree height constraint
|
|
* Return a pointer to the root node, which may have changed
|
|
*/
|
|
_AVLTree.prototype.insert = function (key, value) {
|
|
var insertPath = []
|
|
, currentNode = this
|
|
;
|
|
|
|
// Empty tree, insert as root
|
|
if (!this.hasOwnProperty('key')) {
|
|
this.key = key;
|
|
this.data.push(value);
|
|
this.height = 1;
|
|
return this;
|
|
}
|
|
|
|
// Insert new leaf at the right place
|
|
while (true) {
|
|
// Same key: no change in the tree structure
|
|
if (currentNode.compareKeys(currentNode.key, key) === 0) {
|
|
if (currentNode.unique) {
|
|
var err = new Error("Can't insert key " + key + ", it violates the unique constraint");
|
|
err.key = key;
|
|
err.errorType = 'uniqueViolated';
|
|
throw err;
|
|
} else {
|
|
currentNode.data.push(value);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
insertPath.push(currentNode);
|
|
|
|
if (currentNode.compareKeys(key, currentNode.key) < 0) {
|
|
if (!currentNode.left) {
|
|
insertPath.push(currentNode.createLeftChild({ key: key, value: value }));
|
|
break;
|
|
} else {
|
|
currentNode = currentNode.left;
|
|
}
|
|
} else {
|
|
if (!currentNode.right) {
|
|
insertPath.push(currentNode.createRightChild({ key: key, value: value }));
|
|
break;
|
|
} else {
|
|
currentNode = currentNode.right;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.rebalanceAlongPath(insertPath);
|
|
};
|
|
|
|
// Insert in the internal tree, update the pointer to the root if needed
|
|
AVLTree.prototype.insert = function (key, value) {
|
|
var newTree = this.tree.insert(key, value);
|
|
|
|
// If newTree is undefined, that means its structure was not modified
|
|
if (newTree) { this.tree = newTree; }
|
|
};
|
|
|
|
|
|
/**
|
|
* Delete a key or just a value and return the new root of the tree
|
|
* @param {Key} key
|
|
* @param {Value} value Optional. If not set, the whole key is deleted. If set, only this value is deleted
|
|
*/
|
|
_AVLTree.prototype.delete = function (key, value) {
|
|
var newData = [], replaceWith
|
|
, self = this
|
|
, currentNode = this
|
|
, deletePath = []
|
|
;
|
|
|
|
if (!this.hasOwnProperty('key')) { return this; } // Empty tree
|
|
|
|
// Either no match is found and the function will return from within the loop
|
|
// Or a match is found and deletePath will contain the path from the root to the node to delete after the loop
|
|
while (true) {
|
|
if (currentNode.compareKeys(key, currentNode.key) === 0) { break; }
|
|
|
|
deletePath.push(currentNode);
|
|
|
|
if (currentNode.compareKeys(key, currentNode.key) < 0) {
|
|
if (currentNode.left) {
|
|
currentNode = currentNode.left;
|
|
} else {
|
|
return this; // Key not found, no modification
|
|
}
|
|
} else {
|
|
// currentNode.compareKeys(key, currentNode.key) is > 0
|
|
if (currentNode.right) {
|
|
currentNode = currentNode.right;
|
|
} else {
|
|
return this; // Key not found, no modification
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete only a value (no tree modification)
|
|
if (currentNode.data.length > 1 && value) {
|
|
currentNode.data.forEach(function (d) {
|
|
if (!currentNode.checkValueEquality(d, value)) { newData.push(d); }
|
|
});
|
|
currentNode.data = newData;
|
|
return this;
|
|
}
|
|
|
|
// Delete a whole node
|
|
|
|
// Leaf
|
|
if (!currentNode.left && !currentNode.right) {
|
|
if (currentNode === this) { // This leaf is also the root
|
|
delete currentNode.key;
|
|
currentNode.data = [];
|
|
delete currentNode.height;
|
|
return this;
|
|
} else {
|
|
if (currentNode.parent.left === currentNode) {
|
|
currentNode.parent.left = null;
|
|
} else {
|
|
currentNode.parent.right = null;
|
|
}
|
|
return this.rebalanceAlongPath(deletePath);
|
|
}
|
|
}
|
|
|
|
|
|
// Node with only one child
|
|
if (!currentNode.left || !currentNode.right) {
|
|
replaceWith = currentNode.left ? currentNode.left : currentNode.right;
|
|
|
|
if (currentNode === this) { // This node is also the root
|
|
replaceWith.parent = null;
|
|
return replaceWith; // height of replaceWith is necessarily 1 because the tree was balanced before deletion
|
|
} else {
|
|
if (currentNode.parent.left === currentNode) {
|
|
currentNode.parent.left = replaceWith;
|
|
replaceWith.parent = currentNode.parent;
|
|
} else {
|
|
currentNode.parent.right = replaceWith;
|
|
replaceWith.parent = currentNode.parent;
|
|
}
|
|
|
|
return this.rebalanceAlongPath(deletePath);
|
|
}
|
|
}
|
|
|
|
|
|
// Node with two children
|
|
// Use the in-order predecessor (no need to randomize since we actively rebalance)
|
|
deletePath.push(currentNode);
|
|
replaceWith = currentNode.left;
|
|
|
|
// Special case: the in-order predecessor is right below the node to delete
|
|
if (!replaceWith.right) {
|
|
currentNode.key = replaceWith.key;
|
|
currentNode.data = replaceWith.data;
|
|
currentNode.left = replaceWith.left;
|
|
if (replaceWith.left) { replaceWith.left.parent = currentNode; }
|
|
return this.rebalanceAlongPath(deletePath);
|
|
}
|
|
|
|
// After this loop, replaceWith is the right-most leaf in the left subtree
|
|
// and deletePath the path from the root (inclusive) to replaceWith (exclusive)
|
|
while (true) {
|
|
if (replaceWith.right) {
|
|
deletePath.push(replaceWith);
|
|
replaceWith = replaceWith.right;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
currentNode.key = replaceWith.key;
|
|
currentNode.data = replaceWith.data;
|
|
|
|
replaceWith.parent.right = replaceWith.left;
|
|
if (replaceWith.left) { replaceWith.left.parent = replaceWith.parent; }
|
|
|
|
return this.rebalanceAlongPath(deletePath);
|
|
};
|
|
|
|
// Delete a value
|
|
AVLTree.prototype.delete = function (key, value) {
|
|
var newTree = this.tree.delete(key, value);
|
|
|
|
// If newTree is undefined, that means its structure was not modified
|
|
if (newTree) { this.tree = newTree; }
|
|
};
|
|
|
|
|
|
/**
|
|
* Other functions we want to use on an AVLTree as if it were the internal _AVLTree
|
|
*/
|
|
['getNumberOfKeys', 'search', 'betweenBounds', 'prettyPrint', 'executeOnEveryNode'].forEach(function (fn) {
|
|
AVLTree.prototype[fn] = function () {
|
|
return this.tree[fn].apply(this.tree, arguments);
|
|
};
|
|
});
|
|
|
|
|
|
// Interface
|
|
module.exports = AVLTree;
|
|
|
|
},{"./bst":16,"./customUtils":17,"underscore":19,"util":3}],16:[function(require,module,exports){
|
|
/**
|
|
* Simple binary search tree
|
|
*/
|
|
var customUtils = require('./customUtils');
|
|
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {Object} options Optional
|
|
* @param {Boolean} options.unique Whether to enforce a 'unique' constraint on the key or not
|
|
* @param {Key} options.key Initialize this BST's key with key
|
|
* @param {Value} options.value Initialize this BST's data with [value]
|
|
* @param {Function} options.compareKeys Initialize this BST's compareKeys
|
|
*/
|
|
function BinarySearchTree (options) {
|
|
options = options || {};
|
|
|
|
this.left = null;
|
|
this.right = null;
|
|
this.parent = options.parent !== undefined ? options.parent : null;
|
|
if (options.hasOwnProperty('key')) { this.key = options.key; }
|
|
this.data = options.hasOwnProperty('value') ? [options.value] : [];
|
|
this.unique = options.unique || false;
|
|
|
|
this.compareKeys = options.compareKeys || customUtils.defaultCompareKeysFunction;
|
|
this.checkValueEquality = options.checkValueEquality || customUtils.defaultCheckValueEquality;
|
|
}
|
|
|
|
|
|
// ================================
|
|
// Methods used to test the tree
|
|
// ================================
|
|
|
|
|
|
/**
|
|
* Get the descendant with max key
|
|
*/
|
|
BinarySearchTree.prototype.getMaxKeyDescendant = function () {
|
|
if (this.right) {
|
|
return this.right.getMaxKeyDescendant();
|
|
} else {
|
|
return this;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Get the maximum key
|
|
*/
|
|
BinarySearchTree.prototype.getMaxKey = function () {
|
|
return this.getMaxKeyDescendant().key;
|
|
};
|
|
|
|
|
|
/**
|
|
* Get the descendant with min key
|
|
*/
|
|
BinarySearchTree.prototype.getMinKeyDescendant = function () {
|
|
if (this.left) {
|
|
return this.left.getMinKeyDescendant()
|
|
} else {
|
|
return this;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Get the minimum key
|
|
*/
|
|
BinarySearchTree.prototype.getMinKey = function () {
|
|
return this.getMinKeyDescendant().key;
|
|
};
|
|
|
|
|
|
/**
|
|
* Check that all nodes (incl. leaves) fullfil condition given by fn
|
|
* test is a function passed every (key, data) and which throws if the condition is not met
|
|
*/
|
|
BinarySearchTree.prototype.checkAllNodesFullfillCondition = function (test) {
|
|
if (!this.hasOwnProperty('key')) { return; }
|
|
|
|
test(this.key, this.data);
|
|
if (this.left) { this.left.checkAllNodesFullfillCondition(test); }
|
|
if (this.right) { this.right.checkAllNodesFullfillCondition(test); }
|
|
};
|
|
|
|
|
|
/**
|
|
* Check that the core BST properties on node ordering are verified
|
|
* Throw if they aren't
|
|
*/
|
|
BinarySearchTree.prototype.checkNodeOrdering = function () {
|
|
var self = this;
|
|
|
|
if (!this.hasOwnProperty('key')) { return; }
|
|
|
|
if (this.left) {
|
|
this.left.checkAllNodesFullfillCondition(function (k) {
|
|
if (self.compareKeys(k, self.key) >= 0) {
|
|
throw new Error('Tree with root ' + self.key + ' is not a binary search tree');
|
|
}
|
|
});
|
|
this.left.checkNodeOrdering();
|
|
}
|
|
|
|
if (this.right) {
|
|
this.right.checkAllNodesFullfillCondition(function (k) {
|
|
if (self.compareKeys(k, self.key) <= 0) {
|
|
throw new Error('Tree with root ' + self.key + ' is not a binary search tree');
|
|
}
|
|
});
|
|
this.right.checkNodeOrdering();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Check that all pointers are coherent in this tree
|
|
*/
|
|
BinarySearchTree.prototype.checkInternalPointers = function () {
|
|
if (this.left) {
|
|
if (this.left.parent !== this) { throw new Error('Parent pointer broken for key ' + this.key); }
|
|
this.left.checkInternalPointers();
|
|
}
|
|
|
|
if (this.right) {
|
|
if (this.right.parent !== this) { throw new Error('Parent pointer broken for key ' + this.key); }
|
|
this.right.checkInternalPointers();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Check that a tree is a BST as defined here (node ordering and pointer references)
|
|
*/
|
|
BinarySearchTree.prototype.checkIsBST = function () {
|
|
this.checkNodeOrdering();
|
|
this.checkInternalPointers();
|
|
if (this.parent) { throw new Error("The root shouldn't have a parent"); }
|
|
};
|
|
|
|
|
|
/**
|
|
* Get number of keys inserted
|
|
*/
|
|
BinarySearchTree.prototype.getNumberOfKeys = function () {
|
|
var res;
|
|
|
|
if (!this.hasOwnProperty('key')) { return 0; }
|
|
|
|
res = 1;
|
|
if (this.left) { res += this.left.getNumberOfKeys(); }
|
|
if (this.right) { res += this.right.getNumberOfKeys(); }
|
|
|
|
return res;
|
|
};
|
|
|
|
|
|
|
|
// ============================================
|
|
// Methods used to actually work on the tree
|
|
// ============================================
|
|
|
|
/**
|
|
* Create a BST similar (i.e. same options except for key and value) to the current one
|
|
* Use the same constructor (i.e. BinarySearchTree, AVLTree etc)
|
|
* @param {Object} options see constructor
|
|
*/
|
|
BinarySearchTree.prototype.createSimilar = function (options) {
|
|
options = options || {};
|
|
options.unique = this.unique;
|
|
options.compareKeys = this.compareKeys;
|
|
options.checkValueEquality = this.checkValueEquality;
|
|
|
|
return new this.constructor(options);
|
|
};
|
|
|
|
|
|
/**
|
|
* Create the left child of this BST and return it
|
|
*/
|
|
BinarySearchTree.prototype.createLeftChild = function (options) {
|
|
var leftChild = this.createSimilar(options);
|
|
leftChild.parent = this;
|
|
this.left = leftChild;
|
|
|
|
return leftChild;
|
|
};
|
|
|
|
|
|
/**
|
|
* Create the right child of this BST and return it
|
|
*/
|
|
BinarySearchTree.prototype.createRightChild = function (options) {
|
|
var rightChild = this.createSimilar(options);
|
|
rightChild.parent = this;
|
|
this.right = rightChild;
|
|
|
|
return rightChild;
|
|
};
|
|
|
|
|
|
/**
|
|
* Insert a new element
|
|
*/
|
|
BinarySearchTree.prototype.insert = function (key, value) {
|
|
// Empty tree, insert as root
|
|
if (!this.hasOwnProperty('key')) {
|
|
this.key = key;
|
|
this.data.push(value);
|
|
return;
|
|
}
|
|
|
|
// Same key as root
|
|
if (this.compareKeys(this.key, key) === 0) {
|
|
if (this.unique) {
|
|
var err = new Error("Can't insert key " + key + ", it violates the unique constraint");
|
|
err.key = key;
|
|
err.errorType = 'uniqueViolated';
|
|
throw err;
|
|
} else {
|
|
this.data.push(value);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.compareKeys(key, this.key) < 0) {
|
|
// Insert in left subtree
|
|
if (this.left) {
|
|
this.left.insert(key, value);
|
|
} else {
|
|
this.createLeftChild({ key: key, value: value });
|
|
}
|
|
} else {
|
|
// Insert in right subtree
|
|
if (this.right) {
|
|
this.right.insert(key, value);
|
|
} else {
|
|
this.createRightChild({ key: key, value: value });
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Search for all data corresponding to a key
|
|
*/
|
|
BinarySearchTree.prototype.search = function (key) {
|
|
if (!this.hasOwnProperty('key')) { return []; }
|
|
|
|
if (this.compareKeys(this.key, key) === 0) { return this.data; }
|
|
|
|
if (this.compareKeys(key, this.key) < 0) {
|
|
if (this.left) {
|
|
return this.left.search(key);
|
|
} else {
|
|
return [];
|
|
}
|
|
} else {
|
|
if (this.right) {
|
|
return this.right.search(key);
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Return a function that tells whether a given key matches a lower bound
|
|
*/
|
|
BinarySearchTree.prototype.getLowerBoundMatcher = function (query) {
|
|
var self = this;
|
|
|
|
// No lower bound
|
|
if (!query.hasOwnProperty('$gt') && !query.hasOwnProperty('$gte')) {
|
|
return function () { return true; };
|
|
}
|
|
|
|
if (query.hasOwnProperty('$gt') && query.hasOwnProperty('$gte')) {
|
|
if (self.compareKeys(query.$gte, query.$gt) === 0) {
|
|
return function (key) { return self.compareKeys(key, query.$gt) > 0; };
|
|
}
|
|
|
|
if (self.compareKeys(query.$gte, query.$gt) > 0) {
|
|
return function (key) { return self.compareKeys(key, query.$gte) >= 0; };
|
|
} else {
|
|
return function (key) { return self.compareKeys(key, query.$gt) > 0; };
|
|
}
|
|
}
|
|
|
|
if (query.hasOwnProperty('$gt')) {
|
|
return function (key) { return self.compareKeys(key, query.$gt) > 0; };
|
|
} else {
|
|
return function (key) { return self.compareKeys(key, query.$gte) >= 0; };
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Return a function that tells whether a given key matches an upper bound
|
|
*/
|
|
BinarySearchTree.prototype.getUpperBoundMatcher = function (query) {
|
|
var self = this;
|
|
|
|
// No lower bound
|
|
if (!query.hasOwnProperty('$lt') && !query.hasOwnProperty('$lte')) {
|
|
return function () { return true; };
|
|
}
|
|
|
|
if (query.hasOwnProperty('$lt') && query.hasOwnProperty('$lte')) {
|
|
if (self.compareKeys(query.$lte, query.$lt) === 0) {
|
|
return function (key) { return self.compareKeys(key, query.$lt) < 0; };
|
|
}
|
|
|
|
if (self.compareKeys(query.$lte, query.$lt) < 0) {
|
|
return function (key) { return self.compareKeys(key, query.$lte) <= 0; };
|
|
} else {
|
|
return function (key) { return self.compareKeys(key, query.$lt) < 0; };
|
|
}
|
|
}
|
|
|
|
if (query.hasOwnProperty('$lt')) {
|
|
return function (key) { return self.compareKeys(key, query.$lt) < 0; };
|
|
} else {
|
|
return function (key) { return self.compareKeys(key, query.$lte) <= 0; };
|
|
}
|
|
};
|
|
|
|
|
|
// Append all elements in toAppend to array
|
|
function append (array, toAppend) {
|
|
var i;
|
|
|
|
for (i = 0; i < toAppend.length; i += 1) {
|
|
array.push(toAppend[i]);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get all data for a key between bounds
|
|
* Return it in key order
|
|
* @param {Object} query Mongo-style query where keys are $lt, $lte, $gt or $gte (other keys are not considered)
|
|
* @param {Functions} lbm/ubm matching functions calculated at the first recursive step
|
|
*/
|
|
BinarySearchTree.prototype.betweenBounds = function (query, lbm, ubm) {
|
|
var res = [];
|
|
|
|
if (!this.hasOwnProperty('key')) { return []; } // Empty tree
|
|
|
|
lbm = lbm || this.getLowerBoundMatcher(query);
|
|
ubm = ubm || this.getUpperBoundMatcher(query);
|
|
|
|
if (lbm(this.key) && this.left) { append(res, this.left.betweenBounds(query, lbm, ubm)); }
|
|
if (lbm(this.key) && ubm(this.key)) { append(res, this.data); }
|
|
if (ubm(this.key) && this.right) { append(res, this.right.betweenBounds(query, lbm, ubm)); }
|
|
|
|
return res;
|
|
};
|
|
|
|
|
|
/**
|
|
* Delete the current node if it is a leaf
|
|
* Return true if it was deleted
|
|
*/
|
|
BinarySearchTree.prototype.deleteIfLeaf = function () {
|
|
if (this.left || this.right) { return false; }
|
|
|
|
// The leaf is itself a root
|
|
if (!this.parent) {
|
|
delete this.key;
|
|
this.data = [];
|
|
return true;
|
|
}
|
|
|
|
if (this.parent.left === this) {
|
|
this.parent.left = null;
|
|
} else {
|
|
this.parent.right = null;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Delete the current node if it has only one child
|
|
* Return true if it was deleted
|
|
*/
|
|
BinarySearchTree.prototype.deleteIfOnlyOneChild = function () {
|
|
var child;
|
|
|
|
if (this.left && !this.right) { child = this.left; }
|
|
if (!this.left && this.right) { child = this.right; }
|
|
if (!child) { return false; }
|
|
|
|
// Root
|
|
if (!this.parent) {
|
|
this.key = child.key;
|
|
this.data = child.data;
|
|
|
|
this.left = null;
|
|
if (child.left) {
|
|
this.left = child.left;
|
|
child.left.parent = this;
|
|
}
|
|
|
|
this.right = null;
|
|
if (child.right) {
|
|
this.right = child.right;
|
|
child.right.parent = this;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (this.parent.left === this) {
|
|
this.parent.left = child;
|
|
child.parent = this.parent;
|
|
} else {
|
|
this.parent.right = child;
|
|
child.parent = this.parent;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Delete a key or just a value
|
|
* @param {Key} key
|
|
* @param {Value} value Optional. If not set, the whole key is deleted. If set, only this value is deleted
|
|
*/
|
|
BinarySearchTree.prototype.delete = function (key, value) {
|
|
var newData = [], replaceWith
|
|
, self = this
|
|
;
|
|
|
|
if (!this.hasOwnProperty('key')) { return; }
|
|
|
|
if (this.compareKeys(key, this.key) < 0) {
|
|
if (this.left) { this.left.delete(key, value); }
|
|
return;
|
|
}
|
|
|
|
if (this.compareKeys(key, this.key) > 0) {
|
|
if (this.right) { this.right.delete(key, value); }
|
|
return;
|
|
}
|
|
|
|
if (!this.compareKeys(key, this.key) === 0) { return; }
|
|
|
|
// Delete only a value
|
|
if (this.data.length > 1 && value !== undefined) {
|
|
this.data.forEach(function (d) {
|
|
if (!self.checkValueEquality(d, value)) { newData.push(d); }
|
|
});
|
|
self.data = newData;
|
|
return;
|
|
}
|
|
|
|
// Delete the whole node
|
|
if (this.deleteIfLeaf()) {
|
|
return;
|
|
}
|
|
if (this.deleteIfOnlyOneChild()) {
|
|
return;
|
|
}
|
|
|
|
// We are in the case where the node to delete has two children
|
|
if (Math.random() >= 0.5) { // Randomize replacement to avoid unbalancing the tree too much
|
|
// Use the in-order predecessor
|
|
replaceWith = this.left.getMaxKeyDescendant();
|
|
|
|
this.key = replaceWith.key;
|
|
this.data = replaceWith.data;
|
|
|
|
if (this === replaceWith.parent) { // Special case
|
|
this.left = replaceWith.left;
|
|
if (replaceWith.left) { replaceWith.left.parent = replaceWith.parent; }
|
|
} else {
|
|
replaceWith.parent.right = replaceWith.left;
|
|
if (replaceWith.left) { replaceWith.left.parent = replaceWith.parent; }
|
|
}
|
|
} else {
|
|
// Use the in-order successor
|
|
replaceWith = this.right.getMinKeyDescendant();
|
|
|
|
this.key = replaceWith.key;
|
|
this.data = replaceWith.data;
|
|
|
|
if (this === replaceWith.parent) { // Special case
|
|
this.right = replaceWith.right;
|
|
if (replaceWith.right) { replaceWith.right.parent = replaceWith.parent; }
|
|
} else {
|
|
replaceWith.parent.left = replaceWith.right;
|
|
if (replaceWith.right) { replaceWith.right.parent = replaceWith.parent; }
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Execute a function on every node of the tree, in key order
|
|
* @param {Function} fn Signature: node. Most useful will probably be node.key and node.data
|
|
*/
|
|
BinarySearchTree.prototype.executeOnEveryNode = function (fn) {
|
|
if (this.left) { this.left.executeOnEveryNode(fn); }
|
|
fn(this);
|
|
if (this.right) { this.right.executeOnEveryNode(fn); }
|
|
};
|
|
|
|
|
|
/**
|
|
* Pretty print a tree
|
|
* @param {Boolean} printData To print the nodes' data along with the key
|
|
*/
|
|
BinarySearchTree.prototype.prettyPrint = function (printData, spacing) {
|
|
spacing = spacing || "";
|
|
|
|
console.log(spacing + "* " + this.key);
|
|
if (printData) { console.log(spacing + "* " + this.data); }
|
|
|
|
if (!this.left && !this.right) { return; }
|
|
|
|
if (this.left) {
|
|
this.left.prettyPrint(printData, spacing + " ");
|
|
} else {
|
|
console.log(spacing + " *");
|
|
}
|
|
if (this.right) {
|
|
this.right.prettyPrint(printData, spacing + " ");
|
|
} else {
|
|
console.log(spacing + " *");
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
// Interface
|
|
module.exports = BinarySearchTree;
|
|
|
|
},{"./customUtils":17}],17:[function(require,module,exports){
|
|
/**
|
|
* Return an array with the numbers from 0 to n-1, in a random order
|
|
*/
|
|
function getRandomArray (n) {
|
|
var res, next;
|
|
|
|
if (n === 0) { return []; }
|
|
if (n === 1) { return [0]; }
|
|
|
|
res = getRandomArray(n - 1);
|
|
next = Math.floor(Math.random() * n);
|
|
res.splice(next, 0, n - 1); // Add n-1 at a random position in the array
|
|
|
|
return res;
|
|
};
|
|
module.exports.getRandomArray = getRandomArray;
|
|
|
|
|
|
/*
|
|
* Default compareKeys function will work for numbers, strings and dates
|
|
*/
|
|
function defaultCompareKeysFunction (a, b) {
|
|
if (a < b) { return -1; }
|
|
if (a > b) { return 1; }
|
|
if (a === b) { return 0; }
|
|
|
|
var err = new Error("Couldn't compare elements");
|
|
err.a = a;
|
|
err.b = b;
|
|
throw err;
|
|
}
|
|
module.exports.defaultCompareKeysFunction = defaultCompareKeysFunction;
|
|
|
|
|
|
/**
|
|
* Check whether two values are equal (used in non-unique deletion)
|
|
*/
|
|
function defaultCheckValueEquality (a, b) {
|
|
return a === b;
|
|
}
|
|
module.exports.defaultCheckValueEquality = defaultCheckValueEquality;
|
|
|
|
},{}],18:[function(require,module,exports){
|
|
var process=require("__browserify_process"),global=self;/*!
|
|
localForage -- Offline Storage, Improved
|
|
Version 1.3.0
|
|
https://mozilla.github.io/localForage
|
|
(c) 2013-2015 Mozilla, Apache License 2.0
|
|
*/
|
|
(function() {
|
|
var define, requireModule, require, requirejs;
|
|
|
|
(function() {
|
|
var registry = {}, seen = {};
|
|
|
|
define = function(name, deps, callback) {
|
|
registry[name] = { deps: deps, callback: callback };
|
|
};
|
|
|
|
requirejs = require = requireModule = function(name) {
|
|
requirejs._eak_seen = registry;
|
|
|
|
if (seen[name]) { return seen[name]; }
|
|
seen[name] = {};
|
|
|
|
if (!registry[name]) {
|
|
throw new Error("Could not find module " + name);
|
|
}
|
|
|
|
var mod = registry[name],
|
|
deps = mod.deps,
|
|
callback = mod.callback,
|
|
reified = [],
|
|
exports;
|
|
|
|
for (var i=0, l=deps.length; i<l; i++) {
|
|
if (deps[i] === 'exports') {
|
|
reified.push(exports = {});
|
|
} else {
|
|
reified.push(requireModule(resolve(deps[i])));
|
|
}
|
|
}
|
|
|
|
var value = callback.apply(this, reified);
|
|
return seen[name] = exports || value;
|
|
|
|
function resolve(child) {
|
|
if (child.charAt(0) !== '.') { return child; }
|
|
var parts = child.split("/");
|
|
var parentBase = name.split("/").slice(0, -1);
|
|
|
|
for (var i=0, l=parts.length; i<l; i++) {
|
|
var part = parts[i];
|
|
|
|
if (part === '..') { parentBase.pop(); }
|
|
else if (part === '.') { continue; }
|
|
else { parentBase.push(part); }
|
|
}
|
|
|
|
return parentBase.join("/");
|
|
}
|
|
};
|
|
})();
|
|
|
|
define("promise/all",
|
|
["./utils","exports"],
|
|
function(__dependency1__, __exports__) {
|
|
"use strict";
|
|
/* global toString */
|
|
|
|
var isArray = __dependency1__.isArray;
|
|
var isFunction = __dependency1__.isFunction;
|
|
|
|
/**
|
|
Returns a promise that is fulfilled when all the given promises have been
|
|
fulfilled, or rejected if any of them become rejected. The return promise
|
|
is fulfilled with an array that gives all the values in the order they were
|
|
passed in the `promises` array argument.
|
|
|
|
Example:
|
|
|
|
```javascript
|
|
var promise1 = RSVP.resolve(1);
|
|
var promise2 = RSVP.resolve(2);
|
|
var promise3 = RSVP.resolve(3);
|
|
var promises = [ promise1, promise2, promise3 ];
|
|
|
|
RSVP.all(promises).then(function(array){
|
|
// The array here would be [ 1, 2, 3 ];
|
|
});
|
|
```
|
|
|
|
If any of the `promises` given to `RSVP.all` are rejected, the first promise
|
|
that is rejected will be given as an argument to the returned promises's
|
|
rejection handler. For example:
|
|
|
|
Example:
|
|
|
|
```javascript
|
|
var promise1 = RSVP.resolve(1);
|
|
var promise2 = RSVP.reject(new Error("2"));
|
|
var promise3 = RSVP.reject(new Error("3"));
|
|
var promises = [ promise1, promise2, promise3 ];
|
|
|
|
RSVP.all(promises).then(function(array){
|
|
// Code here never runs because there are rejected promises!
|
|
}, function(error) {
|
|
// error.message === "2"
|
|
});
|
|
```
|
|
|
|
@method all
|
|
@for RSVP
|
|
@param {Array} promises
|
|
@param {String} label
|
|
@return {Promise} promise that is fulfilled when all `promises` have been
|
|
fulfilled, or rejected if any of them become rejected.
|
|
*/
|
|
function all(promises) {
|
|
/*jshint validthis:true */
|
|
var Promise = this;
|
|
|
|
if (!isArray(promises)) {
|
|
throw new TypeError('You must pass an array to all.');
|
|
}
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
var results = [], remaining = promises.length,
|
|
promise;
|
|
|
|
if (remaining === 0) {
|
|
resolve([]);
|
|
}
|
|
|
|
function resolver(index) {
|
|
return function(value) {
|
|
resolveAll(index, value);
|
|
};
|
|
}
|
|
|
|
function resolveAll(index, value) {
|
|
results[index] = value;
|
|
if (--remaining === 0) {
|
|
resolve(results);
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < promises.length; i++) {
|
|
promise = promises[i];
|
|
|
|
if (promise && isFunction(promise.then)) {
|
|
promise.then(resolver(i), reject);
|
|
} else {
|
|
resolveAll(i, promise);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
__exports__.all = all;
|
|
});
|
|
define("promise/asap",
|
|
["exports"],
|
|
function(__exports__) {
|
|
"use strict";
|
|
var browserGlobal = (typeof window !== 'undefined') ? window : {};
|
|
var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
|
|
var local = (typeof global !== 'undefined') ? global : (this === undefined? window:this);
|
|
|
|
// node
|
|
function useNextTick() {
|
|
return function() {
|
|
process.nextTick(flush);
|
|
};
|
|
}
|
|
|
|
function useMutationObserver() {
|
|
var iterations = 0;
|
|
var observer = new BrowserMutationObserver(flush);
|
|
var node = document.createTextNode('');
|
|
observer.observe(node, { characterData: true });
|
|
|
|
return function() {
|
|
node.data = (iterations = ++iterations % 2);
|
|
};
|
|
}
|
|
|
|
function useSetTimeout() {
|
|
return function() {
|
|
local.setTimeout(flush, 1);
|
|
};
|
|
}
|
|
|
|
var queue = [];
|
|
function flush() {
|
|
for (var i = 0; i < queue.length; i++) {
|
|
var tuple = queue[i];
|
|
var callback = tuple[0], arg = tuple[1];
|
|
callback(arg);
|
|
}
|
|
queue = [];
|
|
}
|
|
|
|
var scheduleFlush;
|
|
|
|
// Decide what async method to use to triggering processing of queued callbacks:
|
|
if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') {
|
|
scheduleFlush = useNextTick();
|
|
} else if (BrowserMutationObserver) {
|
|
scheduleFlush = useMutationObserver();
|
|
} else {
|
|
scheduleFlush = useSetTimeout();
|
|
}
|
|
|
|
function asap(callback, arg) {
|
|
var length = queue.push([callback, arg]);
|
|
if (length === 1) {
|
|
// If length is 1, that means that we need to schedule an async flush.
|
|
// If additional callbacks are queued before the queue is flushed, they
|
|
// will be processed by this flush that we are scheduling.
|
|
scheduleFlush();
|
|
}
|
|
}
|
|
|
|
__exports__.asap = asap;
|
|
});
|
|
define("promise/config",
|
|
["exports"],
|
|
function(__exports__) {
|
|
"use strict";
|
|
var config = {
|
|
instrument: false
|
|
};
|
|
|
|
function configure(name, value) {
|
|
if (arguments.length === 2) {
|
|
config[name] = value;
|
|
} else {
|
|
return config[name];
|
|
}
|
|
}
|
|
|
|
__exports__.config = config;
|
|
__exports__.configure = configure;
|
|
});
|
|
define("promise/polyfill",
|
|
["./promise","./utils","exports"],
|
|
function(__dependency1__, __dependency2__, __exports__) {
|
|
"use strict";
|
|
/*global self*/
|
|
var RSVPPromise = __dependency1__.Promise;
|
|
var isFunction = __dependency2__.isFunction;
|
|
|
|
function polyfill() {
|
|
var local;
|
|
|
|
if (typeof global !== 'undefined') {
|
|
local = global;
|
|
} else if (typeof window !== 'undefined' && window.document) {
|
|
local = window;
|
|
} else {
|
|
local = self;
|
|
}
|
|
|
|
var es6PromiseSupport =
|
|
"Promise" in local &&
|
|
// Some of these methods are missing from
|
|
// Firefox/Chrome experimental implementations
|
|
"resolve" in local.Promise &&
|
|
"reject" in local.Promise &&
|
|
"all" in local.Promise &&
|
|
"race" in local.Promise &&
|
|
// Older version of the spec had a resolver object
|
|
// as the arg rather than a function
|
|
(function() {
|
|
var resolve;
|
|
new local.Promise(function(r) { resolve = r; });
|
|
return isFunction(resolve);
|
|
}());
|
|
|
|
if (!es6PromiseSupport) {
|
|
local.Promise = RSVPPromise;
|
|
}
|
|
}
|
|
|
|
__exports__.polyfill = polyfill;
|
|
});
|
|
define("promise/promise",
|
|
["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],
|
|
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __exports__) {
|
|
"use strict";
|
|
var config = __dependency1__.config;
|
|
var configure = __dependency1__.configure;
|
|
var objectOrFunction = __dependency2__.objectOrFunction;
|
|
var isFunction = __dependency2__.isFunction;
|
|
var now = __dependency2__.now;
|
|
var all = __dependency3__.all;
|
|
var race = __dependency4__.race;
|
|
var staticResolve = __dependency5__.resolve;
|
|
var staticReject = __dependency6__.reject;
|
|
var asap = __dependency7__.asap;
|
|
|
|
var counter = 0;
|
|
|
|
config.async = asap; // default async is asap;
|
|
|
|
function Promise(resolver) {
|
|
if (!isFunction(resolver)) {
|
|
throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
|
|
}
|
|
|
|
if (!(this instanceof Promise)) {
|
|
throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
|
|
}
|
|
|
|
this._subscribers = [];
|
|
|
|
invokeResolver(resolver, this);
|
|
}
|
|
|
|
function invokeResolver(resolver, promise) {
|
|
function resolvePromise(value) {
|
|
resolve(promise, value);
|
|
}
|
|
|
|
function rejectPromise(reason) {
|
|
reject(promise, reason);
|
|
}
|
|
|
|
try {
|
|
resolver(resolvePromise, rejectPromise);
|
|
} catch(e) {
|
|
rejectPromise(e);
|
|
}
|
|
}
|
|
|
|
function invokeCallback(settled, promise, callback, detail) {
|
|
var hasCallback = isFunction(callback),
|
|
value, error, succeeded, failed;
|
|
|
|
if (hasCallback) {
|
|
try {
|
|
value = callback(detail);
|
|
succeeded = true;
|
|
} catch(e) {
|
|
failed = true;
|
|
error = e;
|
|
}
|
|
} else {
|
|
value = detail;
|
|
succeeded = true;
|
|
}
|
|
|
|
if (handleThenable(promise, value)) {
|
|
return;
|
|
} else if (hasCallback && succeeded) {
|
|
resolve(promise, value);
|
|
} else if (failed) {
|
|
reject(promise, error);
|
|
} else if (settled === FULFILLED) {
|
|
resolve(promise, value);
|
|
} else if (settled === REJECTED) {
|
|
reject(promise, value);
|
|
}
|
|
}
|
|
|
|
var PENDING = void 0;
|
|
var SEALED = 0;
|
|
var FULFILLED = 1;
|
|
var REJECTED = 2;
|
|
|
|
function subscribe(parent, child, onFulfillment, onRejection) {
|
|
var subscribers = parent._subscribers;
|
|
var length = subscribers.length;
|
|
|
|
subscribers[length] = child;
|
|
subscribers[length + FULFILLED] = onFulfillment;
|
|
subscribers[length + REJECTED] = onRejection;
|
|
}
|
|
|
|
function publish(promise, settled) {
|
|
var child, callback, subscribers = promise._subscribers, detail = promise._detail;
|
|
|
|
for (var i = 0; i < subscribers.length; i += 3) {
|
|
child = subscribers[i];
|
|
callback = subscribers[i + settled];
|
|
|
|
invokeCallback(settled, child, callback, detail);
|
|
}
|
|
|
|
promise._subscribers = null;
|
|
}
|
|
|
|
Promise.prototype = {
|
|
constructor: Promise,
|
|
|
|
_state: undefined,
|
|
_detail: undefined,
|
|
_subscribers: undefined,
|
|
|
|
then: function(onFulfillment, onRejection) {
|
|
var promise = this;
|
|
|
|
var thenPromise = new this.constructor(function() {});
|
|
|
|
if (this._state) {
|
|
var callbacks = arguments;
|
|
config.async(function invokePromiseCallback() {
|
|
invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail);
|
|
});
|
|
} else {
|
|
subscribe(this, thenPromise, onFulfillment, onRejection);
|
|
}
|
|
|
|
return thenPromise;
|
|
},
|
|
|
|
'catch': function(onRejection) {
|
|
return this.then(null, onRejection);
|
|
}
|
|
};
|
|
|
|
Promise.all = all;
|
|
Promise.race = race;
|
|
Promise.resolve = staticResolve;
|
|
Promise.reject = staticReject;
|
|
|
|
function handleThenable(promise, value) {
|
|
var then = null,
|
|
resolved;
|
|
|
|
try {
|
|
if (promise === value) {
|
|
throw new TypeError("A promises callback cannot return that same promise.");
|
|
}
|
|
|
|
if (objectOrFunction(value)) {
|
|
then = value.then;
|
|
|
|
if (isFunction(then)) {
|
|
then.call(value, function(val) {
|
|
if (resolved) { return true; }
|
|
resolved = true;
|
|
|
|
if (value !== val) {
|
|
resolve(promise, val);
|
|
} else {
|
|
fulfill(promise, val);
|
|
}
|
|
}, function(val) {
|
|
if (resolved) { return true; }
|
|
resolved = true;
|
|
|
|
reject(promise, val);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (resolved) { return true; }
|
|
reject(promise, error);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function resolve(promise, value) {
|
|
if (promise === value) {
|
|
fulfill(promise, value);
|
|
} else if (!handleThenable(promise, value)) {
|
|
fulfill(promise, value);
|
|
}
|
|
}
|
|
|
|
function fulfill(promise, value) {
|
|
if (promise._state !== PENDING) { return; }
|
|
promise._state = SEALED;
|
|
promise._detail = value;
|
|
|
|
config.async(publishFulfillment, promise);
|
|
}
|
|
|
|
function reject(promise, reason) {
|
|
if (promise._state !== PENDING) { return; }
|
|
promise._state = SEALED;
|
|
promise._detail = reason;
|
|
|
|
config.async(publishRejection, promise);
|
|
}
|
|
|
|
function publishFulfillment(promise) {
|
|
publish(promise, promise._state = FULFILLED);
|
|
}
|
|
|
|
function publishRejection(promise) {
|
|
publish(promise, promise._state = REJECTED);
|
|
}
|
|
|
|
__exports__.Promise = Promise;
|
|
});
|
|
define("promise/race",
|
|
["./utils","exports"],
|
|
function(__dependency1__, __exports__) {
|
|
"use strict";
|
|
/* global toString */
|
|
var isArray = __dependency1__.isArray;
|
|
|
|
/**
|
|
`RSVP.race` allows you to watch a series of promises and act as soon as the
|
|
first promise given to the `promises` argument fulfills or rejects.
|
|
|
|
Example:
|
|
|
|
```javascript
|
|
var promise1 = new RSVP.Promise(function(resolve, reject){
|
|
setTimeout(function(){
|
|
resolve("promise 1");
|
|
}, 200);
|
|
});
|
|
|
|
var promise2 = new RSVP.Promise(function(resolve, reject){
|
|
setTimeout(function(){
|
|
resolve("promise 2");
|
|
}, 100);
|
|
});
|
|
|
|
RSVP.race([promise1, promise2]).then(function(result){
|
|
// result === "promise 2" because it was resolved before promise1
|
|
// was resolved.
|
|
});
|
|
```
|
|
|
|
`RSVP.race` is deterministic in that only the state of the first completed
|
|
promise matters. For example, even if other promises given to the `promises`
|
|
array argument are resolved, but the first completed promise has become
|
|
rejected before the other promises became fulfilled, the returned promise
|
|
will become rejected:
|
|
|
|
```javascript
|
|
var promise1 = new RSVP.Promise(function(resolve, reject){
|
|
setTimeout(function(){
|
|
resolve("promise 1");
|
|
}, 200);
|
|
});
|
|
|
|
var promise2 = new RSVP.Promise(function(resolve, reject){
|
|
setTimeout(function(){
|
|
reject(new Error("promise 2"));
|
|
}, 100);
|
|
});
|
|
|
|
RSVP.race([promise1, promise2]).then(function(result){
|
|
// Code here never runs because there are rejected promises!
|
|
}, function(reason){
|
|
// reason.message === "promise2" because promise 2 became rejected before
|
|
// promise 1 became fulfilled
|
|
});
|
|
```
|
|
|
|
@method race
|
|
@for RSVP
|
|
@param {Array} promises array of promises to observe
|
|
@param {String} label optional string for describing the promise returned.
|
|
Useful for tooling.
|
|
@return {Promise} a promise that becomes fulfilled with the value the first
|
|
completed promises is resolved with if the first completed promise was
|
|
fulfilled, or rejected with the reason that the first completed promise
|
|
was rejected with.
|
|
*/
|
|
function race(promises) {
|
|
/*jshint validthis:true */
|
|
var Promise = this;
|
|
|
|
if (!isArray(promises)) {
|
|
throw new TypeError('You must pass an array to race.');
|
|
}
|
|
return new Promise(function(resolve, reject) {
|
|
var results = [], promise;
|
|
|
|
for (var i = 0; i < promises.length; i++) {
|
|
promise = promises[i];
|
|
|
|
if (promise && typeof promise.then === 'function') {
|
|
promise.then(resolve, reject);
|
|
} else {
|
|
resolve(promise);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
__exports__.race = race;
|
|
});
|
|
define("promise/reject",
|
|
["exports"],
|
|
function(__exports__) {
|
|
"use strict";
|
|
/**
|
|
`RSVP.reject` returns a promise that will become rejected with the passed
|
|
`reason`. `RSVP.reject` is essentially shorthand for the following:
|
|
|
|
```javascript
|
|
var promise = new RSVP.Promise(function(resolve, reject){
|
|
reject(new Error('WHOOPS'));
|
|
});
|
|
|
|
promise.then(function(value){
|
|
// Code here doesn't run because the promise is rejected!
|
|
}, function(reason){
|
|
// reason.message === 'WHOOPS'
|
|
});
|
|
```
|
|
|
|
Instead of writing the above, your code now simply becomes the following:
|
|
|
|
```javascript
|
|
var promise = RSVP.reject(new Error('WHOOPS'));
|
|
|
|
promise.then(function(value){
|
|
// Code here doesn't run because the promise is rejected!
|
|
}, function(reason){
|
|
// reason.message === 'WHOOPS'
|
|
});
|
|
```
|
|
|
|
@method reject
|
|
@for RSVP
|
|
@param {Any} reason value that the returned promise will be rejected with.
|
|
@param {String} label optional string for identifying the returned promise.
|
|
Useful for tooling.
|
|
@return {Promise} a promise that will become rejected with the given
|
|
`reason`.
|
|
*/
|
|
function reject(reason) {
|
|
/*jshint validthis:true */
|
|
var Promise = this;
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
reject(reason);
|
|
});
|
|
}
|
|
|
|
__exports__.reject = reject;
|
|
});
|
|
define("promise/resolve",
|
|
["exports"],
|
|
function(__exports__) {
|
|
"use strict";
|
|
function resolve(value) {
|
|
/*jshint validthis:true */
|
|
if (value && typeof value === 'object' && value.constructor === this) {
|
|
return value;
|
|
}
|
|
|
|
var Promise = this;
|
|
|
|
return new Promise(function(resolve) {
|
|
resolve(value);
|
|
});
|
|
}
|
|
|
|
__exports__.resolve = resolve;
|
|
});
|
|
define("promise/utils",
|
|
["exports"],
|
|
function(__exports__) {
|
|
"use strict";
|
|
function objectOrFunction(x) {
|
|
return isFunction(x) || (typeof x === "object" && x !== null);
|
|
}
|
|
|
|
function isFunction(x) {
|
|
return typeof x === "function";
|
|
}
|
|
|
|
function isArray(x) {
|
|
return Object.prototype.toString.call(x) === "[object Array]";
|
|
}
|
|
|
|
// Date.now is not available in browsers < IE9
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility
|
|
var now = Date.now || function() { return new Date().getTime(); };
|
|
|
|
|
|
__exports__.objectOrFunction = objectOrFunction;
|
|
__exports__.isFunction = isFunction;
|
|
__exports__.isArray = isArray;
|
|
__exports__.now = now;
|
|
});
|
|
requireModule('promise/polyfill').polyfill();
|
|
}());(function webpackUniversalModuleDefinition(root, factory) {
|
|
if(typeof exports === 'object' && typeof module === 'object')
|
|
module.exports = factory();
|
|
else if(typeof define === 'function' && define.amd)
|
|
define([], factory);
|
|
else if(typeof exports === 'object')
|
|
exports["localforage"] = factory();
|
|
else
|
|
root["localforage"] = factory();
|
|
})(this, function() {
|
|
return /******/ (function(modules) { // webpackBootstrap
|
|
/******/ // The module cache
|
|
/******/ var installedModules = {};
|
|
|
|
/******/ // The require function
|
|
/******/ function __webpack_require__(moduleId) {
|
|
|
|
/******/ // Check if module is in cache
|
|
/******/ if(installedModules[moduleId])
|
|
/******/ return installedModules[moduleId].exports;
|
|
|
|
/******/ // Create a new module (and put it into the cache)
|
|
/******/ var module = installedModules[moduleId] = {
|
|
/******/ exports: {},
|
|
/******/ id: moduleId,
|
|
/******/ loaded: false
|
|
/******/ };
|
|
|
|
/******/ // Execute the module function
|
|
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
|
|
/******/ // Flag the module as loaded
|
|
/******/ module.loaded = true;
|
|
|
|
/******/ // Return the exports of the module
|
|
/******/ return module.exports;
|
|
/******/ }
|
|
|
|
|
|
/******/ // expose the modules object (__webpack_modules__)
|
|
/******/ __webpack_require__.m = modules;
|
|
|
|
/******/ // expose the module cache
|
|
/******/ __webpack_require__.c = installedModules;
|
|
|
|
/******/ // __webpack_public_path__
|
|
/******/ __webpack_require__.p = "";
|
|
|
|
/******/ // Load entry module and return exports
|
|
/******/ return __webpack_require__(0);
|
|
/******/ })
|
|
/************************************************************************/
|
|
/******/ ([
|
|
/* 0 */
|
|
/***/ function(module, exports, __webpack_require__) {
|
|
|
|
'use strict';
|
|
|
|
exports.__esModule = true;
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Custom drivers are stored here when `defineDriver()` is called.
|
|
// They are shared across all instances of localForage.
|
|
var CustomDrivers = {};
|
|
|
|
var DriverType = {
|
|
INDEXEDDB: 'asyncStorage',
|
|
LOCALSTORAGE: 'localStorageWrapper',
|
|
WEBSQL: 'webSQLStorage'
|
|
};
|
|
|
|
var DefaultDriverOrder = [DriverType.INDEXEDDB, DriverType.WEBSQL, DriverType.LOCALSTORAGE];
|
|
|
|
var LibraryMethods = ['clear', 'getItem', 'iterate', 'key', 'keys', 'length', 'removeItem', 'setItem'];
|
|
|
|
var DefaultConfig = {
|
|
description: '',
|
|
driver: DefaultDriverOrder.slice(),
|
|
name: 'localforage',
|
|
// Default DB size is _JUST UNDER_ 5MB, as it's the highest size
|
|
// we can use without a prompt.
|
|
size: 4980736,
|
|
storeName: 'keyvaluepairs',
|
|
version: 1.0
|
|
};
|
|
|
|
// Check to see if IndexedDB is available and if it is the latest
|
|
// implementation; it's our preferred backend library. We use "_spec_test"
|
|
// as the name of the database because it's not the one we'll operate on,
|
|
// but it's useful to make sure its using the right spec.
|
|
// See: https://github.com/mozilla/localForage/issues/128
|
|
var driverSupport = (function (self) {
|
|
// Initialize IndexedDB; fall back to vendor-prefixed versions
|
|
// if needed.
|
|
var indexedDB = indexedDB || self.indexedDB || self.webkitIndexedDB || self.mozIndexedDB || self.OIndexedDB || self.msIndexedDB;
|
|
|
|
var result = {};
|
|
|
|
result[DriverType.WEBSQL] = !!self.openDatabase;
|
|
result[DriverType.INDEXEDDB] = !!(function () {
|
|
// We mimic PouchDB here; just UA test for Safari (which, as of
|
|
// iOS 8/Yosemite, doesn't properly support IndexedDB).
|
|
// IndexedDB support is broken and different from Blink's.
|
|
// This is faster than the test case (and it's sync), so we just
|
|
// do this. *SIGH*
|
|
// http://bl.ocks.org/nolanlawson/raw/c83e9039edf2278047e9/
|
|
//
|
|
// We test for openDatabase because IE Mobile identifies itself
|
|
// as Safari. Oh the lulz...
|
|
if (typeof self.openDatabase !== 'undefined' && self.navigator && self.navigator.userAgent && /Safari/.test(self.navigator.userAgent) && !/Chrome/.test(self.navigator.userAgent)) {
|
|
return false;
|
|
}
|
|
try {
|
|
return indexedDB && typeof indexedDB.open === 'function' &&
|
|
// Some Samsung/HTC Android 4.0-4.3 devices
|
|
// have older IndexedDB specs; if this isn't available
|
|
// their IndexedDB is too old for us to use.
|
|
// (Replaces the onupgradeneeded test.)
|
|
typeof self.IDBKeyRange !== 'undefined';
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
result[DriverType.LOCALSTORAGE] = !!(function () {
|
|
try {
|
|
return self.localStorage && 'setItem' in self.localStorage && self.localStorage.setItem;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
return result;
|
|
})(this);
|
|
|
|
var isArray = Array.isArray || function (arg) {
|
|
return Object.prototype.toString.call(arg) === '[object Array]';
|
|
};
|
|
|
|
function callWhenReady(localForageInstance, libraryMethod) {
|
|
localForageInstance[libraryMethod] = function () {
|
|
var _args = arguments;
|
|
return localForageInstance.ready().then(function () {
|
|
return localForageInstance[libraryMethod].apply(localForageInstance, _args);
|
|
});
|
|
};
|
|
}
|
|
|
|
function extend() {
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
var arg = arguments[i];
|
|
|
|
if (arg) {
|
|
for (var key in arg) {
|
|
if (arg.hasOwnProperty(key)) {
|
|
if (isArray(arg[key])) {
|
|
arguments[0][key] = arg[key].slice();
|
|
} else {
|
|
arguments[0][key] = arg[key];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return arguments[0];
|
|
}
|
|
|
|
function isLibraryDriver(driverName) {
|
|
for (var driver in DriverType) {
|
|
if (DriverType.hasOwnProperty(driver) && DriverType[driver] === driverName) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
var LocalForage = (function () {
|
|
function LocalForage(options) {
|
|
_classCallCheck(this, LocalForage);
|
|
|
|
this.INDEXEDDB = DriverType.INDEXEDDB;
|
|
this.LOCALSTORAGE = DriverType.LOCALSTORAGE;
|
|
this.WEBSQL = DriverType.WEBSQL;
|
|
|
|
this._defaultConfig = extend({}, DefaultConfig);
|
|
this._config = extend({}, this._defaultConfig, options);
|
|
this._driverSet = null;
|
|
this._initDriver = null;
|
|
this._ready = false;
|
|
this._dbInfo = null;
|
|
|
|
this._wrapLibraryMethodsWithReady();
|
|
this.setDriver(this._config.driver);
|
|
}
|
|
|
|
// The actual localForage object that we expose as a module or via a
|
|
// global. It's extended by pulling in one of our other libraries.
|
|
|
|
// Set any config values for localForage; can be called anytime before
|
|
// the first API call (e.g. `getItem`, `setItem`).
|
|
// We loop through options so we don't overwrite existing config
|
|
// values.
|
|
|
|
LocalForage.prototype.config = function config(options) {
|
|
// If the options argument is an object, we use it to set values.
|
|
// Otherwise, we return either a specified config value or all
|
|
// config values.
|
|
if (typeof options === 'object') {
|
|
// If localforage is ready and fully initialized, we can't set
|
|
// any new configuration values. Instead, we return an error.
|
|
if (this._ready) {
|
|
return new Error("Can't call config() after localforage " + 'has been used.');
|
|
}
|
|
|
|
for (var i in options) {
|
|
if (i === 'storeName') {
|
|
options[i] = options[i].replace(/\W/g, '_');
|
|
}
|
|
|
|
this._config[i] = options[i];
|
|
}
|
|
|
|
// after all config options are set and
|
|
// the driver option is used, try setting it
|
|
if ('driver' in options && options.driver) {
|
|
this.setDriver(this._config.driver);
|
|
}
|
|
|
|
return true;
|
|
} else if (typeof options === 'string') {
|
|
return this._config[options];
|
|
} else {
|
|
return this._config;
|
|
}
|
|
};
|
|
|
|
// Used to define a custom driver, shared across all instances of
|
|
// localForage.
|
|
|
|
LocalForage.prototype.defineDriver = function defineDriver(driverObject, callback, errorCallback) {
|
|
var promise = new Promise(function (resolve, reject) {
|
|
try {
|
|
var driverName = driverObject._driver;
|
|
var complianceError = new Error('Custom driver not compliant; see ' + 'https://mozilla.github.io/localForage/#definedriver');
|
|
var namingError = new Error('Custom driver name already in use: ' + driverObject._driver);
|
|
|
|
// A driver name should be defined and not overlap with the
|
|
// library-defined, default drivers.
|
|
if (!driverObject._driver) {
|
|
reject(complianceError);
|
|
return;
|
|
}
|
|
if (isLibraryDriver(driverObject._driver)) {
|
|
reject(namingError);
|
|
return;
|
|
}
|
|
|
|
var customDriverMethods = LibraryMethods.concat('_initStorage');
|
|
for (var i = 0; i < customDriverMethods.length; i++) {
|
|
var customDriverMethod = customDriverMethods[i];
|
|
if (!customDriverMethod || !driverObject[customDriverMethod] || typeof driverObject[customDriverMethod] !== 'function') {
|
|
reject(complianceError);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var supportPromise = Promise.resolve(true);
|
|
if ('_support' in driverObject) {
|
|
if (driverObject._support && typeof driverObject._support === 'function') {
|
|
supportPromise = driverObject._support();
|
|
} else {
|
|
supportPromise = Promise.resolve(!!driverObject._support);
|
|
}
|
|
}
|
|
|
|
supportPromise.then(function (supportResult) {
|
|
driverSupport[driverName] = supportResult;
|
|
CustomDrivers[driverName] = driverObject;
|
|
resolve();
|
|
}, reject);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
|
|
promise.then(callback, errorCallback);
|
|
return promise;
|
|
};
|
|
|
|
LocalForage.prototype.driver = function driver() {
|
|
return this._driver || null;
|
|
};
|
|
|
|
LocalForage.prototype.getDriver = function getDriver(driverName, callback, errorCallback) {
|
|
var self = this;
|
|
var getDriverPromise = (function () {
|
|
if (isLibraryDriver(driverName)) {
|
|
switch (driverName) {
|
|
case self.INDEXEDDB:
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(__webpack_require__(1));
|
|
});
|
|
case self.LOCALSTORAGE:
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(__webpack_require__(2));
|
|
});
|
|
case self.WEBSQL:
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(__webpack_require__(4));
|
|
});
|
|
}
|
|
} else if (CustomDrivers[driverName]) {
|
|
return Promise.resolve(CustomDrivers[driverName]);
|
|
}
|
|
|
|
return Promise.reject(new Error('Driver not found.'));
|
|
})();
|
|
|
|
getDriverPromise.then(callback, errorCallback);
|
|
return getDriverPromise;
|
|
};
|
|
|
|
LocalForage.prototype.getSerializer = function getSerializer(callback) {
|
|
var serializerPromise = new Promise(function (resolve, reject) {
|
|
resolve(__webpack_require__(3));
|
|
});
|
|
if (callback && typeof callback === 'function') {
|
|
serializerPromise.then(function (result) {
|
|
callback(result);
|
|
});
|
|
}
|
|
return serializerPromise;
|
|
};
|
|
|
|
LocalForage.prototype.ready = function ready(callback) {
|
|
var self = this;
|
|
|
|
var promise = self._driverSet.then(function () {
|
|
if (self._ready === null) {
|
|
self._ready = self._initDriver();
|
|
}
|
|
|
|
return self._ready;
|
|
});
|
|
|
|
promise.then(callback, callback);
|
|
return promise;
|
|
};
|
|
|
|
LocalForage.prototype.setDriver = function setDriver(drivers, callback, errorCallback) {
|
|
var self = this;
|
|
|
|
if (!isArray(drivers)) {
|
|
drivers = [drivers];
|
|
}
|
|
|
|
var supportedDrivers = this._getSupportedDrivers(drivers);
|
|
|
|
function setDriverToConfig() {
|
|
self._config.driver = self.driver();
|
|
}
|
|
|
|
function initDriver(supportedDrivers) {
|
|
return function () {
|
|
var currentDriverIndex = 0;
|
|
|
|
function driverPromiseLoop() {
|
|
while (currentDriverIndex < supportedDrivers.length) {
|
|
var driverName = supportedDrivers[currentDriverIndex];
|
|
currentDriverIndex++;
|
|
|
|
self._dbInfo = null;
|
|
self._ready = null;
|
|
|
|
return self.getDriver(driverName).then(function (driver) {
|
|
self._extend(driver);
|
|
setDriverToConfig();
|
|
|
|
self._ready = self._initStorage(self._config);
|
|
return self._ready;
|
|
})['catch'](driverPromiseLoop);
|
|
}
|
|
|
|
setDriverToConfig();
|
|
var error = new Error('No available storage method found.');
|
|
self._driverSet = Promise.reject(error);
|
|
return self._driverSet;
|
|
}
|
|
|
|
return driverPromiseLoop();
|
|
};
|
|
}
|
|
|
|
// There might be a driver initialization in progress
|
|
// so wait for it to finish in order to avoid a possible
|
|
// race condition to set _dbInfo
|
|
var oldDriverSetDone = this._driverSet !== null ? this._driverSet['catch'](function () {
|
|
return Promise.resolve();
|
|
}) : Promise.resolve();
|
|
|
|
this._driverSet = oldDriverSetDone.then(function () {
|
|
var driverName = supportedDrivers[0];
|
|
self._dbInfo = null;
|
|
self._ready = null;
|
|
|
|
return self.getDriver(driverName).then(function (driver) {
|
|
self._driver = driver._driver;
|
|
setDriverToConfig();
|
|
self._wrapLibraryMethodsWithReady();
|
|
self._initDriver = initDriver(supportedDrivers);
|
|
});
|
|
})['catch'](function () {
|
|
setDriverToConfig();
|
|
var error = new Error('No available storage method found.');
|
|
self._driverSet = Promise.reject(error);
|
|
return self._driverSet;
|
|
});
|
|
|
|
this._driverSet.then(callback, errorCallback);
|
|
return this._driverSet;
|
|
};
|
|
|
|
LocalForage.prototype.supports = function supports(driverName) {
|
|
return !!driverSupport[driverName];
|
|
};
|
|
|
|
LocalForage.prototype._extend = function _extend(libraryMethodsAndProperties) {
|
|
extend(this, libraryMethodsAndProperties);
|
|
};
|
|
|
|
LocalForage.prototype._getSupportedDrivers = function _getSupportedDrivers(drivers) {
|
|
var supportedDrivers = [];
|
|
for (var i = 0, len = drivers.length; i < len; i++) {
|
|
var driverName = drivers[i];
|
|
if (this.supports(driverName)) {
|
|
supportedDrivers.push(driverName);
|
|
}
|
|
}
|
|
return supportedDrivers;
|
|
};
|
|
|
|
LocalForage.prototype._wrapLibraryMethodsWithReady = function _wrapLibraryMethodsWithReady() {
|
|
// Add a stub for each driver API method that delays the call to the
|
|
// corresponding driver method until localForage is ready. These stubs
|
|
// will be replaced by the driver methods as soon as the driver is
|
|
// loaded, so there is no performance impact.
|
|
for (var i = 0; i < LibraryMethods.length; i++) {
|
|
callWhenReady(this, LibraryMethods[i]);
|
|
}
|
|
};
|
|
|
|
LocalForage.prototype.createInstance = function createInstance(options) {
|
|
return new LocalForage(options);
|
|
};
|
|
|
|
return LocalForage;
|
|
})();
|
|
|
|
var localForage = new LocalForage();
|
|
|
|
exports['default'] = localForage;
|
|
}).call(typeof window !== 'undefined' ? window : self);
|
|
module.exports = exports['default'];
|
|
|
|
/***/ },
|
|
/* 1 */
|
|
/***/ function(module, exports) {
|
|
|
|
// Some code originally from async_storage.js in
|
|
// [Gaia](https://github.com/mozilla-b2g/gaia).
|
|
'use strict';
|
|
|
|
exports.__esModule = true;
|
|
(function () {
|
|
'use strict';
|
|
|
|
var globalObject = this;
|
|
// Initialize IndexedDB; fall back to vendor-prefixed versions if needed.
|
|
var indexedDB = indexedDB || this.indexedDB || this.webkitIndexedDB || this.mozIndexedDB || this.OIndexedDB || this.msIndexedDB;
|
|
|
|
// If IndexedDB isn't available, we get outta here!
|
|
if (!indexedDB) {
|
|
return;
|
|
}
|
|
|
|
var DETECT_BLOB_SUPPORT_STORE = 'local-forage-detect-blob-support';
|
|
var supportsBlobs;
|
|
var dbContexts;
|
|
|
|
// Abstracts constructing a Blob object, so it also works in older
|
|
// browsers that don't support the native Blob constructor. (i.e.
|
|
// old QtWebKit versions, at least).
|
|
function _createBlob(parts, properties) {
|
|
parts = parts || [];
|
|
properties = properties || {};
|
|
try {
|
|
return new Blob(parts, properties);
|
|
} catch (e) {
|
|
if (e.name !== 'TypeError') {
|
|
throw e;
|
|
}
|
|
var BlobBuilder = globalObject.BlobBuilder || globalObject.MSBlobBuilder || globalObject.MozBlobBuilder || globalObject.WebKitBlobBuilder;
|
|
var builder = new BlobBuilder();
|
|
for (var i = 0; i < parts.length; i += 1) {
|
|
builder.append(parts[i]);
|
|
}
|
|
return builder.getBlob(properties.type);
|
|
}
|
|
}
|
|
|
|
// Transform a binary string to an array buffer, because otherwise
|
|
// weird stuff happens when you try to work with the binary string directly.
|
|
// It is known.
|
|
// From http://stackoverflow.com/questions/14967647/ (continues on next line)
|
|
// encode-decode-image-with-base64-breaks-image (2013-04-21)
|
|
function _binStringToArrayBuffer(bin) {
|
|
var length = bin.length;
|
|
var buf = new ArrayBuffer(length);
|
|
var arr = new Uint8Array(buf);
|
|
for (var i = 0; i < length; i++) {
|
|
arr[i] = bin.charCodeAt(i);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
// Fetch a blob using ajax. This reveals bugs in Chrome < 43.
|
|
// For details on all this junk:
|
|
// https://github.com/nolanlawson/state-of-binary-data-in-the-browser#readme
|
|
function _blobAjax(url) {
|
|
return new Promise(function (resolve, reject) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('GET', url);
|
|
xhr.withCredentials = true;
|
|
xhr.responseType = 'arraybuffer';
|
|
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState !== 4) {
|
|
return;
|
|
}
|
|
if (xhr.status === 200) {
|
|
return resolve({
|
|
response: xhr.response,
|
|
type: xhr.getResponseHeader('Content-Type')
|
|
});
|
|
}
|
|
reject({ status: xhr.status, response: xhr.response });
|
|
};
|
|
xhr.send();
|
|
});
|
|
}
|
|
|
|
//
|
|
// Detect blob support. Chrome didn't support it until version 38.
|
|
// In version 37 they had a broken version where PNGs (and possibly
|
|
// other binary types) aren't stored correctly, because when you fetch
|
|
// them, the content type is always null.
|
|
//
|
|
// Furthermore, they have some outstanding bugs where blobs occasionally
|
|
// are read by FileReader as null, or by ajax as 404s.
|
|
//
|
|
// Sadly we use the 404 bug to detect the FileReader bug, so if they
|
|
// get fixed independently and released in different versions of Chrome,
|
|
// then the bug could come back. So it's worthwhile to watch these issues:
|
|
// 404 bug: https://code.google.com/p/chromium/issues/detail?id=447916
|
|
// FileReader bug: https://code.google.com/p/chromium/issues/detail?id=447836
|
|
//
|
|
function _checkBlobSupportWithoutCaching(idb) {
|
|
return new Promise(function (resolve, reject) {
|
|
var blob = _createBlob([''], { type: 'image/png' });
|
|
var txn = idb.transaction([DETECT_BLOB_SUPPORT_STORE], 'readwrite');
|
|
txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(blob, 'key');
|
|
txn.oncomplete = function () {
|
|
// have to do it in a separate transaction, else the correct
|
|
// content type is always returned
|
|
var blobTxn = idb.transaction([DETECT_BLOB_SUPPORT_STORE], 'readwrite');
|
|
var getBlobReq = blobTxn.objectStore(DETECT_BLOB_SUPPORT_STORE).get('key');
|
|
getBlobReq.onerror = reject;
|
|
getBlobReq.onsuccess = function (e) {
|
|
|
|
var storedBlob = e.target.result;
|
|
var url = URL.createObjectURL(storedBlob);
|
|
|
|
_blobAjax(url).then(function (res) {
|
|
resolve(!!(res && res.type === 'image/png'));
|
|
}, function () {
|
|
resolve(false);
|
|
}).then(function () {
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
};
|
|
};
|
|
})['catch'](function () {
|
|
return false; // error, so assume unsupported
|
|
});
|
|
}
|
|
|
|
function _checkBlobSupport(idb) {
|
|
if (typeof supportsBlobs === 'boolean') {
|
|
return Promise.resolve(supportsBlobs);
|
|
}
|
|
return _checkBlobSupportWithoutCaching(idb).then(function (value) {
|
|
supportsBlobs = value;
|
|
return supportsBlobs;
|
|
});
|
|
}
|
|
|
|
// encode a blob for indexeddb engines that don't support blobs
|
|
function _encodeBlob(blob) {
|
|
return new Promise(function (resolve, reject) {
|
|
var reader = new FileReader();
|
|
reader.onerror = reject;
|
|
reader.onloadend = function (e) {
|
|
var base64 = btoa(e.target.result || '');
|
|
resolve({
|
|
__local_forage_encoded_blob: true,
|
|
data: base64,
|
|
type: blob.type
|
|
});
|
|
};
|
|
reader.readAsBinaryString(blob);
|
|
});
|
|
}
|
|
|
|
// decode an encoded blob
|
|
function _decodeBlob(encodedBlob) {
|
|
var arrayBuff = _binStringToArrayBuffer(atob(encodedBlob.data));
|
|
return _createBlob([arrayBuff], { type: encodedBlob.type });
|
|
}
|
|
|
|
// is this one of our fancy encoded blobs?
|
|
function _isEncodedBlob(value) {
|
|
return value && value.__local_forage_encoded_blob;
|
|
}
|
|
|
|
// Open the IndexedDB database (automatically creates one if one didn't
|
|
// previously exist), using any options set in the config.
|
|
function _initStorage(options) {
|
|
var self = this;
|
|
var dbInfo = {
|
|
db: null
|
|
};
|
|
|
|
if (options) {
|
|
for (var i in options) {
|
|
dbInfo[i] = options[i];
|
|
}
|
|
}
|
|
|
|
// Initialize a singleton container for all running localForages.
|
|
if (!dbContexts) {
|
|
dbContexts = {};
|
|
}
|
|
|
|
// Get the current context of the database;
|
|
var dbContext = dbContexts[dbInfo.name];
|
|
|
|
// ...or create a new context.
|
|
if (!dbContext) {
|
|
dbContext = {
|
|
// Running localForages sharing a database.
|
|
forages: [],
|
|
// Shared database.
|
|
db: null
|
|
};
|
|
// Register the new context in the global container.
|
|
dbContexts[dbInfo.name] = dbContext;
|
|
}
|
|
|
|
// Register itself as a running localForage in the current context.
|
|
dbContext.forages.push(this);
|
|
|
|
// Create an array of readiness of the related localForages.
|
|
var readyPromises = [];
|
|
|
|
function ignoreErrors() {
|
|
// Don't handle errors here,
|
|
// just makes sure related localForages aren't pending.
|
|
return Promise.resolve();
|
|
}
|
|
|
|
for (var j = 0; j < dbContext.forages.length; j++) {
|
|
var forage = dbContext.forages[j];
|
|
if (forage !== this) {
|
|
// Don't wait for itself...
|
|
readyPromises.push(forage.ready()['catch'](ignoreErrors));
|
|
}
|
|
}
|
|
|
|
// Take a snapshot of the related localForages.
|
|
var forages = dbContext.forages.slice(0);
|
|
|
|
// Initialize the connection process only when
|
|
// all the related localForages aren't pending.
|
|
return Promise.all(readyPromises).then(function () {
|
|
dbInfo.db = dbContext.db;
|
|
// Get the connection or open a new one without upgrade.
|
|
return _getOriginalConnection(dbInfo);
|
|
}).then(function (db) {
|
|
dbInfo.db = db;
|
|
if (_isUpgradeNeeded(dbInfo, self._defaultConfig.version)) {
|
|
// Reopen the database for upgrading.
|
|
return _getUpgradedConnection(dbInfo);
|
|
}
|
|
return db;
|
|
}).then(function (db) {
|
|
dbInfo.db = dbContext.db = db;
|
|
self._dbInfo = dbInfo;
|
|
// Share the final connection amongst related localForages.
|
|
for (var k in forages) {
|
|
var forage = forages[k];
|
|
if (forage !== self) {
|
|
// Self is already up-to-date.
|
|
forage._dbInfo.db = dbInfo.db;
|
|
forage._dbInfo.version = dbInfo.version;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function _getOriginalConnection(dbInfo) {
|
|
return _getConnection(dbInfo, false);
|
|
}
|
|
|
|
function _getUpgradedConnection(dbInfo) {
|
|
return _getConnection(dbInfo, true);
|
|
}
|
|
|
|
function _getConnection(dbInfo, upgradeNeeded) {
|
|
return new Promise(function (resolve, reject) {
|
|
if (dbInfo.db) {
|
|
if (upgradeNeeded) {
|
|
dbInfo.db.close();
|
|
} else {
|
|
return resolve(dbInfo.db);
|
|
}
|
|
}
|
|
|
|
var dbArgs = [dbInfo.name];
|
|
|
|
if (upgradeNeeded) {
|
|
dbArgs.push(dbInfo.version);
|
|
}
|
|
|
|
var openreq = indexedDB.open.apply(indexedDB, dbArgs);
|
|
|
|
if (upgradeNeeded) {
|
|
openreq.onupgradeneeded = function (e) {
|
|
var db = openreq.result;
|
|
try {
|
|
db.createObjectStore(dbInfo.storeName);
|
|
if (e.oldVersion <= 1) {
|
|
// Added when support for blob shims was added
|
|
db.createObjectStore(DETECT_BLOB_SUPPORT_STORE);
|
|
}
|
|
} catch (ex) {
|
|
if (ex.name === 'ConstraintError') {
|
|
globalObject.console.warn('The database "' + dbInfo.name + '"' + ' has been upgraded from version ' + e.oldVersion + ' to version ' + e.newVersion + ', but the storage "' + dbInfo.storeName + '" already exists.');
|
|
} else {
|
|
throw ex;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
openreq.onerror = function () {
|
|
reject(openreq.error);
|
|
};
|
|
|
|
openreq.onsuccess = function () {
|
|
resolve(openreq.result);
|
|
};
|
|
});
|
|
}
|
|
|
|
function _isUpgradeNeeded(dbInfo, defaultVersion) {
|
|
if (!dbInfo.db) {
|
|
return true;
|
|
}
|
|
|
|
var isNewStore = !dbInfo.db.objectStoreNames.contains(dbInfo.storeName);
|
|
var isDowngrade = dbInfo.version < dbInfo.db.version;
|
|
var isUpgrade = dbInfo.version > dbInfo.db.version;
|
|
|
|
if (isDowngrade) {
|
|
// If the version is not the default one
|
|
// then warn for impossible downgrade.
|
|
if (dbInfo.version !== defaultVersion) {
|
|
globalObject.console.warn('The database "' + dbInfo.name + '"' + ' can\'t be downgraded from version ' + dbInfo.db.version + ' to version ' + dbInfo.version + '.');
|
|
}
|
|
// Align the versions to prevent errors.
|
|
dbInfo.version = dbInfo.db.version;
|
|
}
|
|
|
|
if (isUpgrade || isNewStore) {
|
|
// If the store is new then increment the version (if needed).
|
|
// This will trigger an "upgradeneeded" event which is required
|
|
// for creating a store.
|
|
if (isNewStore) {
|
|
var incVersion = dbInfo.db.version + 1;
|
|
if (incVersion > dbInfo.version) {
|
|
dbInfo.version = incVersion;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function getItem(key, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName);
|
|
var req = store.get(key);
|
|
|
|
req.onsuccess = function () {
|
|
var value = req.result;
|
|
if (value === undefined) {
|
|
value = null;
|
|
}
|
|
if (_isEncodedBlob(value)) {
|
|
value = _decodeBlob(value);
|
|
}
|
|
resolve(value);
|
|
};
|
|
|
|
req.onerror = function () {
|
|
reject(req.error);
|
|
};
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Iterate over all items stored in database.
|
|
function iterate(iterator, callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName);
|
|
|
|
var req = store.openCursor();
|
|
var iterationNumber = 1;
|
|
|
|
req.onsuccess = function () {
|
|
var cursor = req.result;
|
|
|
|
if (cursor) {
|
|
var value = cursor.value;
|
|
if (_isEncodedBlob(value)) {
|
|
value = _decodeBlob(value);
|
|
}
|
|
var result = iterator(value, cursor.key, iterationNumber++);
|
|
|
|
if (result !== void 0) {
|
|
resolve(result);
|
|
} else {
|
|
cursor['continue']();
|
|
}
|
|
} else {
|
|
resolve();
|
|
}
|
|
};
|
|
|
|
req.onerror = function () {
|
|
reject(req.error);
|
|
};
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
|
|
return promise;
|
|
}
|
|
|
|
function setItem(key, value, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
var dbInfo;
|
|
self.ready().then(function () {
|
|
dbInfo = self._dbInfo;
|
|
return _checkBlobSupport(dbInfo.db);
|
|
}).then(function (blobSupport) {
|
|
if (!blobSupport && value instanceof Blob) {
|
|
return _encodeBlob(value);
|
|
}
|
|
return value;
|
|
}).then(function (value) {
|
|
var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite');
|
|
var store = transaction.objectStore(dbInfo.storeName);
|
|
|
|
// The reason we don't _save_ null is because IE 10 does
|
|
// not support saving the `null` type in IndexedDB. How
|
|
// ironic, given the bug below!
|
|
// See: https://github.com/mozilla/localForage/issues/161
|
|
if (value === null) {
|
|
value = undefined;
|
|
}
|
|
|
|
var req = store.put(value, key);
|
|
transaction.oncomplete = function () {
|
|
// Cast to undefined so the value passed to
|
|
// callback/promise is the same as what one would get out
|
|
// of `getItem()` later. This leads to some weirdness
|
|
// (setItem('foo', undefined) will return `null`), but
|
|
// it's not my fault localStorage is our baseline and that
|
|
// it's weird.
|
|
if (value === undefined) {
|
|
value = null;
|
|
}
|
|
|
|
resolve(value);
|
|
};
|
|
transaction.onabort = transaction.onerror = function () {
|
|
var err = req.error ? req.error : req.transaction.error;
|
|
reject(err);
|
|
};
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function removeItem(key, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite');
|
|
var store = transaction.objectStore(dbInfo.storeName);
|
|
|
|
// We use a Grunt task to make this safe for IE and some
|
|
// versions of Android (including those used by Cordova).
|
|
// Normally IE won't like `.delete()` and will insist on
|
|
// using `['delete']()`, but we have a build step that
|
|
// fixes this for us now.
|
|
var req = store['delete'](key);
|
|
transaction.oncomplete = function () {
|
|
resolve();
|
|
};
|
|
|
|
transaction.onerror = function () {
|
|
reject(req.error);
|
|
};
|
|
|
|
// The request will be also be aborted if we've exceeded our storage
|
|
// space.
|
|
transaction.onabort = function () {
|
|
var err = req.error ? req.error : req.transaction.error;
|
|
reject(err);
|
|
};
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function clear(callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite');
|
|
var store = transaction.objectStore(dbInfo.storeName);
|
|
var req = store.clear();
|
|
|
|
transaction.oncomplete = function () {
|
|
resolve();
|
|
};
|
|
|
|
transaction.onabort = transaction.onerror = function () {
|
|
var err = req.error ? req.error : req.transaction.error;
|
|
reject(err);
|
|
};
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function length(callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName);
|
|
var req = store.count();
|
|
|
|
req.onsuccess = function () {
|
|
resolve(req.result);
|
|
};
|
|
|
|
req.onerror = function () {
|
|
reject(req.error);
|
|
};
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function key(n, callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
if (n < 0) {
|
|
resolve(null);
|
|
|
|
return;
|
|
}
|
|
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName);
|
|
|
|
var advanced = false;
|
|
var req = store.openCursor();
|
|
req.onsuccess = function () {
|
|
var cursor = req.result;
|
|
if (!cursor) {
|
|
// this means there weren't enough keys
|
|
resolve(null);
|
|
|
|
return;
|
|
}
|
|
|
|
if (n === 0) {
|
|
// We have the first key, return it if that's what they
|
|
// wanted.
|
|
resolve(cursor.key);
|
|
} else {
|
|
if (!advanced) {
|
|
// Otherwise, ask the cursor to skip ahead n
|
|
// records.
|
|
advanced = true;
|
|
cursor.advance(n);
|
|
} else {
|
|
// When we get here, we've got the nth key.
|
|
resolve(cursor.key);
|
|
}
|
|
}
|
|
};
|
|
|
|
req.onerror = function () {
|
|
reject(req.error);
|
|
};
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function keys(callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly').objectStore(dbInfo.storeName);
|
|
|
|
var req = store.openCursor();
|
|
var keys = [];
|
|
|
|
req.onsuccess = function () {
|
|
var cursor = req.result;
|
|
|
|
if (!cursor) {
|
|
resolve(keys);
|
|
return;
|
|
}
|
|
|
|
keys.push(cursor.key);
|
|
cursor['continue']();
|
|
};
|
|
|
|
req.onerror = function () {
|
|
reject(req.error);
|
|
};
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function executeCallback(promise, callback) {
|
|
if (callback) {
|
|
promise.then(function (result) {
|
|
callback(null, result);
|
|
}, function (error) {
|
|
callback(error);
|
|
});
|
|
}
|
|
}
|
|
|
|
var asyncStorage = {
|
|
_driver: 'asyncStorage',
|
|
_initStorage: _initStorage,
|
|
iterate: iterate,
|
|
getItem: getItem,
|
|
setItem: setItem,
|
|
removeItem: removeItem,
|
|
clear: clear,
|
|
length: length,
|
|
key: key,
|
|
keys: keys
|
|
};
|
|
|
|
exports['default'] = asyncStorage;
|
|
}).call(typeof window !== 'undefined' ? window : self);
|
|
module.exports = exports['default'];
|
|
|
|
/***/ },
|
|
/* 2 */
|
|
/***/ function(module, exports, __webpack_require__) {
|
|
|
|
// If IndexedDB isn't available, we'll fall back to localStorage.
|
|
// Note that this will have considerable performance and storage
|
|
// side-effects (all data will be serialized on save and only data that
|
|
// can be converted to a string via `JSON.stringify()` will be saved).
|
|
'use strict';
|
|
|
|
exports.__esModule = true;
|
|
(function () {
|
|
'use strict';
|
|
|
|
var globalObject = this;
|
|
var localStorage = null;
|
|
|
|
// If the app is running inside a Google Chrome packaged webapp, or some
|
|
// other context where localStorage isn't available, we don't use
|
|
// localStorage. This feature detection is preferred over the old
|
|
// `if (window.chrome && window.chrome.runtime)` code.
|
|
// See: https://github.com/mozilla/localForage/issues/68
|
|
try {
|
|
// If localStorage isn't available, we get outta here!
|
|
// This should be inside a try catch
|
|
if (!this.localStorage || !('setItem' in this.localStorage)) {
|
|
return;
|
|
}
|
|
// Initialize localStorage and create a variable to use throughout
|
|
// the code.
|
|
localStorage = this.localStorage;
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
// Config the localStorage backend, using options set in the config.
|
|
function _initStorage(options) {
|
|
var self = this;
|
|
var dbInfo = {};
|
|
if (options) {
|
|
for (var i in options) {
|
|
dbInfo[i] = options[i];
|
|
}
|
|
}
|
|
|
|
dbInfo.keyPrefix = dbInfo.name + '/';
|
|
|
|
if (dbInfo.storeName !== self._defaultConfig.storeName) {
|
|
dbInfo.keyPrefix += dbInfo.storeName + '/';
|
|
}
|
|
|
|
self._dbInfo = dbInfo;
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(__webpack_require__(3));
|
|
}).then(function (lib) {
|
|
dbInfo.serializer = lib;
|
|
return Promise.resolve();
|
|
});
|
|
}
|
|
|
|
// Remove all keys from the datastore, effectively destroying all data in
|
|
// the app's key/value store!
|
|
function clear(callback) {
|
|
var self = this;
|
|
var promise = self.ready().then(function () {
|
|
var keyPrefix = self._dbInfo.keyPrefix;
|
|
|
|
for (var i = localStorage.length - 1; i >= 0; i--) {
|
|
var key = localStorage.key(i);
|
|
|
|
if (key.indexOf(keyPrefix) === 0) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Retrieve an item from the store. Unlike the original async_storage
|
|
// library in Gaia, we don't modify return values at all. If a key's value
|
|
// is `undefined`, we pass that value to the callback function.
|
|
function getItem(key, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var result = localStorage.getItem(dbInfo.keyPrefix + key);
|
|
|
|
// If a result was found, parse it from the serialized
|
|
// string into a JS object. If result isn't truthy, the key
|
|
// is likely undefined and we'll pass it straight to the
|
|
// callback.
|
|
if (result) {
|
|
result = dbInfo.serializer.deserialize(result);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Iterate over all items in the store.
|
|
function iterate(iterator, callback) {
|
|
var self = this;
|
|
|
|
var promise = self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var keyPrefix = dbInfo.keyPrefix;
|
|
var keyPrefixLength = keyPrefix.length;
|
|
var length = localStorage.length;
|
|
|
|
// We use a dedicated iterator instead of the `i` variable below
|
|
// so other keys we fetch in localStorage aren't counted in
|
|
// the `iterationNumber` argument passed to the `iterate()`
|
|
// callback.
|
|
//
|
|
// See: github.com/mozilla/localForage/pull/435#discussion_r38061530
|
|
var iterationNumber = 1;
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
var key = localStorage.key(i);
|
|
if (key.indexOf(keyPrefix) !== 0) {
|
|
continue;
|
|
}
|
|
var value = localStorage.getItem(key);
|
|
|
|
// If a result was found, parse it from the serialized
|
|
// string into a JS object. If result isn't truthy, the
|
|
// key is likely undefined and we'll pass it straight
|
|
// to the iterator.
|
|
if (value) {
|
|
value = dbInfo.serializer.deserialize(value);
|
|
}
|
|
|
|
value = iterator(value, key.substring(keyPrefixLength), iterationNumber++);
|
|
|
|
if (value !== void 0) {
|
|
return value;
|
|
}
|
|
}
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Same as localStorage's key() method, except takes a callback.
|
|
function key(n, callback) {
|
|
var self = this;
|
|
var promise = self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var result;
|
|
try {
|
|
result = localStorage.key(n);
|
|
} catch (error) {
|
|
result = null;
|
|
}
|
|
|
|
// Remove the prefix from the key, if a key is found.
|
|
if (result) {
|
|
result = result.substring(dbInfo.keyPrefix.length);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function keys(callback) {
|
|
var self = this;
|
|
var promise = self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
var length = localStorage.length;
|
|
var keys = [];
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
if (localStorage.key(i).indexOf(dbInfo.keyPrefix) === 0) {
|
|
keys.push(localStorage.key(i).substring(dbInfo.keyPrefix.length));
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Supply the number of keys in the datastore to the callback function.
|
|
function length(callback) {
|
|
var self = this;
|
|
var promise = self.keys().then(function (keys) {
|
|
return keys.length;
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Remove an item from the store, nice and simple.
|
|
function removeItem(key, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
localStorage.removeItem(dbInfo.keyPrefix + key);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Set a key's value and run an optional callback once the value is set.
|
|
// Unlike Gaia's implementation, the callback function is passed the value,
|
|
// in case you want to operate on that value only after you're sure it
|
|
// saved, or something like that.
|
|
function setItem(key, value, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = self.ready().then(function () {
|
|
// Convert undefined values to null.
|
|
// https://github.com/mozilla/localForage/pull/42
|
|
if (value === undefined) {
|
|
value = null;
|
|
}
|
|
|
|
// Save the original value to pass to the callback.
|
|
var originalValue = value;
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
var dbInfo = self._dbInfo;
|
|
dbInfo.serializer.serialize(value, function (value, error) {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
try {
|
|
localStorage.setItem(dbInfo.keyPrefix + key, value);
|
|
resolve(originalValue);
|
|
} catch (e) {
|
|
// localStorage capacity exceeded.
|
|
// TODO: Make this a specific error/event.
|
|
if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
|
|
reject(e);
|
|
}
|
|
reject(e);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function executeCallback(promise, callback) {
|
|
if (callback) {
|
|
promise.then(function (result) {
|
|
callback(null, result);
|
|
}, function (error) {
|
|
callback(error);
|
|
});
|
|
}
|
|
}
|
|
|
|
var localStorageWrapper = {
|
|
_driver: 'localStorageWrapper',
|
|
_initStorage: _initStorage,
|
|
// Default API, from Gaia/localStorage.
|
|
iterate: iterate,
|
|
getItem: getItem,
|
|
setItem: setItem,
|
|
removeItem: removeItem,
|
|
clear: clear,
|
|
length: length,
|
|
key: key,
|
|
keys: keys
|
|
};
|
|
|
|
exports['default'] = localStorageWrapper;
|
|
}).call(typeof window !== 'undefined' ? window : self);
|
|
module.exports = exports['default'];
|
|
|
|
/***/ },
|
|
/* 3 */
|
|
/***/ function(module, exports) {
|
|
|
|
'use strict';
|
|
|
|
exports.__esModule = true;
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Sadly, the best way to save binary data in WebSQL/localStorage is serializing
|
|
// it to Base64, so this is how we store it to prevent very strange errors with less
|
|
// verbose ways of binary <-> string data storage.
|
|
var BASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
|
|
var BLOB_TYPE_PREFIX = '~~local_forage_type~';
|
|
var BLOB_TYPE_PREFIX_REGEX = /^~~local_forage_type~([^~]+)~/;
|
|
|
|
var SERIALIZED_MARKER = '__lfsc__:';
|
|
var SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER.length;
|
|
|
|
// OMG the serializations!
|
|
var TYPE_ARRAYBUFFER = 'arbf';
|
|
var TYPE_BLOB = 'blob';
|
|
var TYPE_INT8ARRAY = 'si08';
|
|
var TYPE_UINT8ARRAY = 'ui08';
|
|
var TYPE_UINT8CLAMPEDARRAY = 'uic8';
|
|
var TYPE_INT16ARRAY = 'si16';
|
|
var TYPE_INT32ARRAY = 'si32';
|
|
var TYPE_UINT16ARRAY = 'ur16';
|
|
var TYPE_UINT32ARRAY = 'ui32';
|
|
var TYPE_FLOAT32ARRAY = 'fl32';
|
|
var TYPE_FLOAT64ARRAY = 'fl64';
|
|
var TYPE_SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER_LENGTH + TYPE_ARRAYBUFFER.length;
|
|
|
|
// Get out of our habit of using `window` inline, at least.
|
|
var globalObject = this;
|
|
|
|
// Abstracts constructing a Blob object, so it also works in older
|
|
// browsers that don't support the native Blob constructor. (i.e.
|
|
// old QtWebKit versions, at least).
|
|
function _createBlob(parts, properties) {
|
|
parts = parts || [];
|
|
properties = properties || {};
|
|
|
|
try {
|
|
return new Blob(parts, properties);
|
|
} catch (err) {
|
|
if (err.name !== 'TypeError') {
|
|
throw err;
|
|
}
|
|
|
|
var BlobBuilder = globalObject.BlobBuilder || globalObject.MSBlobBuilder || globalObject.MozBlobBuilder || globalObject.WebKitBlobBuilder;
|
|
|
|
var builder = new BlobBuilder();
|
|
for (var i = 0; i < parts.length; i += 1) {
|
|
builder.append(parts[i]);
|
|
}
|
|
|
|
return builder.getBlob(properties.type);
|
|
}
|
|
}
|
|
|
|
// Serialize a value, afterwards executing a callback (which usually
|
|
// instructs the `setItem()` callback/promise to be executed). This is how
|
|
// we store binary data with localStorage.
|
|
function serialize(value, callback) {
|
|
var valueString = '';
|
|
if (value) {
|
|
valueString = value.toString();
|
|
}
|
|
|
|
// Cannot use `value instanceof ArrayBuffer` or such here, as these
|
|
// checks fail when running the tests using casper.js...
|
|
//
|
|
// TODO: See why those tests fail and use a better solution.
|
|
if (value && (value.toString() === '[object ArrayBuffer]' || value.buffer && value.buffer.toString() === '[object ArrayBuffer]')) {
|
|
// Convert binary arrays to a string and prefix the string with
|
|
// a special marker.
|
|
var buffer;
|
|
var marker = SERIALIZED_MARKER;
|
|
|
|
if (value instanceof ArrayBuffer) {
|
|
buffer = value;
|
|
marker += TYPE_ARRAYBUFFER;
|
|
} else {
|
|
buffer = value.buffer;
|
|
|
|
if (valueString === '[object Int8Array]') {
|
|
marker += TYPE_INT8ARRAY;
|
|
} else if (valueString === '[object Uint8Array]') {
|
|
marker += TYPE_UINT8ARRAY;
|
|
} else if (valueString === '[object Uint8ClampedArray]') {
|
|
marker += TYPE_UINT8CLAMPEDARRAY;
|
|
} else if (valueString === '[object Int16Array]') {
|
|
marker += TYPE_INT16ARRAY;
|
|
} else if (valueString === '[object Uint16Array]') {
|
|
marker += TYPE_UINT16ARRAY;
|
|
} else if (valueString === '[object Int32Array]') {
|
|
marker += TYPE_INT32ARRAY;
|
|
} else if (valueString === '[object Uint32Array]') {
|
|
marker += TYPE_UINT32ARRAY;
|
|
} else if (valueString === '[object Float32Array]') {
|
|
marker += TYPE_FLOAT32ARRAY;
|
|
} else if (valueString === '[object Float64Array]') {
|
|
marker += TYPE_FLOAT64ARRAY;
|
|
} else {
|
|
callback(new Error('Failed to get type for BinaryArray'));
|
|
}
|
|
}
|
|
|
|
callback(marker + bufferToString(buffer));
|
|
} else if (valueString === '[object Blob]') {
|
|
// Conver the blob to a binaryArray and then to a string.
|
|
var fileReader = new FileReader();
|
|
|
|
fileReader.onload = function () {
|
|
// Backwards-compatible prefix for the blob type.
|
|
var str = BLOB_TYPE_PREFIX + value.type + '~' + bufferToString(this.result);
|
|
|
|
callback(SERIALIZED_MARKER + TYPE_BLOB + str);
|
|
};
|
|
|
|
fileReader.readAsArrayBuffer(value);
|
|
} else {
|
|
try {
|
|
callback(JSON.stringify(value));
|
|
} catch (e) {
|
|
console.error("Couldn't convert value into a JSON string: ", value);
|
|
|
|
callback(null, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deserialize data we've inserted into a value column/field. We place
|
|
// special markers into our strings to mark them as encoded; this isn't
|
|
// as nice as a meta field, but it's the only sane thing we can do whilst
|
|
// keeping localStorage support intact.
|
|
//
|
|
// Oftentimes this will just deserialize JSON content, but if we have a
|
|
// special marker (SERIALIZED_MARKER, defined above), we will extract
|
|
// some kind of arraybuffer/binary data/typed array out of the string.
|
|
function deserialize(value) {
|
|
// If we haven't marked this string as being specially serialized (i.e.
|
|
// something other than serialized JSON), we can just return it and be
|
|
// done with it.
|
|
if (value.substring(0, SERIALIZED_MARKER_LENGTH) !== SERIALIZED_MARKER) {
|
|
return JSON.parse(value);
|
|
}
|
|
|
|
// The following code deals with deserializing some kind of Blob or
|
|
// TypedArray. First we separate out the type of data we're dealing
|
|
// with from the data itself.
|
|
var serializedString = value.substring(TYPE_SERIALIZED_MARKER_LENGTH);
|
|
var type = value.substring(SERIALIZED_MARKER_LENGTH, TYPE_SERIALIZED_MARKER_LENGTH);
|
|
|
|
var blobType;
|
|
// Backwards-compatible blob type serialization strategy.
|
|
// DBs created with older versions of localForage will simply not have the blob type.
|
|
if (type === TYPE_BLOB && BLOB_TYPE_PREFIX_REGEX.test(serializedString)) {
|
|
var matcher = serializedString.match(BLOB_TYPE_PREFIX_REGEX);
|
|
blobType = matcher[1];
|
|
serializedString = serializedString.substring(matcher[0].length);
|
|
}
|
|
var buffer = stringToBuffer(serializedString);
|
|
|
|
// Return the right type based on the code/type set during
|
|
// serialization.
|
|
switch (type) {
|
|
case TYPE_ARRAYBUFFER:
|
|
return buffer;
|
|
case TYPE_BLOB:
|
|
return _createBlob([buffer], { type: blobType });
|
|
case TYPE_INT8ARRAY:
|
|
return new Int8Array(buffer);
|
|
case TYPE_UINT8ARRAY:
|
|
return new Uint8Array(buffer);
|
|
case TYPE_UINT8CLAMPEDARRAY:
|
|
return new Uint8ClampedArray(buffer);
|
|
case TYPE_INT16ARRAY:
|
|
return new Int16Array(buffer);
|
|
case TYPE_UINT16ARRAY:
|
|
return new Uint16Array(buffer);
|
|
case TYPE_INT32ARRAY:
|
|
return new Int32Array(buffer);
|
|
case TYPE_UINT32ARRAY:
|
|
return new Uint32Array(buffer);
|
|
case TYPE_FLOAT32ARRAY:
|
|
return new Float32Array(buffer);
|
|
case TYPE_FLOAT64ARRAY:
|
|
return new Float64Array(buffer);
|
|
default:
|
|
throw new Error('Unkown type: ' + type);
|
|
}
|
|
}
|
|
|
|
function stringToBuffer(serializedString) {
|
|
// Fill the string into a ArrayBuffer.
|
|
var bufferLength = serializedString.length * 0.75;
|
|
var len = serializedString.length;
|
|
var i;
|
|
var p = 0;
|
|
var encoded1, encoded2, encoded3, encoded4;
|
|
|
|
if (serializedString[serializedString.length - 1] === '=') {
|
|
bufferLength--;
|
|
if (serializedString[serializedString.length - 2] === '=') {
|
|
bufferLength--;
|
|
}
|
|
}
|
|
|
|
var buffer = new ArrayBuffer(bufferLength);
|
|
var bytes = new Uint8Array(buffer);
|
|
|
|
for (i = 0; i < len; i += 4) {
|
|
encoded1 = BASE_CHARS.indexOf(serializedString[i]);
|
|
encoded2 = BASE_CHARS.indexOf(serializedString[i + 1]);
|
|
encoded3 = BASE_CHARS.indexOf(serializedString[i + 2]);
|
|
encoded4 = BASE_CHARS.indexOf(serializedString[i + 3]);
|
|
|
|
/*jslint bitwise: true */
|
|
bytes[p++] = encoded1 << 2 | encoded2 >> 4;
|
|
bytes[p++] = (encoded2 & 15) << 4 | encoded3 >> 2;
|
|
bytes[p++] = (encoded3 & 3) << 6 | encoded4 & 63;
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
// Converts a buffer to a string to store, serialized, in the backend
|
|
// storage library.
|
|
function bufferToString(buffer) {
|
|
// base64-arraybuffer
|
|
var bytes = new Uint8Array(buffer);
|
|
var base64String = '';
|
|
var i;
|
|
|
|
for (i = 0; i < bytes.length; i += 3) {
|
|
/*jslint bitwise: true */
|
|
base64String += BASE_CHARS[bytes[i] >> 2];
|
|
base64String += BASE_CHARS[(bytes[i] & 3) << 4 | bytes[i + 1] >> 4];
|
|
base64String += BASE_CHARS[(bytes[i + 1] & 15) << 2 | bytes[i + 2] >> 6];
|
|
base64String += BASE_CHARS[bytes[i + 2] & 63];
|
|
}
|
|
|
|
if (bytes.length % 3 === 2) {
|
|
base64String = base64String.substring(0, base64String.length - 1) + '=';
|
|
} else if (bytes.length % 3 === 1) {
|
|
base64String = base64String.substring(0, base64String.length - 2) + '==';
|
|
}
|
|
|
|
return base64String;
|
|
}
|
|
|
|
var localforageSerializer = {
|
|
serialize: serialize,
|
|
deserialize: deserialize,
|
|
stringToBuffer: stringToBuffer,
|
|
bufferToString: bufferToString
|
|
};
|
|
|
|
exports['default'] = localforageSerializer;
|
|
}).call(typeof window !== 'undefined' ? window : self);
|
|
module.exports = exports['default'];
|
|
|
|
/***/ },
|
|
/* 4 */
|
|
/***/ function(module, exports, __webpack_require__) {
|
|
|
|
/*
|
|
* Includes code from:
|
|
*
|
|
* base64-arraybuffer
|
|
* https://github.com/niklasvh/base64-arraybuffer
|
|
*
|
|
* Copyright (c) 2012 Niklas von Hertzen
|
|
* Licensed under the MIT license.
|
|
*/
|
|
'use strict';
|
|
|
|
exports.__esModule = true;
|
|
(function () {
|
|
'use strict';
|
|
|
|
var globalObject = this;
|
|
var openDatabase = this.openDatabase;
|
|
|
|
// If WebSQL methods aren't available, we can stop now.
|
|
if (!openDatabase) {
|
|
return;
|
|
}
|
|
|
|
// Open the WebSQL database (automatically creates one if one didn't
|
|
// previously exist), using any options set in the config.
|
|
function _initStorage(options) {
|
|
var self = this;
|
|
var dbInfo = {
|
|
db: null
|
|
};
|
|
|
|
if (options) {
|
|
for (var i in options) {
|
|
dbInfo[i] = typeof options[i] !== 'string' ? options[i].toString() : options[i];
|
|
}
|
|
}
|
|
|
|
var dbInfoPromise = new Promise(function (resolve, reject) {
|
|
// Open the database; the openDatabase API will automatically
|
|
// create it for us if it doesn't exist.
|
|
try {
|
|
dbInfo.db = openDatabase(dbInfo.name, String(dbInfo.version), dbInfo.description, dbInfo.size);
|
|
} catch (e) {
|
|
return self.setDriver(self.LOCALSTORAGE).then(function () {
|
|
return self._initStorage(options);
|
|
}).then(resolve)['catch'](reject);
|
|
}
|
|
|
|
// Create our key/value table if it doesn't exist.
|
|
dbInfo.db.transaction(function (t) {
|
|
t.executeSql('CREATE TABLE IF NOT EXISTS ' + dbInfo.storeName + ' (id INTEGER PRIMARY KEY, key unique, value)', [], function () {
|
|
self._dbInfo = dbInfo;
|
|
resolve();
|
|
}, function (t, error) {
|
|
reject(error);
|
|
});
|
|
});
|
|
});
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
resolve(__webpack_require__(3));
|
|
}).then(function (lib) {
|
|
dbInfo.serializer = lib;
|
|
return dbInfoPromise;
|
|
});
|
|
}
|
|
|
|
function getItem(key, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
dbInfo.db.transaction(function (t) {
|
|
t.executeSql('SELECT * FROM ' + dbInfo.storeName + ' WHERE key = ? LIMIT 1', [key], function (t, results) {
|
|
var result = results.rows.length ? results.rows.item(0).value : null;
|
|
|
|
// Check to see if this is serialized content we need to
|
|
// unpack.
|
|
if (result) {
|
|
result = dbInfo.serializer.deserialize(result);
|
|
}
|
|
|
|
resolve(result);
|
|
}, function (t, error) {
|
|
|
|
reject(error);
|
|
});
|
|
});
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function iterate(iterator, callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
|
|
dbInfo.db.transaction(function (t) {
|
|
t.executeSql('SELECT * FROM ' + dbInfo.storeName, [], function (t, results) {
|
|
var rows = results.rows;
|
|
var length = rows.length;
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
var item = rows.item(i);
|
|
var result = item.value;
|
|
|
|
// Check to see if this is serialized content
|
|
// we need to unpack.
|
|
if (result) {
|
|
result = dbInfo.serializer.deserialize(result);
|
|
}
|
|
|
|
result = iterator(result, item.key, i + 1);
|
|
|
|
// void(0) prevents problems with redefinition
|
|
// of `undefined`.
|
|
if (result !== void 0) {
|
|
resolve(result);
|
|
return;
|
|
}
|
|
}
|
|
|
|
resolve();
|
|
}, function (t, error) {
|
|
reject(error);
|
|
});
|
|
});
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function setItem(key, value, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
// The localStorage API doesn't return undefined values in an
|
|
// "expected" way, so undefined is always cast to null in all
|
|
// drivers. See: https://github.com/mozilla/localForage/pull/42
|
|
if (value === undefined) {
|
|
value = null;
|
|
}
|
|
|
|
// Save the original value to pass to the callback.
|
|
var originalValue = value;
|
|
|
|
var dbInfo = self._dbInfo;
|
|
dbInfo.serializer.serialize(value, function (value, error) {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
dbInfo.db.transaction(function (t) {
|
|
t.executeSql('INSERT OR REPLACE INTO ' + dbInfo.storeName + ' (key, value) VALUES (?, ?)', [key, value], function () {
|
|
resolve(originalValue);
|
|
}, function (t, error) {
|
|
reject(error);
|
|
});
|
|
}, function (sqlError) {
|
|
// The transaction failed; check
|
|
// to see if it's a quota error.
|
|
if (sqlError.code === sqlError.QUOTA_ERR) {
|
|
// We reject the callback outright for now, but
|
|
// it's worth trying to re-run the transaction.
|
|
// Even if the user accepts the prompt to use
|
|
// more storage on Safari, this error will
|
|
// be called.
|
|
//
|
|
// TODO: Try to re-run the transaction.
|
|
reject(sqlError);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function removeItem(key, callback) {
|
|
var self = this;
|
|
|
|
// Cast the key to a string, as that's all we can set as a key.
|
|
if (typeof key !== 'string') {
|
|
globalObject.console.warn(key + ' used as a key, but it is not a string.');
|
|
key = String(key);
|
|
}
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
dbInfo.db.transaction(function (t) {
|
|
t.executeSql('DELETE FROM ' + dbInfo.storeName + ' WHERE key = ?', [key], function () {
|
|
resolve();
|
|
}, function (t, error) {
|
|
|
|
reject(error);
|
|
});
|
|
});
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Deletes every item in the table.
|
|
// TODO: Find out if this resets the AUTO_INCREMENT number.
|
|
function clear(callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
dbInfo.db.transaction(function (t) {
|
|
t.executeSql('DELETE FROM ' + dbInfo.storeName, [], function () {
|
|
resolve();
|
|
}, function (t, error) {
|
|
reject(error);
|
|
});
|
|
});
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Does a simple `COUNT(key)` to get the number of items stored in
|
|
// localForage.
|
|
function length(callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
dbInfo.db.transaction(function (t) {
|
|
// Ahhh, SQL makes this one soooooo easy.
|
|
t.executeSql('SELECT COUNT(key) as c FROM ' + dbInfo.storeName, [], function (t, results) {
|
|
var result = results.rows.item(0).c;
|
|
|
|
resolve(result);
|
|
}, function (t, error) {
|
|
|
|
reject(error);
|
|
});
|
|
});
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
// Return the key located at key index X; essentially gets the key from a
|
|
// `WHERE id = ?`. This is the most efficient way I can think to implement
|
|
// this rarely-used (in my experience) part of the API, but it can seem
|
|
// inconsistent, because we do `INSERT OR REPLACE INTO` on `setItem()`, so
|
|
// the ID of each key will change every time it's updated. Perhaps a stored
|
|
// procedure for the `setItem()` SQL would solve this problem?
|
|
// TODO: Don't change ID on `setItem()`.
|
|
function key(n, callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
dbInfo.db.transaction(function (t) {
|
|
t.executeSql('SELECT key FROM ' + dbInfo.storeName + ' WHERE id = ? LIMIT 1', [n + 1], function (t, results) {
|
|
var result = results.rows.length ? results.rows.item(0).key : null;
|
|
resolve(result);
|
|
}, function (t, error) {
|
|
reject(error);
|
|
});
|
|
});
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function keys(callback) {
|
|
var self = this;
|
|
|
|
var promise = new Promise(function (resolve, reject) {
|
|
self.ready().then(function () {
|
|
var dbInfo = self._dbInfo;
|
|
dbInfo.db.transaction(function (t) {
|
|
t.executeSql('SELECT key FROM ' + dbInfo.storeName, [], function (t, results) {
|
|
var keys = [];
|
|
|
|
for (var i = 0; i < results.rows.length; i++) {
|
|
keys.push(results.rows.item(i).key);
|
|
}
|
|
|
|
resolve(keys);
|
|
}, function (t, error) {
|
|
|
|
reject(error);
|
|
});
|
|
});
|
|
})['catch'](reject);
|
|
});
|
|
|
|
executeCallback(promise, callback);
|
|
return promise;
|
|
}
|
|
|
|
function executeCallback(promise, callback) {
|
|
if (callback) {
|
|
promise.then(function (result) {
|
|
callback(null, result);
|
|
}, function (error) {
|
|
callback(error);
|
|
});
|
|
}
|
|
}
|
|
|
|
var webSQLStorage = {
|
|
_driver: 'webSQLStorage',
|
|
_initStorage: _initStorage,
|
|
iterate: iterate,
|
|
getItem: getItem,
|
|
setItem: setItem,
|
|
removeItem: removeItem,
|
|
clear: clear,
|
|
length: length,
|
|
key: key,
|
|
keys: keys
|
|
};
|
|
|
|
exports['default'] = webSQLStorage;
|
|
}).call(typeof window !== 'undefined' ? window : self);
|
|
module.exports = exports['default'];
|
|
|
|
/***/ }
|
|
/******/ ])
|
|
});
|
|
;
|
|
},{"__browserify_process":4}],19:[function(require,module,exports){
|
|
// Underscore.js 1.4.4
|
|
// http://underscorejs.org
|
|
// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
|
|
// Underscore may be freely distributed under the MIT license.
|
|
|
|
(function() {
|
|
|
|
// Baseline setup
|
|
// --------------
|
|
|
|
// Establish the root object, `window` in the browser, or `global` on the server.
|
|
var root = this;
|
|
|
|
// Save the previous value of the `_` variable.
|
|
var previousUnderscore = root._;
|
|
|
|
// Establish the object that gets returned to break out of a loop iteration.
|
|
var breaker = {};
|
|
|
|
// Save bytes in the minified (but not gzipped) version:
|
|
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
|
|
|
|
// Create quick reference variables for speed access to core prototypes.
|
|
var push = ArrayProto.push,
|
|
slice = ArrayProto.slice,
|
|
concat = ArrayProto.concat,
|
|
toString = ObjProto.toString,
|
|
hasOwnProperty = ObjProto.hasOwnProperty;
|
|
|
|
// All **ECMAScript 5** native function implementations that we hope to use
|
|
// are declared here.
|
|
var
|
|
nativeForEach = ArrayProto.forEach,
|
|
nativeMap = ArrayProto.map,
|
|
nativeReduce = ArrayProto.reduce,
|
|
nativeReduceRight = ArrayProto.reduceRight,
|
|
nativeFilter = ArrayProto.filter,
|
|
nativeEvery = ArrayProto.every,
|
|
nativeSome = ArrayProto.some,
|
|
nativeIndexOf = ArrayProto.indexOf,
|
|
nativeLastIndexOf = ArrayProto.lastIndexOf,
|
|
nativeIsArray = Array.isArray,
|
|
nativeKeys = Object.keys,
|
|
nativeBind = FuncProto.bind;
|
|
|
|
// Create a safe reference to the Underscore object for use below.
|
|
var _ = function(obj) {
|
|
if (obj instanceof _) return obj;
|
|
if (!(this instanceof _)) return new _(obj);
|
|
this._wrapped = obj;
|
|
};
|
|
|
|
// Export the Underscore object for **Node.js**, with
|
|
// backwards-compatibility for the old `require()` API. If we're in
|
|
// the browser, add `_` as a global object via a string identifier,
|
|
// for Closure Compiler "advanced" mode.
|
|
if (typeof exports !== 'undefined') {
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
exports = module.exports = _;
|
|
}
|
|
exports._ = _;
|
|
} else {
|
|
root._ = _;
|
|
}
|
|
|
|
// Current version.
|
|
_.VERSION = '1.4.4';
|
|
|
|
// Collection Functions
|
|
// --------------------
|
|
|
|
// The cornerstone, an `each` implementation, aka `forEach`.
|
|
// Handles objects with the built-in `forEach`, arrays, and raw objects.
|
|
// Delegates to **ECMAScript 5**'s native `forEach` if available.
|
|
var each = _.each = _.forEach = function(obj, iterator, context) {
|
|
if (obj == null) return;
|
|
if (nativeForEach && obj.forEach === nativeForEach) {
|
|
obj.forEach(iterator, context);
|
|
} else if (obj.length === +obj.length) {
|
|
for (var i = 0, l = obj.length; i < l; i++) {
|
|
if (iterator.call(context, obj[i], i, obj) === breaker) return;
|
|
}
|
|
} else {
|
|
for (var key in obj) {
|
|
if (_.has(obj, key)) {
|
|
if (iterator.call(context, obj[key], key, obj) === breaker) return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Return the results of applying the iterator to each element.
|
|
// Delegates to **ECMAScript 5**'s native `map` if available.
|
|
_.map = _.collect = function(obj, iterator, context) {
|
|
var results = [];
|
|
if (obj == null) return results;
|
|
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
|
|
each(obj, function(value, index, list) {
|
|
results[results.length] = iterator.call(context, value, index, list);
|
|
});
|
|
return results;
|
|
};
|
|
|
|
var reduceError = 'Reduce of empty array with no initial value';
|
|
|
|
// **Reduce** builds up a single result from a list of values, aka `inject`,
|
|
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
|
|
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
|
|
var initial = arguments.length > 2;
|
|
if (obj == null) obj = [];
|
|
if (nativeReduce && obj.reduce === nativeReduce) {
|
|
if (context) iterator = _.bind(iterator, context);
|
|
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
|
|
}
|
|
each(obj, function(value, index, list) {
|
|
if (!initial) {
|
|
memo = value;
|
|
initial = true;
|
|
} else {
|
|
memo = iterator.call(context, memo, value, index, list);
|
|
}
|
|
});
|
|
if (!initial) throw new TypeError(reduceError);
|
|
return memo;
|
|
};
|
|
|
|
// The right-associative version of reduce, also known as `foldr`.
|
|
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
|
|
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
|
|
var initial = arguments.length > 2;
|
|
if (obj == null) obj = [];
|
|
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
|
|
if (context) iterator = _.bind(iterator, context);
|
|
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
|
|
}
|
|
var length = obj.length;
|
|
if (length !== +length) {
|
|
var keys = _.keys(obj);
|
|
length = keys.length;
|
|
}
|
|
each(obj, function(value, index, list) {
|
|
index = keys ? keys[--length] : --length;
|
|
if (!initial) {
|
|
memo = obj[index];
|
|
initial = true;
|
|
} else {
|
|
memo = iterator.call(context, memo, obj[index], index, list);
|
|
}
|
|
});
|
|
if (!initial) throw new TypeError(reduceError);
|
|
return memo;
|
|
};
|
|
|
|
// Return the first value which passes a truth test. Aliased as `detect`.
|
|
_.find = _.detect = function(obj, iterator, context) {
|
|
var result;
|
|
any(obj, function(value, index, list) {
|
|
if (iterator.call(context, value, index, list)) {
|
|
result = value;
|
|
return true;
|
|
}
|
|
});
|
|
return result;
|
|
};
|
|
|
|
// Return all the elements that pass a truth test.
|
|
// Delegates to **ECMAScript 5**'s native `filter` if available.
|
|
// Aliased as `select`.
|
|
_.filter = _.select = function(obj, iterator, context) {
|
|
var results = [];
|
|
if (obj == null) return results;
|
|
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
|
|
each(obj, function(value, index, list) {
|
|
if (iterator.call(context, value, index, list)) results[results.length] = value;
|
|
});
|
|
return results;
|
|
};
|
|
|
|
// Return all the elements for which a truth test fails.
|
|
_.reject = function(obj, iterator, context) {
|
|
return _.filter(obj, function(value, index, list) {
|
|
return !iterator.call(context, value, index, list);
|
|
}, context);
|
|
};
|
|
|
|
// Determine whether all of the elements match a truth test.
|
|
// Delegates to **ECMAScript 5**'s native `every` if available.
|
|
// Aliased as `all`.
|
|
_.every = _.all = function(obj, iterator, context) {
|
|
iterator || (iterator = _.identity);
|
|
var result = true;
|
|
if (obj == null) return result;
|
|
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
|
|
each(obj, function(value, index, list) {
|
|
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
|
|
});
|
|
return !!result;
|
|
};
|
|
|
|
// Determine if at least one element in the object matches a truth test.
|
|
// Delegates to **ECMAScript 5**'s native `some` if available.
|
|
// Aliased as `any`.
|
|
var any = _.some = _.any = function(obj, iterator, context) {
|
|
iterator || (iterator = _.identity);
|
|
var result = false;
|
|
if (obj == null) return result;
|
|
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
|
|
each(obj, function(value, index, list) {
|
|
if (result || (result = iterator.call(context, value, index, list))) return breaker;
|
|
});
|
|
return !!result;
|
|
};
|
|
|
|
// Determine if the array or object contains a given value (using `===`).
|
|
// Aliased as `include`.
|
|
_.contains = _.include = function(obj, target) {
|
|
if (obj == null) return false;
|
|
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
|
|
return any(obj, function(value) {
|
|
return value === target;
|
|
});
|
|
};
|
|
|
|
// Invoke a method (with arguments) on every item in a collection.
|
|
_.invoke = function(obj, method) {
|
|
var args = slice.call(arguments, 2);
|
|
var isFunc = _.isFunction(method);
|
|
return _.map(obj, function(value) {
|
|
return (isFunc ? method : value[method]).apply(value, args);
|
|
});
|
|
};
|
|
|
|
// Convenience version of a common use case of `map`: fetching a property.
|
|
_.pluck = function(obj, key) {
|
|
return _.map(obj, function(value){ return value[key]; });
|
|
};
|
|
|
|
// Convenience version of a common use case of `filter`: selecting only objects
|
|
// containing specific `key:value` pairs.
|
|
_.where = function(obj, attrs, first) {
|
|
if (_.isEmpty(attrs)) return first ? null : [];
|
|
return _[first ? 'find' : 'filter'](obj, function(value) {
|
|
for (var key in attrs) {
|
|
if (attrs[key] !== value[key]) return false;
|
|
}
|
|
return true;
|
|
});
|
|
};
|
|
|
|
// Convenience version of a common use case of `find`: getting the first object
|
|
// containing specific `key:value` pairs.
|
|
_.findWhere = function(obj, attrs) {
|
|
return _.where(obj, attrs, true);
|
|
};
|
|
|
|
// Return the maximum element or (element-based computation).
|
|
// Can't optimize arrays of integers longer than 65,535 elements.
|
|
// See: https://bugs.webkit.org/show_bug.cgi?id=80797
|
|
_.max = function(obj, iterator, context) {
|
|
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
|
|
return Math.max.apply(Math, obj);
|
|
}
|
|
if (!iterator && _.isEmpty(obj)) return -Infinity;
|
|
var result = {computed : -Infinity, value: -Infinity};
|
|
each(obj, function(value, index, list) {
|
|
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
|
computed >= result.computed && (result = {value : value, computed : computed});
|
|
});
|
|
return result.value;
|
|
};
|
|
|
|
// Return the minimum element (or element-based computation).
|
|
_.min = function(obj, iterator, context) {
|
|
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
|
|
return Math.min.apply(Math, obj);
|
|
}
|
|
if (!iterator && _.isEmpty(obj)) return Infinity;
|
|
var result = {computed : Infinity, value: Infinity};
|
|
each(obj, function(value, index, list) {
|
|
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
|
computed < result.computed && (result = {value : value, computed : computed});
|
|
});
|
|
return result.value;
|
|
};
|
|
|
|
// Shuffle an array.
|
|
_.shuffle = function(obj) {
|
|
var rand;
|
|
var index = 0;
|
|
var shuffled = [];
|
|
each(obj, function(value) {
|
|
rand = _.random(index++);
|
|
shuffled[index - 1] = shuffled[rand];
|
|
shuffled[rand] = value;
|
|
});
|
|
return shuffled;
|
|
};
|
|
|
|
// An internal function to generate lookup iterators.
|
|
var lookupIterator = function(value) {
|
|
return _.isFunction(value) ? value : function(obj){ return obj[value]; };
|
|
};
|
|
|
|
// Sort the object's values by a criterion produced by an iterator.
|
|
_.sortBy = function(obj, value, context) {
|
|
var iterator = lookupIterator(value);
|
|
return _.pluck(_.map(obj, function(value, index, list) {
|
|
return {
|
|
value : value,
|
|
index : index,
|
|
criteria : iterator.call(context, value, index, list)
|
|
};
|
|
}).sort(function(left, right) {
|
|
var a = left.criteria;
|
|
var b = right.criteria;
|
|
if (a !== b) {
|
|
if (a > b || a === void 0) return 1;
|
|
if (a < b || b === void 0) return -1;
|
|
}
|
|
return left.index < right.index ? -1 : 1;
|
|
}), 'value');
|
|
};
|
|
|
|
// An internal function used for aggregate "group by" operations.
|
|
var group = function(obj, value, context, behavior) {
|
|
var result = {};
|
|
var iterator = lookupIterator(value || _.identity);
|
|
each(obj, function(value, index) {
|
|
var key = iterator.call(context, value, index, obj);
|
|
behavior(result, key, value);
|
|
});
|
|
return result;
|
|
};
|
|
|
|
// Groups the object's values by a criterion. Pass either a string attribute
|
|
// to group by, or a function that returns the criterion.
|
|
_.groupBy = function(obj, value, context) {
|
|
return group(obj, value, context, function(result, key, value) {
|
|
(_.has(result, key) ? result[key] : (result[key] = [])).push(value);
|
|
});
|
|
};
|
|
|
|
// Counts instances of an object that group by a certain criterion. Pass
|
|
// either a string attribute to count by, or a function that returns the
|
|
// criterion.
|
|
_.countBy = function(obj, value, context) {
|
|
return group(obj, value, context, function(result, key) {
|
|
if (!_.has(result, key)) result[key] = 0;
|
|
result[key]++;
|
|
});
|
|
};
|
|
|
|
// Use a comparator function to figure out the smallest index at which
|
|
// an object should be inserted so as to maintain order. Uses binary search.
|
|
_.sortedIndex = function(array, obj, iterator, context) {
|
|
iterator = iterator == null ? _.identity : lookupIterator(iterator);
|
|
var value = iterator.call(context, obj);
|
|
var low = 0, high = array.length;
|
|
while (low < high) {
|
|
var mid = (low + high) >>> 1;
|
|
iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
|
|
}
|
|
return low;
|
|
};
|
|
|
|
// Safely convert anything iterable into a real, live array.
|
|
_.toArray = function(obj) {
|
|
if (!obj) return [];
|
|
if (_.isArray(obj)) return slice.call(obj);
|
|
if (obj.length === +obj.length) return _.map(obj, _.identity);
|
|
return _.values(obj);
|
|
};
|
|
|
|
// Return the number of elements in an object.
|
|
_.size = function(obj) {
|
|
if (obj == null) return 0;
|
|
return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
|
|
};
|
|
|
|
// Array Functions
|
|
// ---------------
|
|
|
|
// Get the first element of an array. Passing **n** will return the first N
|
|
// values in the array. Aliased as `head` and `take`. The **guard** check
|
|
// allows it to work with `_.map`.
|
|
_.first = _.head = _.take = function(array, n, guard) {
|
|
if (array == null) return void 0;
|
|
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
|
|
};
|
|
|
|
// Returns everything but the last entry of the array. Especially useful on
|
|
// the arguments object. Passing **n** will return all the values in
|
|
// the array, excluding the last N. The **guard** check allows it to work with
|
|
// `_.map`.
|
|
_.initial = function(array, n, guard) {
|
|
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
|
|
};
|
|
|
|
// Get the last element of an array. Passing **n** will return the last N
|
|
// values in the array. The **guard** check allows it to work with `_.map`.
|
|
_.last = function(array, n, guard) {
|
|
if (array == null) return void 0;
|
|
if ((n != null) && !guard) {
|
|
return slice.call(array, Math.max(array.length - n, 0));
|
|
} else {
|
|
return array[array.length - 1];
|
|
}
|
|
};
|
|
|
|
// Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
|
|
// Especially useful on the arguments object. Passing an **n** will return
|
|
// the rest N values in the array. The **guard**
|
|
// check allows it to work with `_.map`.
|
|
_.rest = _.tail = _.drop = function(array, n, guard) {
|
|
return slice.call(array, (n == null) || guard ? 1 : n);
|
|
};
|
|
|
|
// Trim out all falsy values from an array.
|
|
_.compact = function(array) {
|
|
return _.filter(array, _.identity);
|
|
};
|
|
|
|
// Internal implementation of a recursive `flatten` function.
|
|
var flatten = function(input, shallow, output) {
|
|
each(input, function(value) {
|
|
if (_.isArray(value)) {
|
|
shallow ? push.apply(output, value) : flatten(value, shallow, output);
|
|
} else {
|
|
output.push(value);
|
|
}
|
|
});
|
|
return output;
|
|
};
|
|
|
|
// Return a completely flattened version of an array.
|
|
_.flatten = function(array, shallow) {
|
|
return flatten(array, shallow, []);
|
|
};
|
|
|
|
// Return a version of the array that does not contain the specified value(s).
|
|
_.without = function(array) {
|
|
return _.difference(array, slice.call(arguments, 1));
|
|
};
|
|
|
|
// Produce a duplicate-free version of the array. If the array has already
|
|
// been sorted, you have the option of using a faster algorithm.
|
|
// Aliased as `unique`.
|
|
_.uniq = _.unique = function(array, isSorted, iterator, context) {
|
|
if (_.isFunction(isSorted)) {
|
|
context = iterator;
|
|
iterator = isSorted;
|
|
isSorted = false;
|
|
}
|
|
var initial = iterator ? _.map(array, iterator, context) : array;
|
|
var results = [];
|
|
var seen = [];
|
|
each(initial, function(value, index) {
|
|
if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
|
|
seen.push(value);
|
|
results.push(array[index]);
|
|
}
|
|
});
|
|
return results;
|
|
};
|
|
|
|
// Produce an array that contains the union: each distinct element from all of
|
|
// the passed-in arrays.
|
|
_.union = function() {
|
|
return _.uniq(concat.apply(ArrayProto, arguments));
|
|
};
|
|
|
|
// Produce an array that contains every item shared between all the
|
|
// passed-in arrays.
|
|
_.intersection = function(array) {
|
|
var rest = slice.call(arguments, 1);
|
|
return _.filter(_.uniq(array), function(item) {
|
|
return _.every(rest, function(other) {
|
|
return _.indexOf(other, item) >= 0;
|
|
});
|
|
});
|
|
};
|
|
|
|
// Take the difference between one array and a number of other arrays.
|
|
// Only the elements present in just the first array will remain.
|
|
_.difference = function(array) {
|
|
var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
|
|
return _.filter(array, function(value){ return !_.contains(rest, value); });
|
|
};
|
|
|
|
// Zip together multiple lists into a single array -- elements that share
|
|
// an index go together.
|
|
_.zip = function() {
|
|
var args = slice.call(arguments);
|
|
var length = _.max(_.pluck(args, 'length'));
|
|
var results = new Array(length);
|
|
for (var i = 0; i < length; i++) {
|
|
results[i] = _.pluck(args, "" + i);
|
|
}
|
|
return results;
|
|
};
|
|
|
|
// Converts lists into objects. Pass either a single array of `[key, value]`
|
|
// pairs, or two parallel arrays of the same length -- one of keys, and one of
|
|
// the corresponding values.
|
|
_.object = function(list, values) {
|
|
if (list == null) return {};
|
|
var result = {};
|
|
for (var i = 0, l = list.length; i < l; i++) {
|
|
if (values) {
|
|
result[list[i]] = values[i];
|
|
} else {
|
|
result[list[i][0]] = list[i][1];
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
|
|
// we need this function. Return the position of the first occurrence of an
|
|
// item in an array, or -1 if the item is not included in the array.
|
|
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
|
|
// If the array is large and already in sort order, pass `true`
|
|
// for **isSorted** to use binary search.
|
|
_.indexOf = function(array, item, isSorted) {
|
|
if (array == null) return -1;
|
|
var i = 0, l = array.length;
|
|
if (isSorted) {
|
|
if (typeof isSorted == 'number') {
|
|
i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
|
|
} else {
|
|
i = _.sortedIndex(array, item);
|
|
return array[i] === item ? i : -1;
|
|
}
|
|
}
|
|
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
|
|
for (; i < l; i++) if (array[i] === item) return i;
|
|
return -1;
|
|
};
|
|
|
|
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
|
|
_.lastIndexOf = function(array, item, from) {
|
|
if (array == null) return -1;
|
|
var hasIndex = from != null;
|
|
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
|
|
return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
|
|
}
|
|
var i = (hasIndex ? from : array.length);
|
|
while (i--) if (array[i] === item) return i;
|
|
return -1;
|
|
};
|
|
|
|
// Generate an integer Array containing an arithmetic progression. A port of
|
|
// the native Python `range()` function. See
|
|
// [the Python documentation](http://docs.python.org/library/functions.html#range).
|
|
_.range = function(start, stop, step) {
|
|
if (arguments.length <= 1) {
|
|
stop = start || 0;
|
|
start = 0;
|
|
}
|
|
step = arguments[2] || 1;
|
|
|
|
var len = Math.max(Math.ceil((stop - start) / step), 0);
|
|
var idx = 0;
|
|
var range = new Array(len);
|
|
|
|
while(idx < len) {
|
|
range[idx++] = start;
|
|
start += step;
|
|
}
|
|
|
|
return range;
|
|
};
|
|
|
|
// Function (ahem) Functions
|
|
// ------------------
|
|
|
|
// Create a function bound to a given object (assigning `this`, and arguments,
|
|
// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
|
|
// available.
|
|
_.bind = function(func, context) {
|
|
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
|
|
var args = slice.call(arguments, 2);
|
|
return function() {
|
|
return func.apply(context, args.concat(slice.call(arguments)));
|
|
};
|
|
};
|
|
|
|
// Partially apply a function by creating a version that has had some of its
|
|
// arguments pre-filled, without changing its dynamic `this` context.
|
|
_.partial = function(func) {
|
|
var args = slice.call(arguments, 1);
|
|
return function() {
|
|
return func.apply(this, args.concat(slice.call(arguments)));
|
|
};
|
|
};
|
|
|
|
// Bind all of an object's methods to that object. Useful for ensuring that
|
|
// all callbacks defined on an object belong to it.
|
|
_.bindAll = function(obj) {
|
|
var funcs = slice.call(arguments, 1);
|
|
if (funcs.length === 0) funcs = _.functions(obj);
|
|
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
|
|
return obj;
|
|
};
|
|
|
|
// Memoize an expensive function by storing its results.
|
|
_.memoize = function(func, hasher) {
|
|
var memo = {};
|
|
hasher || (hasher = _.identity);
|
|
return function() {
|
|
var key = hasher.apply(this, arguments);
|
|
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
|
|
};
|
|
};
|
|
|
|
// Delays a function for the given number of milliseconds, and then calls
|
|
// it with the arguments supplied.
|
|
_.delay = function(func, wait) {
|
|
var args = slice.call(arguments, 2);
|
|
return setTimeout(function(){ return func.apply(null, args); }, wait);
|
|
};
|
|
|
|
// Defers a function, scheduling it to run after the current call stack has
|
|
// cleared.
|
|
_.defer = function(func) {
|
|
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
|
|
};
|
|
|
|
// Returns a function, that, when invoked, will only be triggered at most once
|
|
// during a given window of time.
|
|
_.throttle = function(func, wait) {
|
|
var context, args, timeout, result;
|
|
var previous = 0;
|
|
var later = function() {
|
|
previous = new Date;
|
|
timeout = null;
|
|
result = func.apply(context, args);
|
|
};
|
|
return function() {
|
|
var now = new Date;
|
|
var remaining = wait - (now - previous);
|
|
context = this;
|
|
args = arguments;
|
|
if (remaining <= 0) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
previous = now;
|
|
result = func.apply(context, args);
|
|
} else if (!timeout) {
|
|
timeout = setTimeout(later, remaining);
|
|
}
|
|
return result;
|
|
};
|
|
};
|
|
|
|
// Returns a function, that, as long as it continues to be invoked, will not
|
|
// be triggered. The function will be called after it stops being called for
|
|
// N milliseconds. If `immediate` is passed, trigger the function on the
|
|
// leading edge, instead of the trailing.
|
|
_.debounce = function(func, wait, immediate) {
|
|
var timeout, result;
|
|
return function() {
|
|
var context = this, args = arguments;
|
|
var later = function() {
|
|
timeout = null;
|
|
if (!immediate) result = func.apply(context, args);
|
|
};
|
|
var callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
if (callNow) result = func.apply(context, args);
|
|
return result;
|
|
};
|
|
};
|
|
|
|
// Returns a function that will be executed at most one time, no matter how
|
|
// often you call it. Useful for lazy initialization.
|
|
_.once = function(func) {
|
|
var ran = false, memo;
|
|
return function() {
|
|
if (ran) return memo;
|
|
ran = true;
|
|
memo = func.apply(this, arguments);
|
|
func = null;
|
|
return memo;
|
|
};
|
|
};
|
|
|
|
// Returns the first function passed as an argument to the second,
|
|
// allowing you to adjust arguments, run code before and after, and
|
|
// conditionally execute the original function.
|
|
_.wrap = function(func, wrapper) {
|
|
return function() {
|
|
var args = [func];
|
|
push.apply(args, arguments);
|
|
return wrapper.apply(this, args);
|
|
};
|
|
};
|
|
|
|
// Returns a function that is the composition of a list of functions, each
|
|
// consuming the return value of the function that follows.
|
|
_.compose = function() {
|
|
var funcs = arguments;
|
|
return function() {
|
|
var args = arguments;
|
|
for (var i = funcs.length - 1; i >= 0; i--) {
|
|
args = [funcs[i].apply(this, args)];
|
|
}
|
|
return args[0];
|
|
};
|
|
};
|
|
|
|
// Returns a function that will only be executed after being called N times.
|
|
_.after = function(times, func) {
|
|
if (times <= 0) return func();
|
|
return function() {
|
|
if (--times < 1) {
|
|
return func.apply(this, arguments);
|
|
}
|
|
};
|
|
};
|
|
|
|
// Object Functions
|
|
// ----------------
|
|
|
|
// Retrieve the names of an object's properties.
|
|
// Delegates to **ECMAScript 5**'s native `Object.keys`
|
|
_.keys = nativeKeys || function(obj) {
|
|
if (obj !== Object(obj)) throw new TypeError('Invalid object');
|
|
var keys = [];
|
|
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
|
|
return keys;
|
|
};
|
|
|
|
// Retrieve the values of an object's properties.
|
|
_.values = function(obj) {
|
|
var values = [];
|
|
for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
|
|
return values;
|
|
};
|
|
|
|
// Convert an object into a list of `[key, value]` pairs.
|
|
_.pairs = function(obj) {
|
|
var pairs = [];
|
|
for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
|
|
return pairs;
|
|
};
|
|
|
|
// Invert the keys and values of an object. The values must be serializable.
|
|
_.invert = function(obj) {
|
|
var result = {};
|
|
for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
|
|
return result;
|
|
};
|
|
|
|
// Return a sorted list of the function names available on the object.
|
|
// Aliased as `methods`
|
|
_.functions = _.methods = function(obj) {
|
|
var names = [];
|
|
for (var key in obj) {
|
|
if (_.isFunction(obj[key])) names.push(key);
|
|
}
|
|
return names.sort();
|
|
};
|
|
|
|
// Extend a given object with all the properties in passed-in object(s).
|
|
_.extend = function(obj) {
|
|
each(slice.call(arguments, 1), function(source) {
|
|
if (source) {
|
|
for (var prop in source) {
|
|
obj[prop] = source[prop];
|
|
}
|
|
}
|
|
});
|
|
return obj;
|
|
};
|
|
|
|
// Return a copy of the object only containing the whitelisted properties.
|
|
_.pick = function(obj) {
|
|
var copy = {};
|
|
var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
|
|
each(keys, function(key) {
|
|
if (key in obj) copy[key] = obj[key];
|
|
});
|
|
return copy;
|
|
};
|
|
|
|
// Return a copy of the object without the blacklisted properties.
|
|
_.omit = function(obj) {
|
|
var copy = {};
|
|
var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
|
|
for (var key in obj) {
|
|
if (!_.contains(keys, key)) copy[key] = obj[key];
|
|
}
|
|
return copy;
|
|
};
|
|
|
|
// Fill in a given object with default properties.
|
|
_.defaults = function(obj) {
|
|
each(slice.call(arguments, 1), function(source) {
|
|
if (source) {
|
|
for (var prop in source) {
|
|
if (obj[prop] == null) obj[prop] = source[prop];
|
|
}
|
|
}
|
|
});
|
|
return obj;
|
|
};
|
|
|
|
// Create a (shallow-cloned) duplicate of an object.
|
|
_.clone = function(obj) {
|
|
if (!_.isObject(obj)) return obj;
|
|
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
|
|
};
|
|
|
|
// Invokes interceptor with the obj, and then returns obj.
|
|
// The primary purpose of this method is to "tap into" a method chain, in
|
|
// order to perform operations on intermediate results within the chain.
|
|
_.tap = function(obj, interceptor) {
|
|
interceptor(obj);
|
|
return obj;
|
|
};
|
|
|
|
// Internal recursive comparison function for `isEqual`.
|
|
var eq = function(a, b, aStack, bStack) {
|
|
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
|
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
|
|
if (a === b) return a !== 0 || 1 / a == 1 / b;
|
|
// A strict comparison is necessary because `null == undefined`.
|
|
if (a == null || b == null) return a === b;
|
|
// Unwrap any wrapped objects.
|
|
if (a instanceof _) a = a._wrapped;
|
|
if (b instanceof _) b = b._wrapped;
|
|
// Compare `[[Class]]` names.
|
|
var className = toString.call(a);
|
|
if (className != toString.call(b)) return false;
|
|
switch (className) {
|
|
// Strings, numbers, dates, and booleans are compared by value.
|
|
case '[object String]':
|
|
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
|
|
// equivalent to `new String("5")`.
|
|
return a == String(b);
|
|
case '[object Number]':
|
|
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
|
|
// other numeric values.
|
|
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
|
|
case '[object Date]':
|
|
case '[object Boolean]':
|
|
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
|
|
// millisecond representations. Note that invalid dates with millisecond representations
|
|
// of `NaN` are not equivalent.
|
|
return +a == +b;
|
|
// RegExps are compared by their source patterns and flags.
|
|
case '[object RegExp]':
|
|
return a.source == b.source &&
|
|
a.global == b.global &&
|
|
a.multiline == b.multiline &&
|
|
a.ignoreCase == b.ignoreCase;
|
|
}
|
|
if (typeof a != 'object' || typeof b != 'object') return false;
|
|
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
|
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
|
var length = aStack.length;
|
|
while (length--) {
|
|
// Linear search. Performance is inversely proportional to the number of
|
|
// unique nested structures.
|
|
if (aStack[length] == a) return bStack[length] == b;
|
|
}
|
|
// Add the first object to the stack of traversed objects.
|
|
aStack.push(a);
|
|
bStack.push(b);
|
|
var size = 0, result = true;
|
|
// Recursively compare objects and arrays.
|
|
if (className == '[object Array]') {
|
|
// Compare array lengths to determine if a deep comparison is necessary.
|
|
size = a.length;
|
|
result = size == b.length;
|
|
if (result) {
|
|
// Deep compare the contents, ignoring non-numeric properties.
|
|
while (size--) {
|
|
if (!(result = eq(a[size], b[size], aStack, bStack))) break;
|
|
}
|
|
}
|
|
} else {
|
|
// Objects with different constructors are not equivalent, but `Object`s
|
|
// from different frames are.
|
|
var aCtor = a.constructor, bCtor = b.constructor;
|
|
if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
|
|
_.isFunction(bCtor) && (bCtor instanceof bCtor))) {
|
|
return false;
|
|
}
|
|
// Deep compare objects.
|
|
for (var key in a) {
|
|
if (_.has(a, key)) {
|
|
// Count the expected number of properties.
|
|
size++;
|
|
// Deep compare each member.
|
|
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
|
|
}
|
|
}
|
|
// Ensure that both objects contain the same number of properties.
|
|
if (result) {
|
|
for (key in b) {
|
|
if (_.has(b, key) && !(size--)) break;
|
|
}
|
|
result = !size;
|
|
}
|
|
}
|
|
// Remove the first object from the stack of traversed objects.
|
|
aStack.pop();
|
|
bStack.pop();
|
|
return result;
|
|
};
|
|
|
|
// Perform a deep comparison to check if two objects are equal.
|
|
_.isEqual = function(a, b) {
|
|
return eq(a, b, [], []);
|
|
};
|
|
|
|
// Is a given array, string, or object empty?
|
|
// An "empty" object has no enumerable own-properties.
|
|
_.isEmpty = function(obj) {
|
|
if (obj == null) return true;
|
|
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
|
|
for (var key in obj) if (_.has(obj, key)) return false;
|
|
return true;
|
|
};
|
|
|
|
// Is a given value a DOM element?
|
|
_.isElement = function(obj) {
|
|
return !!(obj && obj.nodeType === 1);
|
|
};
|
|
|
|
// Is a given value an array?
|
|
// Delegates to ECMA5's native Array.isArray
|
|
_.isArray = nativeIsArray || function(obj) {
|
|
return toString.call(obj) == '[object Array]';
|
|
};
|
|
|
|
// Is a given variable an object?
|
|
_.isObject = function(obj) {
|
|
return obj === Object(obj);
|
|
};
|
|
|
|
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
|
|
each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
|
|
_['is' + name] = function(obj) {
|
|
return toString.call(obj) == '[object ' + name + ']';
|
|
};
|
|
});
|
|
|
|
// Define a fallback version of the method in browsers (ahem, IE), where
|
|
// there isn't any inspectable "Arguments" type.
|
|
if (!_.isArguments(arguments)) {
|
|
_.isArguments = function(obj) {
|
|
return !!(obj && _.has(obj, 'callee'));
|
|
};
|
|
}
|
|
|
|
// Optimize `isFunction` if appropriate.
|
|
if (typeof (/./) !== 'function') {
|
|
_.isFunction = function(obj) {
|
|
return typeof obj === 'function';
|
|
};
|
|
}
|
|
|
|
// Is a given object a finite number?
|
|
_.isFinite = function(obj) {
|
|
return isFinite(obj) && !isNaN(parseFloat(obj));
|
|
};
|
|
|
|
// Is the given value `NaN`? (NaN is the only number which does not equal itself).
|
|
_.isNaN = function(obj) {
|
|
return _.isNumber(obj) && obj != +obj;
|
|
};
|
|
|
|
// Is a given value a boolean?
|
|
_.isBoolean = function(obj) {
|
|
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
|
|
};
|
|
|
|
// Is a given value equal to null?
|
|
_.isNull = function(obj) {
|
|
return obj === null;
|
|
};
|
|
|
|
// Is a given variable undefined?
|
|
_.isUndefined = function(obj) {
|
|
return obj === void 0;
|
|
};
|
|
|
|
// Shortcut function for checking if an object has a given property directly
|
|
// on itself (in other words, not on a prototype).
|
|
_.has = function(obj, key) {
|
|
return hasOwnProperty.call(obj, key);
|
|
};
|
|
|
|
// Utility Functions
|
|
// -----------------
|
|
|
|
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
|
|
// previous owner. Returns a reference to the Underscore object.
|
|
_.noConflict = function() {
|
|
root._ = previousUnderscore;
|
|
return this;
|
|
};
|
|
|
|
// Keep the identity function around for default iterators.
|
|
_.identity = function(value) {
|
|
return value;
|
|
};
|
|
|
|
// Run a function **n** times.
|
|
_.times = function(n, iterator, context) {
|
|
var accum = Array(n);
|
|
for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
|
|
return accum;
|
|
};
|
|
|
|
// Return a random integer between min and max (inclusive).
|
|
_.random = function(min, max) {
|
|
if (max == null) {
|
|
max = min;
|
|
min = 0;
|
|
}
|
|
return min + Math.floor(Math.random() * (max - min + 1));
|
|
};
|
|
|
|
// List of HTML entities for escaping.
|
|
var entityMap = {
|
|
escape: {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'/': '/'
|
|
}
|
|
};
|
|
entityMap.unescape = _.invert(entityMap.escape);
|
|
|
|
// Regexes containing the keys and values listed immediately above.
|
|
var entityRegexes = {
|
|
escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
|
|
unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
|
|
};
|
|
|
|
// Functions for escaping and unescaping strings to/from HTML interpolation.
|
|
_.each(['escape', 'unescape'], function(method) {
|
|
_[method] = function(string) {
|
|
if (string == null) return '';
|
|
return ('' + string).replace(entityRegexes[method], function(match) {
|
|
return entityMap[method][match];
|
|
});
|
|
};
|
|
});
|
|
|
|
// If the value of the named property is a function then invoke it;
|
|
// otherwise, return it.
|
|
_.result = function(object, property) {
|
|
if (object == null) return null;
|
|
var value = object[property];
|
|
return _.isFunction(value) ? value.call(object) : value;
|
|
};
|
|
|
|
// Add your own custom functions to the Underscore object.
|
|
_.mixin = function(obj) {
|
|
each(_.functions(obj), function(name){
|
|
var func = _[name] = obj[name];
|
|
_.prototype[name] = function() {
|
|
var args = [this._wrapped];
|
|
push.apply(args, arguments);
|
|
return result.call(this, func.apply(_, args));
|
|
};
|
|
});
|
|
};
|
|
|
|
// Generate a unique integer id (unique within the entire client session).
|
|
// Useful for temporary DOM ids.
|
|
var idCounter = 0;
|
|
_.uniqueId = function(prefix) {
|
|
var id = ++idCounter + '';
|
|
return prefix ? prefix + id : id;
|
|
};
|
|
|
|
// By default, Underscore uses ERB-style template delimiters, change the
|
|
// following template settings to use alternative delimiters.
|
|
_.templateSettings = {
|
|
evaluate : /<%([\s\S]+?)%>/g,
|
|
interpolate : /<%=([\s\S]+?)%>/g,
|
|
escape : /<%-([\s\S]+?)%>/g
|
|
};
|
|
|
|
// When customizing `templateSettings`, if you don't want to define an
|
|
// interpolation, evaluation or escaping regex, we need one that is
|
|
// guaranteed not to match.
|
|
var noMatch = /(.)^/;
|
|
|
|
// Certain characters need to be escaped so that they can be put into a
|
|
// string literal.
|
|
var escapes = {
|
|
"'": "'",
|
|
'\\': '\\',
|
|
'\r': 'r',
|
|
'\n': 'n',
|
|
'\t': 't',
|
|
'\u2028': 'u2028',
|
|
'\u2029': 'u2029'
|
|
};
|
|
|
|
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
|
|
|
|
// JavaScript micro-templating, similar to John Resig's implementation.
|
|
// Underscore templating handles arbitrary delimiters, preserves whitespace,
|
|
// and correctly escapes quotes within interpolated code.
|
|
_.template = function(text, data, settings) {
|
|
var render;
|
|
settings = _.defaults({}, settings, _.templateSettings);
|
|
|
|
// Combine delimiters into one regular expression via alternation.
|
|
var matcher = new RegExp([
|
|
(settings.escape || noMatch).source,
|
|
(settings.interpolate || noMatch).source,
|
|
(settings.evaluate || noMatch).source
|
|
].join('|') + '|$', 'g');
|
|
|
|
// Compile the template source, escaping string literals appropriately.
|
|
var index = 0;
|
|
var source = "__p+='";
|
|
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
|
|
source += text.slice(index, offset)
|
|
.replace(escaper, function(match) { return '\\' + escapes[match]; });
|
|
|
|
if (escape) {
|
|
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
|
|
}
|
|
if (interpolate) {
|
|
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
|
|
}
|
|
if (evaluate) {
|
|
source += "';\n" + evaluate + "\n__p+='";
|
|
}
|
|
index = offset + match.length;
|
|
return match;
|
|
});
|
|
source += "';\n";
|
|
|
|
// If a variable is not specified, place data values in local scope.
|
|
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
|
|
|
|
source = "var __t,__p='',__j=Array.prototype.join," +
|
|
"print=function(){__p+=__j.call(arguments,'');};\n" +
|
|
source + "return __p;\n";
|
|
|
|
try {
|
|
render = new Function(settings.variable || 'obj', '_', source);
|
|
} catch (e) {
|
|
e.source = source;
|
|
throw e;
|
|
}
|
|
|
|
if (data) return render(data, _);
|
|
var template = function(data) {
|
|
return render.call(this, data, _);
|
|
};
|
|
|
|
// Provide the compiled function source as a convenience for precompilation.
|
|
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
|
|
|
|
return template;
|
|
};
|
|
|
|
// Add a "chain" function, which will delegate to the wrapper.
|
|
_.chain = function(obj) {
|
|
return _(obj).chain();
|
|
};
|
|
|
|
// OOP
|
|
// ---------------
|
|
// If Underscore is called as a function, it returns a wrapped object that
|
|
// can be used OO-style. This wrapper holds altered versions of all the
|
|
// underscore functions. Wrapped objects may be chained.
|
|
|
|
// Helper function to continue chaining intermediate results.
|
|
var result = function(obj) {
|
|
return this._chain ? _(obj).chain() : obj;
|
|
};
|
|
|
|
// Add all of the Underscore functions to the wrapper object.
|
|
_.mixin(_);
|
|
|
|
// Add all mutator Array functions to the wrapper.
|
|
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
|
|
var method = ArrayProto[name];
|
|
_.prototype[name] = function() {
|
|
var obj = this._wrapped;
|
|
method.apply(obj, arguments);
|
|
if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
|
|
return result.call(this, obj);
|
|
};
|
|
});
|
|
|
|
// Add all accessor Array functions to the wrapper.
|
|
each(['concat', 'join', 'slice'], function(name) {
|
|
var method = ArrayProto[name];
|
|
_.prototype[name] = function() {
|
|
return result.call(this, method.apply(this._wrapped, arguments));
|
|
};
|
|
});
|
|
|
|
_.extend(_.prototype, {
|
|
|
|
// Start chaining a wrapped Underscore object.
|
|
chain: function() {
|
|
this._chain = true;
|
|
return this;
|
|
},
|
|
|
|
// Extracts the result from a wrapped and chained object.
|
|
value: function() {
|
|
return this._wrapped;
|
|
}
|
|
|
|
});
|
|
|
|
}).call(this);
|
|
|
|
},{}]},{},[7])(7)
|
|
});
|
|
; |