From 82b5eb082e5786a8a3a6de52b197b97183101ecf Mon Sep 17 00:00:00 2001 From: obeliskos Date: Sun, 4 Feb 2018 07:31:10 -0500 Subject: [PATCH] added 'useJavascriptSorting' option to simplesort to allow or fallback to js sorting with no indexing on sort property, basic javascript sorting will be faster than loki sorting but may yield unpredictable results if sort property contains dirty or mixed data. when indexing is available on sort property, passing 'useJavascriptSorting' option will fallback to javascript sorting if your filtered results amount to 10% or more of total document count. (best performance tradeoff for most data situations) See #645 for more info. --- benchmark/benchmark_sorting.js | 78 +- benchmark/lokijs-experimental-simplesort.js | 6947 ------------------- build/lokijs.min.js | 4 +- spec/generic/dynamicview.spec.js | 68 + spec/generic/sortingIndexing.spec.js | 26 + src/lokijs.js | 21 +- 6 files changed, 185 insertions(+), 6959 deletions(-) delete mode 100644 benchmark/lokijs-experimental-simplesort.js diff --git a/benchmark/benchmark_sorting.js b/benchmark/benchmark_sorting.js index 2f96d89f..bfd701a2 100644 --- a/benchmark/benchmark_sorting.js +++ b/benchmark/benchmark_sorting.js @@ -108,7 +108,7 @@ function sortfun(obj1, obj2) { if (obj1.b < obj2.b) return -1; } -// unindexed, unfiltered, basic sort function (unsafe for mixed types) +// unindexed, unfiltered, minimal sort function (pre-jitted) function profile1a() { var start, end; var totalTimes = []; @@ -198,8 +198,8 @@ function profile3() { console.log("'a' filtered, 'b' unindexed : " + totalMS + "ms (" + rate + " ops/s) " + loopIterations + " iterations"); } -// filtered, unindexed (minimal sort function) -function profile3b() { +// filtered, unindexed, minimal sort function (pre-jitted) +function profile3a() { var start, end; var totalTimes = []; var totalMS = 0; @@ -229,6 +229,38 @@ function profile3b() { console.log("'a' filtered, 'b' unindexed (minimal sort function) : " + totalMS + "ms (" + rate + " ops/s) " + loopIterations + " iterations"); } +// filtered, unindexed useJavascriptSorting (runtime jit) +function profile3b() { + var start, end; + var totalTimes = []; + var totalMS = 0; + var loopIterations = P2_ITER; + var idx, compare, results; + + createDatabase(false); + + for(idx=0; idx - * - * A lightweight document oriented javascript database - */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD - define([], factory); - } else if (typeof exports === 'object') { - // CommonJS - module.exports = factory(); - } else { - // Browser globals - root.loki = factory(); - } -}(this, function () { - - return (function () { - 'use strict'; - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - var Utils = { - copyProperties: function (src, dest) { - var prop; - for (prop in src) { - dest[prop] = src[prop]; - } - }, - // used to recursively scan hierarchical transform step object for param substitution - resolveTransformObject: function (subObj, params, depth) { - var prop, - pname; - - if (typeof depth !== 'number') { - depth = 0; - } - - if (++depth >= 10) return subObj; - - for (prop in subObj) { - if (typeof subObj[prop] === 'string' && subObj[prop].indexOf("[%lktxp]") === 0) { - pname = subObj[prop].substring(8); - if (params.hasOwnProperty(pname)) { - subObj[prop] = params[pname]; - } - } else if (typeof subObj[prop] === "object") { - subObj[prop] = Utils.resolveTransformObject(subObj[prop], params, depth); - } - } - - return subObj; - }, - // top level utility to resolve an entire (single) transform (array of steps) for parameter substitution - resolveTransformParams: function (transform, params) { - var idx, - clonedStep, - resolvedTransform = []; - - if (typeof params === 'undefined') return transform; - - // iterate all steps in the transform array - for (idx = 0; idx < transform.length; idx++) { - // clone transform so our scan/replace can operate directly on cloned transform - clonedStep = clone(transform[idx], "shallow-recurse-objects"); - resolvedTransform.push(Utils.resolveTransformObject(clonedStep, params)); - } - - return resolvedTransform; - } - }; - - /** Helper function for determining 'loki' abstract equality which is a little more abstract than == - * aeqHelper(5, '5') === true - * aeqHelper(5.0, '5') === true - * aeqHelper(new Date("1/1/2011"), new Date("1/1/2011")) === true - * aeqHelper({a:1}, {z:4}) === true (all objects sorted equally) - * aeqHelper([1, 2, 3], [1, 3]) === false - * aeqHelper([1, 2, 3], [1, 2, 3]) === true - * aeqHelper(undefined, null) === true - */ - function aeqHelper(prop1, prop2) { - var cv1, cv2, t1, t2; - - if (prop1 === prop2) return true; - - // 'falsy' and Boolean handling - if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) { - // dates and NaN conditions (typed dates before serialization) - switch (prop1) { - case undefined: t1 = 1; break; - case null: t1 = 1; break; - case false: t1 = 3; break; - case true: t1 = 4; break; - case "": t1 = 5; break; - default: t1 = (prop1 === prop1)?9:0; break; - } - - switch (prop2) { - case undefined: t2 = 1; break; - case null: t2 = 1; break; - case false: t2 = 3; break; - case true: t2 = 4; break; - case "": t2 = 5; break; - default: t2 = (prop2 === prop2)?9:0; break; - } - - // one or both is edge case - if (t1 !== 9 || t2 !== 9) { - return (t1===t2); - } - } - - // Handle 'Number-like' comparisons - cv1 = Number(prop1); - cv2 = Number(prop2); - - // if one or both are 'number-like'... - if (cv1 === cv1 || cv2 === cv2) { - return (cv1 === cv2); - } - - // not strict equal nor less than nor gt so must be mixed types, convert to string and use that to compare - cv1 = prop1.toString(); - cv2 = prop2.toString(); - - return (cv1 == cv2); - } - - /** Helper function for determining 'less-than' conditions for ops, sorting, and binary indices. - * In the future we might want $lt and $gt ops to use their own functionality/helper. - * Since binary indices on a property might need to index [12, NaN, new Date(), Infinity], we - * need this function (as well as gtHelper) to always ensure one value is LT, GT, or EQ to another. - */ - function ltHelper(prop1, prop2, equal) { - var cv1, cv2, t1, t2; - - // if one of the params is falsy or strictly true or not equal to itself - // 0, 0.0, "", NaN, null, undefined, not defined, false, true - if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) { - switch (prop1) { - case undefined: t1 = 1; break; - case null: t1 = 1; break; - case false: t1 = 3; break; - case true: t1 = 4; break; - case "": t1 = 5; break; - // if strict equal probably 0 so sort higher, otherwise probably NaN so sort lower than even null - default: t1 = (prop1 === prop1)?9:0; break; - } - - switch (prop2) { - case undefined: t2 = 1; break; - case null: t2 = 1; break; - case false: t2 = 3; break; - case true: t2 = 4; break; - case "": t2 = 5; break; - default: t2 = (prop2 === prop2)?9:0; break; - } - - // one or both is edge case - if (t1 !== 9 || t2 !== 9) { - return (t1===t2)?equal:(t1 cv2) return false; - return equal; - } - - if (cv1 === cv1 && cv2 !== cv2) { - return true; - } - - if (cv2 === cv2 && cv1 !== cv1) { - return false; - } - - if (prop1 < prop2) return true; - if (prop1 > prop2) return false; - if (prop1 == prop2) return equal; - - // not strict equal nor less than nor gt so must be mixed types, convert to string and use that to compare - cv1 = prop1.toString(); - cv2 = prop2.toString(); - - if (cv1 < cv2) { - return true; - } - - if (cv1 == cv2) { - return equal; - } - - return false; - } - - function gtHelper(prop1, prop2, equal) { - var cv1, cv2, t1, t2; - - // 'falsy' and Boolean handling - if (!prop1 || !prop2 || prop1 === true || prop2 === true || prop1 !== prop1 || prop2 !== prop2) { - switch (prop1) { - case undefined: t1 = 1; break; - case null: t1 = 1; break; - case false: t1 = 3; break; - case true: t1 = 4; break; - case "": t1 = 5; break; - // NaN 0 - default: t1 = (prop1 === prop1)?9:0; break; - } - - switch (prop2) { - case undefined: t2 = 1; break; - case null: t2 = 1; break; - case false: t2 = 3; break; - case true: t2 = 4; break; - case "": t2 = 5; break; - default: t2 = (prop2 === prop2)?9:0; break; - } - - // one or both is edge case - if (t1 !== 9 || t2 !== 9) { - return (t1===t2)?equal:(t1>t2); - } - } - - // if both are numbers (string encoded or not), compare as numbers - cv1 = Number(prop1); - cv2 = Number(prop2); - if (cv1 === cv1 && cv2 === cv2) { - if (cv1 > cv2) return true; - if (cv1 < cv2) return false; - return equal; - } - - if (cv1 === cv1 && cv2 !== cv2) { - return false; - } - - if (cv2 === cv2 && cv1 !== cv1) { - return true; - } - - if (prop1 > prop2) return true; - if (prop1 < prop2) return false; - if (prop1 == prop2) return equal; - - // not strict equal nor less than nor gt so must be dates or mixed types - // convert to string and use that to compare - cv1 = prop1.toString(); - cv2 = prop2.toString(); - - if (cv1 > cv2) { - return true; - } - - if (cv1 == cv2) { - return equal; - } - - return false; - } - - function sortHelper(prop1, prop2, desc) { - if (aeqHelper(prop1, prop2)) return 0; - - if (ltHelper(prop1, prop2, false)) { - return (desc) ? (1) : (-1); - } - - if (gtHelper(prop1, prop2, false)) { - return (desc) ? (-1) : (1); - } - - // not lt, not gt so implied equality-- date compatible - return 0; - } - - /** - * compoundeval() - helper function for compoundsort(), performing individual object comparisons - * - * @param {array} properties - array of property names, in order, by which to evaluate sort order - * @param {object} obj1 - first object to compare - * @param {object} obj2 - second object to compare - * @returns {integer} 0, -1, or 1 to designate if identical (sortwise) or which should be first - */ - function compoundeval(properties, obj1, obj2) { - var res = 0; - var prop, field, val1, val2, arr; - for (var i = 0, len = properties.length; i < len; i++) { - prop = properties[i]; - field = prop[0]; - if (~field.indexOf('.')) { - arr = field.split('.'); - val1 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, obj1); - val2 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, obj2); - } else { - val1 = obj1[field]; - val2 = obj2[field]; - } - res = sortHelper(val1, val2, prop[1]); - if (res !== 0) { - return res; - } - } - return 0; - } - - /** - * dotSubScan - helper function used for dot notation queries. - * - * @param {object} root - object to traverse - * @param {array} paths - array of properties to drill into - * @param {function} fun - evaluation function to test with - * @param {any} value - comparative value to also pass to (compare) fun - * @param {number} poffset - index of the item in 'paths' to start the sub-scan from - */ - function dotSubScan(root, paths, fun, value, poffset) { - var pathOffset = poffset || 0; - var path = paths[pathOffset]; - if (root === undefined || root === null || !hasOwnProperty.call(root, path)) { - return false; - } - - var valueFound = false; - var element = root[path]; - if (pathOffset + 1 >= paths.length) { - // if we have already expanded out the dot notation, - // then just evaluate the test function and value on the element - valueFound = fun(element, value); - } else if (Array.isArray(element)) { - for (var index = 0, len = element.length; index < len; index += 1) { - valueFound = dotSubScan(element[index], paths, fun, value, pathOffset + 1); - if (valueFound === true) { - break; - } - } - } else { - valueFound = dotSubScan(element, paths, fun, value, pathOffset + 1); - } - - return valueFound; - } - - function containsCheckFn(a) { - if (typeof a === 'string' || Array.isArray(a)) { - return function (b) { - return a.indexOf(b) !== -1; - }; - } else if (typeof a === 'object' && a !== null) { - return function (b) { - return hasOwnProperty.call(a, b); - }; - } - return null; - } - - function doQueryOp(val, op) { - for (var p in op) { - if (hasOwnProperty.call(op, p)) { - return LokiOps[p](val, op[p]); - } - } - return false; - } - - var LokiOps = { - // comparison operators - // a is the value in the collection - // b is the query value - $eq: function (a, b) { - return a === b; - }, - - // abstract/loose equality - $aeq: function (a, b) { - return a == b; - }, - - $ne: function (a, b) { - // ecma 5 safe test for NaN - if (b !== b) { - // ecma 5 test value is not NaN - return (a === a); - } - - return a !== b; - }, - // date equality / loki abstract equality test - $dteq: function (a, b) { - return aeqHelper(a, b); - }, - - $gt: function (a, b) { - return gtHelper(a, b, false); - }, - - $gte: function (a, b) { - return gtHelper(a, b, true); - }, - - $lt: function (a, b) { - return ltHelper(a, b, false); - }, - - $lte: function (a, b) { - return ltHelper(a, b, true); - }, - - // ex : coll.find({'orderCount': {$between: [10, 50]}}); - $between: function (a, vals) { - if (a === undefined || a === null) return false; - return (gtHelper(a, vals[0], true) && ltHelper(a, vals[1], true)); - }, - - $in: function (a, b) { - return b.indexOf(a) !== -1; - }, - - $nin: function (a, b) { - return b.indexOf(a) === -1; - }, - - $keyin: function (a, b) { - return a in b; - }, - - $nkeyin: function (a, b) { - return !(a in b); - }, - - $definedin: function (a, b) { - return b[a] !== undefined; - }, - - $undefinedin: function (a, b) { - return b[a] === undefined; - }, - - $regex: function (a, b) { - return b.test(a); - }, - - $containsString: function (a, b) { - return (typeof a === 'string') && (a.indexOf(b) !== -1); - }, - - $containsNone: function (a, b) { - return !LokiOps.$containsAny(a, b); - }, - - $containsAny: function (a, b) { - var checkFn = containsCheckFn(a); - if (checkFn !== null) { - return (Array.isArray(b)) ? (b.some(checkFn)) : (checkFn(b)); - } - return false; - }, - - $contains: function (a, b) { - var checkFn = containsCheckFn(a); - if (checkFn !== null) { - return (Array.isArray(b)) ? (b.every(checkFn)) : (checkFn(b)); - } - return false; - }, - - $type: function (a, b) { - var type = typeof a; - if (type === 'object') { - if (Array.isArray(a)) { - type = 'array'; - } else if (a instanceof Date) { - type = 'date'; - } - } - return (typeof b !== 'object') ? (type === b) : doQueryOp(type, b); - }, - - $finite: function(a, b) { - return (b === isFinite(a)); - }, - - $size: function (a, b) { - if (Array.isArray(a)) { - return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b); - } - return false; - }, - - $len: function (a, b) { - if (typeof a === 'string') { - return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b); - } - return false; - }, - - $where: function (a, b) { - return b(a) === true; - }, - - // field-level logical operators - // a is the value in the collection - // b is the nested query operation (for '$not') - // or an array of nested query operations (for '$and' and '$or') - $not: function (a, b) { - return !doQueryOp(a, b); - }, - - $and: function (a, b) { - for (var idx = 0, len = b.length; idx < len; idx += 1) { - if (!doQueryOp(a, b[idx])) { - return false; - } - } - return true; - }, - - $or: function (a, b) { - for (var idx = 0, len = b.length; idx < len; idx += 1) { - if (doQueryOp(a, b[idx])) { - return true; - } - } - return false; - } - }; - - // if an op is registered in this object, our 'calculateRange' can use it with our binary indices. - // if the op is registered to a function, we will run that function/op as a 2nd pass filter on results. - // those 2nd pass filter functions should be similar to LokiOps functions, accepting 2 vals to compare. - var indexedOps = { - $eq: LokiOps.$eq, - $aeq: true, - $dteq: true, - $gt: true, - $gte: true, - $lt: true, - $lte: true, - $in: true, - $between: true - }; - - function clone(data, method) { - if (data === null || data === undefined) { - return null; - } - - var cloneMethod = method || 'parse-stringify', - cloned; - - switch (cloneMethod) { - case "parse-stringify": - cloned = JSON.parse(JSON.stringify(data)); - break; - case "jquery-extend-deep": - cloned = jQuery.extend(true, {}, data); - break; - case "shallow": - // more compatible method for older browsers - cloned = Object.create(data.constructor.prototype); - Object.keys(data).map(function (i) { - cloned[i] = data[i]; - }); - break; - case "shallow-assign": - // should be supported by newer environments/browsers - cloned = Object.create(data.constructor.prototype); - Object.assign(cloned, data); - break; - case "shallow-recurse-objects": - // shallow clone top level properties - cloned = clone(data, "shallow"); - var keys = Object.keys(data); - // for each of the top level properties which are object literals, recursively shallow copy - keys.forEach(function(key) { - if (typeof data[key] === "object" && data[key].constructor.name === "Object") { - cloned[key] = clone(data[key], "shallow-recurse-objects"); - } - }); - break; - default: - break; - } - - return cloned; - } - - function cloneObjectArray(objarray, method) { - var i, - result = []; - - if (method == "parse-stringify") { - return clone(objarray, method); - } - - i = objarray.length - 1; - - for (; i <= 0; i--) { - result.push(clone(objarray[i], method)); - } - - return result; - } - - function localStorageAvailable() { - try { - return (window && window.localStorage !== undefined && window.localStorage !== null); - } catch (e) { - return false; - } - } - - - /** - * LokiEventEmitter is a minimalist version of EventEmitter. It enables any - * constructor that inherits EventEmitter to emit events and trigger - * listeners that have been added to the event through the on(event, callback) method - * - * @constructor LokiEventEmitter - */ - function LokiEventEmitter() {} - - /** - * @prop {hashmap} events - a hashmap, with each property being an array of callbacks - * @memberof LokiEventEmitter - */ - LokiEventEmitter.prototype.events = {}; - - /** - * @prop {boolean} asyncListeners - boolean determines whether or not the callbacks associated with each event - * should happen in an async fashion or not - * Default is false, which means events are synchronous - * @memberof LokiEventEmitter - */ - LokiEventEmitter.prototype.asyncListeners = false; - - /** - * on(eventName, listener) - adds a listener to the queue of callbacks associated to an event - * @param {string|string[]} eventName - the name(s) of the event(s) to listen to - * @param {function} listener - callback function of listener to attach - * @returns {int} the index of the callback in the array of listeners for a particular event - * @memberof LokiEventEmitter - */ - LokiEventEmitter.prototype.on = function (eventName, listener) { - var event; - var self = this; - - if (Array.isArray(eventName)) { - eventName.forEach(function(currentEventName) { - self.on(currentEventName, listener); - }); - return listener; - } - - event = this.events[eventName]; - if (!event) { - event = this.events[eventName] = []; - } - event.push(listener); - return listener; - }; - - /** - * emit(eventName, data) - emits a particular event - * with the option of passing optional parameters which are going to be processed by the callback - * provided signatures match (i.e. if passing emit(event, arg0, arg1) the listener should take two parameters) - * @param {string} eventName - the name of the event - * @param {object=} data - optional object passed with the event - * @memberof LokiEventEmitter - */ - LokiEventEmitter.prototype.emit = function (eventName) { - var self = this; - var selfArgs = Array.prototype.slice.call(arguments, 1); - if (eventName && this.events[eventName]) { - this.events[eventName].forEach(function (listener) { - if (self.asyncListeners) { - setTimeout(function () { - listener.apply(self, selfArgs); - }, 1); - } else { - listener.apply(self, selfArgs); - } - - }); - } else { - throw new Error('No event ' + eventName + ' defined'); - } - }; - - /** - * Alias of LokiEventEmitter.prototype.on - * addListener(eventName, listener) - adds a listener to the queue of callbacks associated to an event - * @param {string|string[]} eventName - the name(s) of the event(s) to listen to - * @param {function} listener - callback function of listener to attach - * @returns {int} the index of the callback in the array of listeners for a particular event - * @memberof LokiEventEmitter - */ - LokiEventEmitter.prototype.addListener = LokiEventEmitter.prototype.on; - - /** - * removeListener() - removes the listener at position 'index' from the event 'eventName' - * @param {string|string[]} eventName - the name(s) of the event(s) which the listener is attached to - * @param {function} listener - the listener callback function to remove from emitter - * @memberof LokiEventEmitter - */ - LokiEventEmitter.prototype.removeListener = function (eventName, listener) { - var self = this; - - if (Array.isArray(eventName)) { - eventName.forEach(function(currentEventName) { - self.removeListener(currentEventName, listener); - }); - - return; - } - - if (this.events[eventName]) { - var listeners = this.events[eventName]; - listeners.splice(listeners.indexOf(listener), 1); - } - }; - - /** - * Loki: The main database class - * @constructor Loki - * @implements LokiEventEmitter - * @param {string} filename - name of the file to be saved to - * @param {object=} options - (Optional) config options object - * @param {string} options.env - override environment detection as 'NODEJS', 'BROWSER', 'CORDOVA' - * @param {boolean} [options.verbose=false] - enable console output - * @param {boolean} [options.autosave=false] - enables autosave - * @param {int} [options.autosaveInterval=5000] - time interval (in milliseconds) between saves (if dirty) - * @param {boolean} [options.autoload=false] - enables autoload on loki instantiation - * @param {function} options.autoloadCallback - user callback called after database load - * @param {adapter} options.adapter - an instance of a loki persistence adapter - * @param {string} [options.serializationMethod='normal'] - ['normal', 'pretty', 'destructured'] - * @param {string} options.destructureDelimiter - string delimiter used for destructured serialization - * @param {boolean} [options.throttledSaves=true] - debounces multiple calls to to saveDatabase reducing number of disk I/O operations - and guaranteeing proper serialization of the calls. - */ - function Loki(filename, options) { - this.filename = filename || 'loki.db'; - this.collections = []; - - // persist version of code which created the database to the database. - // could use for upgrade scenarios - this.databaseVersion = 1.5; - this.engineVersion = 1.5; - - // autosave support (disabled by default) - // pass autosave: true, autosaveInterval: 6000 in options to set 6 second autosave - this.autosave = false; - this.autosaveInterval = 5000; - this.autosaveHandle = null; - this.throttledSaves = true; - - this.options = {}; - - // currently keeping persistenceMethod and persistenceAdapter as loki level properties that - // will not or cannot be deserialized. You are required to configure persistence every time - // you instantiate a loki object (or use default environment detection) in order to load the database anyways. - - // persistenceMethod could be 'fs', 'localStorage', or 'adapter' - // this is optional option param, otherwise environment detection will be used - // if user passes their own adapter we will force this method to 'adapter' later, so no need to pass method option. - this.persistenceMethod = null; - - // retain reference to optional (non-serializable) persistenceAdapter 'instance' - this.persistenceAdapter = null; - - // flags used to throttle saves - this.throttledSavePending = false; - this.throttledCallbacks = []; - - // enable console output if verbose flag is set (disabled by default) - this.verbose = options && options.hasOwnProperty('verbose') ? options.verbose : false; - - this.events = { - 'init': [], - 'loaded': [], - 'flushChanges': [], - 'close': [], - 'changes': [], - 'warning': [] - }; - - var getENV = function () { - if (typeof global !== 'undefined' && (global.android || global.NSObject)) { - // If no adapter assume nativescript which needs adapter to be passed manually - return 'NATIVESCRIPT'; //nativescript - } - - if (typeof window === 'undefined') { - return 'NODEJS'; - } - - if (typeof global !== 'undefined' && global.window) { - return 'NODEJS'; //node-webkit - } - - if (typeof document !== 'undefined') { - if (document.URL.indexOf('http://') === -1 && document.URL.indexOf('https://') === -1) { - return 'CORDOVA'; - } - return 'BROWSER'; - } - return 'CORDOVA'; - }; - - // refactored environment detection due to invalid detection for browser environments. - // if they do not specify an options.env we want to detect env rather than default to nodejs. - // currently keeping two properties for similar thing (options.env and options.persistenceMethod) - // might want to review whether we can consolidate. - if (options && options.hasOwnProperty('env')) { - this.ENV = options.env; - } else { - this.ENV = getENV(); - } - - // not sure if this is necessary now that i have refactored the line above - if (this.ENV === 'undefined') { - this.ENV = 'NODEJS'; - } - - this.configureOptions(options, true); - - this.on('init', this.clearChanges); - - } - - // db class is an EventEmitter - Loki.prototype = new LokiEventEmitter(); - Loki.prototype.constructor = Loki; - - // experimental support for browserify's abstract syntax scan to pick up dependency of indexed adapter. - // Hopefully, once this hits npm a browserify require of lokijs should scan the main file and detect this indexed adapter reference. - Loki.prototype.getIndexedAdapter = function () { - var adapter; - - if (typeof require === 'function') { - adapter = require("./loki-indexed-adapter.js"); - } - - return adapter; - }; - - - /** - * Allows reconfiguring database options - * - * @param {object} options - configuration options to apply to loki db object - * @param {string} options.env - override environment detection as 'NODEJS', 'BROWSER', 'CORDOVA' - * @param {boolean} options.verbose - enable console output (default is 'false') - * @param {boolean} options.autosave - enables autosave - * @param {int} options.autosaveInterval - time interval (in milliseconds) between saves (if dirty) - * @param {boolean} options.autoload - enables autoload on loki instantiation - * @param {function} options.autoloadCallback - user callback called after database load - * @param {adapter} options.adapter - an instance of a loki persistence adapter - * @param {string} options.serializationMethod - ['normal', 'pretty', 'destructured'] - * @param {string} options.destructureDelimiter - string delimiter used for destructured serialization - * @param {boolean} initialConfig - (internal) true is passed when loki ctor is invoking - * @memberof Loki - */ - Loki.prototype.configureOptions = function (options, initialConfig) { - var defaultPersistence = { - 'NODEJS': 'fs', - 'BROWSER': 'localStorage', - 'CORDOVA': 'localStorage', - 'MEMORY': 'memory' - }, - persistenceMethods = { - 'fs': LokiFsAdapter, - 'localStorage': LokiLocalStorageAdapter, - 'memory': LokiMemoryAdapter - }; - - this.options = {}; - - this.persistenceMethod = null; - // retain reference to optional persistence adapter 'instance' - // currently keeping outside options because it can't be serialized - this.persistenceAdapter = null; - - // process the options - if (typeof (options) !== 'undefined') { - this.options = options; - - if (this.options.hasOwnProperty('persistenceMethod')) { - // check if the specified persistence method is known - if (typeof (persistenceMethods[options.persistenceMethod]) == 'function') { - this.persistenceMethod = options.persistenceMethod; - this.persistenceAdapter = new persistenceMethods[options.persistenceMethod](); - } - // should be throw an error here, or just fall back to defaults ?? - } - - // if user passes adapter, set persistence mode to adapter and retain persistence adapter instance - if (this.options.hasOwnProperty('adapter')) { - this.persistenceMethod = 'adapter'; - this.persistenceAdapter = options.adapter; - this.options.adapter = null; - } - - - // if they want to load database on loki instantiation, now is a good time to load... after adapter set and before possible autosave initiation - if (options.autoload && initialConfig) { - // for autoload, let the constructor complete before firing callback - var self = this; - setTimeout(function () { - self.loadDatabase(options, options.autoloadCallback); - }, 1); - } - - if (this.options.hasOwnProperty('autosaveInterval')) { - this.autosaveDisable(); - this.autosaveInterval = parseInt(this.options.autosaveInterval, 10); - } - - if (this.options.hasOwnProperty('autosave') && this.options.autosave) { - this.autosaveDisable(); - this.autosave = true; - - if (this.options.hasOwnProperty('autosaveCallback')) { - this.autosaveEnable(options, options.autosaveCallback); - } else { - this.autosaveEnable(); - } - } - - if (this.options.hasOwnProperty('throttledSaves')) { - this.throttledSaves = this.options.throttledSaves; - } - } // end of options processing - - // ensure defaults exists for options which were not set - if (!this.options.hasOwnProperty('serializationMethod')) { - this.options.serializationMethod = 'normal'; - } - - // ensure passed or default option exists - if (!this.options.hasOwnProperty('destructureDelimiter')) { - this.options.destructureDelimiter = '$<\n'; - } - - // if by now there is no adapter specified by user nor derived from persistenceMethod: use sensible defaults - if (this.persistenceAdapter === null) { - this.persistenceMethod = defaultPersistence[this.ENV]; - if (this.persistenceMethod) { - this.persistenceAdapter = new persistenceMethods[this.persistenceMethod](); - } - } - - }; - - /** - * Copies 'this' database into a new Loki instance. Object references are shared to make lightweight. - * - * @param {object} options - apply or override collection level settings - * @param {bool} options.removeNonSerializable - nulls properties not safe for serialization. - * @memberof Loki - */ - Loki.prototype.copy = function(options) { - // in case running in an environment without accurate environment detection, pass 'NA' - var databaseCopy = new Loki(this.filename, { env: "NA" }); - var clen, idx; - - options = options || {}; - - // currently inverting and letting loadJSONObject do most of the work - databaseCopy.loadJSONObject(this, { retainDirtyFlags: true }); - - // since our JSON serializeReplacer is not invoked for reference database adapters, this will let us mimic - if(options.hasOwnProperty("removeNonSerializable") && options.removeNonSerializable === true) { - databaseCopy.autosaveHandle = null; - databaseCopy.persistenceAdapter = null; - - clen = databaseCopy.collections.length; - for (idx=0; idx 0) { - throw new Error("disableMeta option cannot be passed as true when ttl is enabled"); - } - } - - for (i = 0; i < len; i += 1) { - if (this.collections[i].name === name) { - return this.collections[i]; - } - } - - var collection = new Collection(name, options); - this.collections.push(collection); - - if (this.verbose) - collection.console = console; - - return collection; - }; - - Loki.prototype.loadCollection = function (collection) { - if (!collection.name) { - throw new Error('Collection must have a name property to be loaded'); - } - this.collections.push(collection); - }; - - /** - * Retrieves reference to a collection by name. - * @param {string} collectionName - name of collection to look up - * @returns {Collection} Reference to collection in database by that name, or null if not found - * @memberof Loki - */ - Loki.prototype.getCollection = function (collectionName) { - var i, - len = this.collections.length; - - for (i = 0; i < len; i += 1) { - if (this.collections[i].name === collectionName) { - return this.collections[i]; - } - } - - // no such collection - this.emit('warning', 'collection ' + collectionName + ' not found'); - return null; - }; - - /** - * Renames an existing loki collection - * @param {string} oldName - name of collection to rename - * @param {string} newName - new name of collection - * @returns {Collection} reference to the newly renamed collection - * @memberof Loki - */ - Loki.prototype.renameCollection = function (oldName, newName) { - var c = this.getCollection(oldName); - - if (c) { - c.name = newName; - } - - return c; - }; - - /** - * Returns a list of collections in the database. - * @returns {object[]} array of objects containing 'name', 'type', and 'count' properties. - * @memberof Loki - */ - Loki.prototype.listCollections = function () { - - var i = this.collections.length, - colls = []; - - while (i--) { - colls.push({ - name: this.collections[i].name, - type: this.collections[i].objType, - count: this.collections[i].data.length - }); - } - return colls; - }; - - /** - * Removes a collection from the database. - * @param {string} collectionName - name of collection to remove - * @memberof Loki - */ - Loki.prototype.removeCollection = function (collectionName) { - var i, - len = this.collections.length; - - for (i = 0; i < len; i += 1) { - if (this.collections[i].name === collectionName) { - var tmpcol = new Collection(collectionName, {}); - var curcol = this.collections[i]; - for (var prop in curcol) { - if (curcol.hasOwnProperty(prop) && tmpcol.hasOwnProperty(prop)) { - curcol[prop] = tmpcol[prop]; - } - } - this.collections.splice(i, 1); - return; - } - } - }; - - Loki.prototype.getName = function () { - return this.name; - }; - - /** - * serializeReplacer - used to prevent certain properties from being serialized - * - */ - Loki.prototype.serializeReplacer = function (key, value) { - switch (key) { - case 'autosaveHandle': - case 'persistenceAdapter': - case 'constraints': - case 'ttl': - return null; - case 'throttledSavePending': - case 'throttledCallbacks': - return undefined; - default: - return value; - } - }; - - /** - * Serialize database to a string which can be loaded via {@link Loki#loadJSON} - * - * @returns {string} Stringified representation of the loki database. - * @memberof Loki - */ - Loki.prototype.serialize = function (options) { - options = options || {}; - - if (!options.hasOwnProperty("serializationMethod")) { - options.serializationMethod = this.options.serializationMethod; - } - - switch(options.serializationMethod) { - case "normal": return JSON.stringify(this, this.serializeReplacer); - case "pretty": return JSON.stringify(this, this.serializeReplacer, 2); - case "destructured": return this.serializeDestructured(); // use default options - default: return JSON.stringify(this, this.serializeReplacer); - } - }; - - // alias of serialize - Loki.prototype.toJson = Loki.prototype.serialize; - - /** - * Database level destructured JSON serialization routine to allow alternate serialization methods. - * Internally, Loki supports destructuring via loki "serializationMethod' option and - * the optional LokiPartitioningAdapter class. It is also available if you wish to do - * your own structured persistence or data exchange. - * - * @param {object=} options - output format options for use externally to loki - * @param {bool=} options.partitioned - (default: false) whether db and each collection are separate - * @param {int=} options.partition - can be used to only output an individual collection or db (-1) - * @param {bool=} options.delimited - (default: true) whether subitems are delimited or subarrays - * @param {string=} options.delimiter - override default delimiter - * - * @returns {string|array} A custom, restructured aggregation of independent serializations. - * @memberof Loki - */ - Loki.prototype.serializeDestructured = function(options) { - var idx, sidx, result, resultlen; - var reconstruct = []; - var dbcopy; - - options = options || {}; - - if (!options.hasOwnProperty("partitioned")) { - options.partitioned = false; - } - - if (!options.hasOwnProperty("delimited")) { - options.delimited = true; - } - - if (!options.hasOwnProperty("delimiter")) { - options.delimiter = this.options.destructureDelimiter; - } - - // 'partitioned' along with 'partition' of 0 or greater is a request for single collection serialization - if (options.partitioned === true && options.hasOwnProperty("partition") && options.partition >= 0) { - return this.serializeCollection({ - delimited: options.delimited, - delimiter: options.delimiter, - collectionIndex: options.partition - }); - } - - // not just an individual collection, so we will need to serialize db container via shallow copy - dbcopy = new Loki(this.filename); - dbcopy.loadJSONObject(this); - - for(idx=0; idx < dbcopy.collections.length; idx++) { - dbcopy.collections[idx].data = []; - } - - // if we -only- wanted the db container portion, return it now - if (options.partitioned === true && options.partition === -1) { - // since we are deconstructing, override serializationMethod to normal for here - return dbcopy.serialize({ - serializationMethod: "normal" - }); - } - - // at this point we must be deconstructing the entire database - // start by pushing db serialization into first array element - reconstruct.push(dbcopy.serialize({ - serializationMethod: "normal" - })); - - dbcopy = null; - - // push collection data into subsequent elements - for(idx=0; idx < this.collections.length; idx++) { - result = this.serializeCollection({ - delimited: options.delimited, - delimiter: options.delimiter, - collectionIndex: idx - }); - - // NDA : Non-Delimited Array : one iterable concatenated array with empty string collection partitions - if (options.partitioned === false && options.delimited === false) { - if (!Array.isArray(result)) { - throw new Error("a nondelimited, non partitioned collection serialization did not return an expected array"); - } - - // Array.concat would probably duplicate memory overhead for copying strings. - // Instead copy each individually, and clear old value after each copy. - // Hopefully this will allow g.c. to reduce memory pressure, if needed. - resultlen = result.length; - - for (sidx=0; sidx < resultlen; sidx++) { - reconstruct.push(result[sidx]); - result[sidx] = null; - } - - reconstruct.push(""); - } - else { - reconstruct.push(result); - } - } - - // Reconstruct / present results according to four combinations : D, DA, NDA, NDAA - if (options.partitioned) { - // DA : Delimited Array of strings [0] db [1] collection [n] collection { partitioned: true, delimited: true } - // useful for simple future adaptations of existing persistence adapters to save collections separately - if (options.delimited) { - return reconstruct; - } - // NDAA : Non-Delimited Array with subArrays. db at [0] and collection subarrays at [n] { partitioned: true, delimited : false } - // This format might be the most versatile for 'rolling your own' partitioned sync or save. - // Memory overhead can be reduced by specifying a specific partition, but at this code path they did not, so its all. - else { - return reconstruct; - } - } - else { - // D : one big Delimited string { partitioned: false, delimited : true } - // This is the method Loki will use internally if 'destructured'. - // Little memory overhead improvements but does not require multiple asynchronous adapter call scheduling - if (options.delimited) { - // indicate no more collections - reconstruct.push(""); - - return reconstruct.join(options.delimiter); - } - // NDA : Non-Delimited Array : one iterable array with empty string collection partitions { partitioned: false, delimited: false } - // This format might be best candidate for custom synchronous syncs or saves - else { - // indicate no more collections - reconstruct.push(""); - - return reconstruct; - } - } - - reconstruct.push(""); - - return reconstruct.join(delim); - }; - - /** - * Collection level utility method to serialize a collection in a 'destructured' format - * - * @param {object=} options - used to determine output of method - * @param {int} options.delimited - whether to return single delimited string or an array - * @param {string} options.delimiter - (optional) if delimited, this is delimiter to use - * @param {int} options.collectionIndex - specify which collection to serialize data for - * - * @returns {string|array} A custom, restructured aggregation of independent serializations for a single collection. - * @memberof Loki - */ - Loki.prototype.serializeCollection = function(options) { - var doccount, - docidx, - resultlines = []; - - options = options || {}; - - if (!options.hasOwnProperty("delimited")) { - options.delimited = true; - } - - if (!options.hasOwnProperty("collectionIndex")) { - throw new Error("serializeCollection called without 'collectionIndex' option"); - } - - doccount = this.collections[options.collectionIndex].data.length; - - resultlines = []; - - for(docidx=0; docidx collCount) { - done = true; - } - } - else { - currObject = JSON.parse(workarray[lineIndex]); - cdb.collections[collIndex].data.push(currObject); - } - - // lower memory pressure and advance iterator - workarray[lineIndex++] = null; - } - - return cdb; - }; - - /** - * Collection level utility function to deserializes a destructured collection. - * - * @param {string|array} destructuredSource - destructured representation of collection to inflate - * @param {object=} options - used to describe format of destructuredSource input - * @param {int=} [options.delimited=false] - whether source is delimited string or an array - * @param {string=} options.delimiter - if delimited, this is delimiter to use (if other than default) - * - * @returns {array} an array of documents to attach to collection.data. - * @memberof Loki - */ - Loki.prototype.deserializeCollection = function(destructuredSource, options) { - var workarray=[]; - var idx, len; - - options = options || {}; - - if (!options.hasOwnProperty("partitioned")) { - options.partitioned = false; - } - - if (!options.hasOwnProperty("delimited")) { - options.delimited = true; - } - - if (!options.hasOwnProperty("delimiter")) { - options.delimiter = this.options.destructureDelimiter; - } - - if (options.delimited) { - workarray = destructuredSource.split(options.delimiter); - workarray.pop(); - } - else { - workarray = destructuredSource; - } - - len = workarray.length; - for (idx=0; idx < len; idx++) { - workarray[idx] = JSON.parse(workarray[idx]); - } - - return workarray; - }; - - /** - * Inflates a loki database from a serialized JSON string - * - * @param {string} serializedDb - a serialized loki database string - * @param {object=} options - apply or override collection level settings - * @param {bool} options.retainDirtyFlags - whether collection dirty flags will be preserved - * @memberof Loki - */ - Loki.prototype.loadJSON = function (serializedDb, options) { - var dbObject; - if (serializedDb.length === 0) { - dbObject = {}; - } else { - // using option defined in instantiated db not what was in serialized db - switch (this.options.serializationMethod) { - case "normal": - case "pretty": dbObject = JSON.parse(serializedDb); break; - case "destructured": dbObject = this.deserializeDestructured(serializedDb); break; - default: dbObject = JSON.parse(serializedDb); break; - } - } - - this.loadJSONObject(dbObject, options); - }; - - /** - * Inflates a loki database from a JS object - * - * @param {object} dbObject - a serialized loki database string - * @param {object=} options - apply or override collection level settings - * @param {bool} options.retainDirtyFlags - whether collection dirty flags will be preserved - * @memberof Loki - */ - Loki.prototype.loadJSONObject = function (dbObject, options) { - var i = 0, - len = dbObject.collections ? dbObject.collections.length : 0, - coll, - copyColl, - clen, - j, - loader, - collObj; - - this.name = dbObject.name; - - // restore save throttled boolean only if not defined in options - if (dbObject.hasOwnProperty('throttledSaves') && options && !options.hasOwnProperty('throttledSaves')) { - this.throttledSaves = dbObject.throttledSaves; - } - - this.collections = []; - - function makeLoader(coll) { - var collOptions = options[coll.name]; - var inflater; - - if(collOptions.proto) { - inflater = collOptions.inflate || Utils.copyProperties; - - return function(data) { - var collObj = new(collOptions.proto)(); - inflater(data, collObj); - return collObj; - }; - } - - return collOptions.inflate; - } - - for (i; i < len; i += 1) { - coll = dbObject.collections[i]; - - copyColl = this.addCollection(coll.name, { disableChangesApi: coll.disableChangesApi, disableDeltaChangesApi: coll.disableDeltaChangesApi, disableMeta: coll.disableMeta }); - - copyColl.adaptiveBinaryIndices = coll.hasOwnProperty('adaptiveBinaryIndices')?(coll.adaptiveBinaryIndices === true): false; - copyColl.transactional = coll.transactional; - copyColl.asyncListeners = coll.asyncListeners; - copyColl.cloneObjects = coll.cloneObjects; - copyColl.cloneMethod = coll.cloneMethod || "parse-stringify"; - copyColl.autoupdate = coll.autoupdate; - copyColl.changes = coll.changes; - - if (options && options.retainDirtyFlags === true) { - copyColl.dirty = coll.dirty; - } - else { - copyColl.dirty = false; - } - - // load each element individually - clen = coll.data.length; - j = 0; - if (options && options.hasOwnProperty(coll.name)) { - loader = makeLoader(coll); - - for (j; j < clen; j++) { - collObj = loader(coll.data[j]); - copyColl.data[j] = collObj; - copyColl.addAutoUpdateObserver(collObj); - } - } else { - - for (j; j < clen; j++) { - copyColl.data[j] = coll.data[j]; - copyColl.addAutoUpdateObserver(copyColl.data[j]); - } - } - - copyColl.maxId = (typeof coll.maxId === 'undefined') ? 0 : coll.maxId; - copyColl.idIndex = coll.idIndex; - if (typeof (coll.binaryIndices) !== 'undefined') { - copyColl.binaryIndices = coll.binaryIndices; - } - if (typeof coll.transforms !== 'undefined') { - copyColl.transforms = coll.transforms; - } - - copyColl.ensureId(); - - // regenerate unique indexes - copyColl.uniqueNames = []; - if (coll.hasOwnProperty("uniqueNames")) { - copyColl.uniqueNames = coll.uniqueNames; - for (j = 0; j < copyColl.uniqueNames.length; j++) { - copyColl.ensureUniqueIndex(copyColl.uniqueNames[j]); - } - } - - // in case they are loading a database created before we added dynamic views, handle undefined - if (typeof (coll.DynamicViews) === 'undefined') continue; - - // reinflate DynamicViews and attached Resultsets - for (var idx = 0; idx < coll.DynamicViews.length; idx++) { - var colldv = coll.DynamicViews[idx]; - - var dv = copyColl.addDynamicView(colldv.name, colldv.options); - dv.resultdata = colldv.resultdata; - dv.resultsdirty = colldv.resultsdirty; - dv.filterPipeline = colldv.filterPipeline; - - dv.sortCriteria = colldv.sortCriteria; - dv.sortFunction = null; - - dv.sortDirty = colldv.sortDirty; - dv.resultset.filteredrows = colldv.resultset.filteredrows; - dv.resultset.filterInitialized = colldv.resultset.filterInitialized; - - dv.rematerialize({ - removeWhereFilters: true - }); - } - - // Upgrade Logic for binary index refactoring at version 1.5 - if (dbObject.databaseVersion < 1.5) { - // rebuild all indices - copyColl.ensureAllIndexes(true); - copyColl.dirty = true; - } - } - }; - - /** - * Emits the close event. In autosave scenarios, if the database is dirty, this will save and disable timer. - * Does not actually destroy the db. - * - * @param {function=} callback - (Optional) if supplied will be registered with close event before emitting. - * @memberof Loki - */ - Loki.prototype.close = function (callback) { - // for autosave scenarios, we will let close perform final save (if dirty) - // For web use, you might call from window.onbeforeunload to shutdown database, saving pending changes - if (this.autosave) { - this.autosaveDisable(); - if (this.autosaveDirty()) { - this.saveDatabase(callback); - callback = undefined; - } - } - - if (callback) { - this.on('close', callback); - } - this.emit('close'); - }; - - /**-------------------------+ - | Changes API | - +--------------------------*/ - - /** - * The Changes API enables the tracking the changes occurred in the collections since the beginning of the session, - * so it's possible to create a differential dataset for synchronization purposes (possibly to a remote db) - */ - - /** - * (Changes API) : takes all the changes stored in each - * collection and creates a single array for the entire database. If an array of names - * of collections is passed then only the included collections will be tracked. - * - * @param {array=} optional array of collection names. No arg means all collections are processed. - * @returns {array} array of changes - * @see private method createChange() in Collection - * @memberof Loki - */ - Loki.prototype.generateChangesNotification = function (arrayOfCollectionNames) { - function getCollName(coll) { - return coll.name; - } - var changes = [], - selectedCollections = arrayOfCollectionNames || this.collections.map(getCollName); - - this.collections.forEach(function (coll) { - if (selectedCollections.indexOf(getCollName(coll)) !== -1) { - changes = changes.concat(coll.getChanges()); - } - }); - return changes; - }; - - /** - * (Changes API) - stringify changes for network transmission - * @returns {string} string representation of the changes - * @memberof Loki - */ - Loki.prototype.serializeChanges = function (collectionNamesArray) { - return JSON.stringify(this.generateChangesNotification(collectionNamesArray)); - }; - - /** - * (Changes API) : clears all the changes in all collections. - * @memberof Loki - */ - Loki.prototype.clearChanges = function () { - this.collections.forEach(function (coll) { - if (coll.flushChanges) { - coll.flushChanges(); - } - }); - }; - - /*------------------+ - | PERSISTENCE | - -------------------*/ - - /** there are two build in persistence adapters for internal use - * fs for use in Nodejs type environments - * localStorage for use in browser environment - * defined as helper classes here so its easy and clean to use - */ - - /** - * In in-memory persistence adapter for an in-memory database. - * This simple 'key/value' adapter is intended for unit testing and diagnostics. - * - * @param {object=} options - memory adapter options - * @param {boolean} [options.asyncResponses=false] - whether callbacks are invoked asynchronously - * @param {int} [options.asyncTimeout=50] - timeout in ms to queue callbacks - * @constructor LokiMemoryAdapter - */ - function LokiMemoryAdapter(options) { - this.hashStore = {}; - this.options = options || {}; - - if (!this.options.hasOwnProperty('asyncResponses')) { - this.options.asyncResponses = false; - } - - if (!this.options.hasOwnProperty('asyncTimeout')) { - this.options.asyncTimeout = 50; // 50 ms default - } - } - - /** - * Loads a serialized database from its in-memory store. - * (Loki persistence adapter interface function) - * - * @param {string} dbname - name of the database (filename/keyname) - * @param {function} callback - adapter callback to return load result to caller - * @memberof LokiMemoryAdapter - */ - LokiMemoryAdapter.prototype.loadDatabase = function (dbname, callback) { - var self=this; - - if (this.options.asyncResponses) { - setTimeout(function() { - if (self.hashStore.hasOwnProperty(dbname)) { - callback(self.hashStore[dbname].value); - } - else { - // database doesn't exist, return falsy - callback (null); - } - }, this.options.asyncTimeout); - } - else { - if (this.hashStore.hasOwnProperty(dbname)) { - // database doesn't exist, return falsy - callback(this.hashStore[dbname].value); - } - else { - callback (null); - } - } - }; - - /** - * Saves a serialized database to its in-memory store. - * (Loki persistence adapter interface function) - * - * @param {string} dbname - name of the database (filename/keyname) - * @param {function} callback - adapter callback to return load result to caller - * @memberof LokiMemoryAdapter - */ - LokiMemoryAdapter.prototype.saveDatabase = function (dbname, dbstring, callback) { - var self=this; - var saveCount; - - if (this.options.asyncResponses) { - setTimeout(function() { - saveCount = (self.hashStore.hasOwnProperty(dbname)?self.hashStore[dbname].savecount:0); - - self.hashStore[dbname] = { - savecount: saveCount+1, - lastsave: new Date(), - value: dbstring - }; - - callback(); - }, this.options.asyncTimeout); - } - else { - saveCount = (this.hashStore.hasOwnProperty(dbname)?this.hashStore[dbname].savecount:0); - - this.hashStore[dbname] = { - savecount: saveCount+1, - lastsave: new Date(), - value: dbstring - }; - - callback(); - } - }; - - /** - * Deletes a database from its in-memory store. - * - * @param {string} dbname - name of the database (filename/keyname) - * @param {function} callback - function to call when done - * @memberof LokiMemoryAdapter - */ - LokiMemoryAdapter.prototype.deleteDatabase = function(dbname, callback) { - if (this.hashStore.hasOwnProperty(dbname)) { - delete this.hashStore[dbname]; - } - - if (typeof callback === "function") { - callback(); - } - }; - - /** - * An adapter for adapters. Converts a non reference mode adapter into a reference mode adapter - * which can perform destructuring and partioning. Each collection will be stored in its own key/save and - * only dirty collections will be saved. If you turn on paging with default page size of 25megs and save - * a 75 meg collection it should use up roughly 3 save slots (key/value pairs sent to inner adapter). - * A dirty collection that spans three pages will save all three pages again - * Paging mode was added mainly because Chrome has issues saving 'too large' of a string within a - * single indexeddb row. If a single document update causes the collection to be flagged as dirty, all - * of that collection's pages will be written on next save. - * - * @param {object} adapter - reference to a 'non-reference' mode loki adapter instance. - * @param {object=} options - configuration options for partitioning and paging - * @param {bool} options.paging - (default: false) set to true to enable paging collection data. - * @param {int} options.pageSize - (default : 25MB) you can use this to limit size of strings passed to inner adapter. - * @param {string} options.delimiter - allows you to override the default delimeter - * @constructor LokiPartitioningAdapter - */ - function LokiPartitioningAdapter(adapter, options) { - this.mode = "reference"; - this.adapter = null; - this.options = options || {}; - this.dbref = null; - this.dbname = ""; - this.pageIterator = {}; - - // verify user passed an appropriate adapter - if (adapter) { - if (adapter.mode === "reference") { - throw new Error("LokiPartitioningAdapter cannot be instantiated with a reference mode adapter"); - } - else { - this.adapter = adapter; - } - } - else { - throw new Error("LokiPartitioningAdapter requires a (non-reference mode) adapter on construction"); - } - - // set collection paging defaults - if (!this.options.hasOwnProperty("paging")) { - this.options.paging = false; - } - - // default to page size of 25 megs (can be up to your largest serialized object size larger than this) - if (!this.options.hasOwnProperty("pageSize")) { - this.options.pageSize = 25*1024*1024; - } - - if (!this.options.hasOwnProperty("delimiter")) { - this.options.delimiter = '$<\n'; - } - } - - /** - * Loads a database which was partitioned into several key/value saves. - * (Loki persistence adapter interface function) - * - * @param {string} dbname - name of the database (filename/keyname) - * @param {function} callback - adapter callback to return load result to caller - * @memberof LokiPartitioningAdapter - */ - LokiPartitioningAdapter.prototype.loadDatabase = function (dbname, callback) { - var self=this; - this.dbname = dbname; - this.dbref = new Loki(dbname); - - // load the db container (without data) - this.adapter.loadDatabase(dbname, function(result) { - // empty database condition is for inner adapter return null/undefined/falsy - if (!result) { - // partition 0 not found so new database, no need to try to load other partitions. - // return same falsy result to loadDatabase to signify no database exists (yet) - callback(result); - return; - } - - if (typeof result !== "string") { - callback(new Error("LokiPartitioningAdapter received an unexpected response from inner adapter loadDatabase()")); - } - - // I will want to use loki destructuring helper methods so i will inflate into typed instance - var db = JSON.parse(result); - self.dbref.loadJSONObject(db); - db = null; - - var clen = self.dbref.collections.length; - - if (self.dbref.collections.length === 0) { - callback(self.dbref); - return; - } - - self.pageIterator = { - collection: 0, - pageIndex: 0 - }; - - self.loadNextPartition(0, function() { - callback(self.dbref); - }); - }); - }; - - /** - * Used to sequentially load each collection partition, one at a time. - * - * @param {int} partition - ordinal collection position to load next - * @param {function} callback - adapter callback to return load result to caller - */ - LokiPartitioningAdapter.prototype.loadNextPartition = function(partition, callback) { - var keyname = this.dbname + "." + partition; - var self=this; - - if (this.options.paging === true) { - this.pageIterator.pageIndex = 0; - this.loadNextPage(callback); - return; - } - - this.adapter.loadDatabase(keyname, function(result) { - var data = self.dbref.deserializeCollection(result, { delimited: true, collectionIndex: partition }); - self.dbref.collections[partition].data = data; - - if (++partition < self.dbref.collections.length) { - self.loadNextPartition(partition, callback); - } - else { - callback(); - } - }); - }; - - /** - * Used to sequentially load the next page of collection partition, one at a time. - * - * @param {function} callback - adapter callback to return load result to caller - */ - LokiPartitioningAdapter.prototype.loadNextPage = function(callback) { - // calculate name for next saved page in sequence - var keyname = this.dbname + "." + this.pageIterator.collection + "." + this.pageIterator.pageIndex; - var self=this; - - // load whatever page is next in sequence - this.adapter.loadDatabase(keyname, function(result) { - var data = result.split(self.options.delimiter); - result = ""; // free up memory now that we have split it into array - var dlen = data.length; - var idx; - - // detect if last page by presence of final empty string element and remove it if so - var isLastPage = (data[dlen-1] === ""); - if (isLastPage) { - data.pop(); - dlen = data.length; - // empty collections are just a delimiter meaning two blank items - if (data[dlen-1] === "" && dlen === 1) { - data.pop(); - dlen = data.length; - } - } - - // convert stringified array elements to object instances and push to collection data - for(idx=0; idx < dlen; idx++) { - self.dbref.collections[self.pageIterator.collection].data.push(JSON.parse(data[idx])); - data[idx] = null; - } - data = []; - - // if last page, we are done with this partition - if (isLastPage) { - - // if there are more partitions, kick off next partition load - if (++self.pageIterator.collection < self.dbref.collections.length) { - self.loadNextPartition(self.pageIterator.collection, callback); - } - else { - callback(); - } - } - else { - self.pageIterator.pageIndex++; - self.loadNextPage(callback); - } - }); - }; - - /** - * Saves a database by partioning into separate key/value saves. - * (Loki 'reference mode' persistence adapter interface function) - * - * @param {string} dbname - name of the database (filename/keyname) - * @param {object} dbref - reference to database which we will partition and save. - * @param {function} callback - adapter callback to return load result to caller - * - * @memberof LokiPartitioningAdapter - */ - LokiPartitioningAdapter.prototype.exportDatabase = function(dbname, dbref, callback) { - var self=this; - var idx, clen = dbref.collections.length; - - this.dbref = dbref; - this.dbname = dbname; - - // queue up dirty partitions to be saved - this.dirtyPartitions = [-1]; - for(idx=0; idx= cdlen) doneWithPartition = true; - } - // if our current page is bigger than defined pageSize, we are done with page - if (pageLen >= this.options.pageSize) doneWithPage = true; - - // if not done with current page, need delimiter before next item - // if done with partition we also want a delmiter to indicate 'end of pages' final empty row - if (!doneWithPage || doneWithPartition) { - pageBuilder += this.options.delimiter; - pageLen += delimlen; - } - - // if we are done with page save it and pass off to next recursive call or callback - if (doneWithPartition || doneWithPage) { - this.adapter.saveDatabase(keyname, pageBuilder, pageSaveCallback); - return; - } - } - }; - - /** - * A loki persistence adapter which persists using node fs module - * @constructor LokiFsAdapter - */ - function LokiFsAdapter() { - this.fs = require('fs'); - } - - /** - * loadDatabase() - Load data from file, will throw an error if the file does not exist - * @param {string} dbname - the filename of the database to load - * @param {function} callback - the callback to handle the result - * @memberof LokiFsAdapter - */ - LokiFsAdapter.prototype.loadDatabase = function loadDatabase(dbname, callback) { - var self = this; - - this.fs.stat(dbname, function (err, stats) { - if (!err && stats.isFile()) { - self.fs.readFile(dbname, { - encoding: 'utf8' - }, function readFileCallback(err, data) { - if (err) { - callback(new Error(err)); - } else { - callback(data); - } - }); - } - else { - callback(null); - } - }); - }; - - /** - * saveDatabase() - save data to file, will throw an error if the file can't be saved - * might want to expand this to avoid dataloss on partial save - * @param {string} dbname - the filename of the database to load - * @param {function} callback - the callback to handle the result - * @memberof LokiFsAdapter - */ - LokiFsAdapter.prototype.saveDatabase = function saveDatabase(dbname, dbstring, callback) { - var self = this; - var tmpdbname = dbname + '~'; - this.fs.writeFile(tmpdbname, dbstring, function writeFileCallback(err) { - if (err) { - callback(new Error(err)); - } else { - self.fs.rename(tmpdbname,dbname,callback); - } - }); - }; - - /** - * deleteDatabase() - delete the database file, will throw an error if the - * file can't be deleted - * @param {string} dbname - the filename of the database to delete - * @param {function} callback - the callback to handle the result - * @memberof LokiFsAdapter - */ - LokiFsAdapter.prototype.deleteDatabase = function deleteDatabase(dbname, callback) { - this.fs.unlink(dbname, function deleteDatabaseCallback(err) { - if (err) { - callback(new Error(err)); - } else { - callback(); - } - }); - }; - - - /** - * A loki persistence adapter which persists to web browser's local storage object - * @constructor LokiLocalStorageAdapter - */ - function LokiLocalStorageAdapter() {} - - /** - * loadDatabase() - Load data from localstorage - * @param {string} dbname - the name of the database to load - * @param {function} callback - the callback to handle the result - * @memberof LokiLocalStorageAdapter - */ - LokiLocalStorageAdapter.prototype.loadDatabase = function loadDatabase(dbname, callback) { - if (localStorageAvailable()) { - callback(localStorage.getItem(dbname)); - } else { - callback(new Error('localStorage is not available')); - } - }; - - /** - * saveDatabase() - save data to localstorage, will throw an error if the file can't be saved - * might want to expand this to avoid dataloss on partial save - * @param {string} dbname - the filename of the database to load - * @param {function} callback - the callback to handle the result - * @memberof LokiLocalStorageAdapter - */ - LokiLocalStorageAdapter.prototype.saveDatabase = function saveDatabase(dbname, dbstring, callback) { - if (localStorageAvailable()) { - localStorage.setItem(dbname, dbstring); - callback(null); - } else { - callback(new Error('localStorage is not available')); - } - }; - - /** - * deleteDatabase() - delete the database from localstorage, will throw an error if it - * can't be deleted - * @param {string} dbname - the filename of the database to delete - * @param {function} callback - the callback to handle the result - * @memberof LokiLocalStorageAdapter - */ - LokiLocalStorageAdapter.prototype.deleteDatabase = function deleteDatabase(dbname, callback) { - if (localStorageAvailable()) { - localStorage.removeItem(dbname); - callback(null); - } else { - callback(new Error('localStorage is not available')); - } - }; - - /** - * Wait for throttledSaves to complete and invoke your callback when drained or duration is met. - * - * @param {function} callback - callback to fire when save queue is drained, it is passed a sucess parameter value - * @param {object=} options - configuration options - * @param {boolean} options.recursiveWait - (default: true) if after queue is drained, another save was kicked off, wait for it - * @param {bool} options.recursiveWaitLimit - (default: false) limit our recursive waiting to a duration - * @param {int} options.recursiveWaitLimitDelay - (default: 2000) cutoff in ms to stop recursively re-draining - * @memberof Loki - */ - Loki.prototype.throttledSaveDrain = function(callback, options) { - var self = this; - var now = (new Date()).getTime(); - - if (!this.throttledSaves) { - callback(true); - } - - options = options || {}; - if (!options.hasOwnProperty('recursiveWait')) { - options.recursiveWait = true; - } - if (!options.hasOwnProperty('recursiveWaitLimit')) { - options.recursiveWaitLimit = false; - } - if (!options.hasOwnProperty('recursiveWaitLimitDuration')) { - options.recursiveWaitLimitDuration = 2000; - } - if (!options.hasOwnProperty('started')) { - options.started = (new Date()).getTime(); - } - - // if save is pending - if (this.throttledSaves && this.throttledSavePending) { - // if we want to wait until we are in a state where there are no pending saves at all - if (options.recursiveWait) { - // queue the following meta callback for when it completes - this.throttledCallbacks.push(function() { - // if there is now another save pending... - if (self.throttledSavePending) { - // if we wish to wait only so long and we have exceeded limit of our waiting, callback with false success value - if (options.recursiveWaitLimit && (now - options.started > options.recursiveWaitLimitDuration)) { - callback(false); - return; - } - // it must be ok to wait on next queue drain - self.throttledSaveDrain(callback, options); - return; - } - // no pending saves so callback with true success - else { - callback(true); - return; - } - }); - } - // just notify when current queue is depleted - else { - this.throttledCallbacks.push(callback); - return; - } - } - // no save pending, just callback - else { - callback(true); - } - }; - - /** - * Internal load logic, decoupled from throttling/contention logic - * - * @param {object} options - not currently used (remove or allow overrides?) - * @param {function=} callback - (Optional) user supplied async callback / error handler - */ - Loki.prototype.loadDatabaseInternal = function (options, callback) { - var cFun = callback || function (err, data) { - if (err) { - throw err; - } - }, - self = this; - - // the persistenceAdapter should be present if all is ok, but check to be sure. - if (this.persistenceAdapter !== null) { - - this.persistenceAdapter.loadDatabase(this.filename, function loadDatabaseCallback(dbString) { - if (typeof (dbString) === 'string') { - var parseSuccess = false; - try { - self.loadJSON(dbString, options || {}); - parseSuccess = true; - } catch (err) { - cFun(err); - } - if (parseSuccess) { - cFun(null); - self.emit('loaded', 'database ' + self.filename + ' loaded'); - } - } else { - // falsy result means new database - if (!dbString) { - cFun(null); - self.emit('loaded', 'empty database ' + self.filename + ' loaded'); - return; - } - - // instanceof error means load faulted - if (dbString instanceof Error) { - cFun(dbString); - return; - } - - // if adapter has returned an js object (other than null or error) attempt to load from JSON object - if (typeof (dbString) === "object") { - self.loadJSONObject(dbString, options || {}); - cFun(null); // return null on success - self.emit('loaded', 'database ' + self.filename + ' loaded'); - return; - } - - cFun("unexpected adapter response : " + dbString); - } - }); - - } else { - cFun(new Error('persistenceAdapter not configured')); - } - }; - - /** - * Handles manually loading from file system, local storage, or adapter (such as indexeddb) - * This method utilizes loki configuration options (if provided) to determine which - * persistence method to use, or environment detection (if configuration was not provided). - * To avoid contention with any throttledSaves, we will drain the save queue first. - * - * If you are configured with autosave, you do not need to call this method yourself. - * - * @param {object} options - if throttling saves and loads, this controls how we drain save queue before loading - * @param {boolean} options.recursiveWait - (default: true) wait recursively until no saves are queued - * @param {bool} options.recursiveWaitLimit - (default: false) limit our recursive waiting to a duration - * @param {int} options.recursiveWaitLimitDelay - (default: 2000) cutoff in ms to stop recursively re-draining - * @param {function=} callback - (Optional) user supplied async callback / error handler - * @memberof Loki - * @example - * db.loadDatabase({}, function(err) { - * if (err) { - * console.log("error : " + err); - * } - * else { - * console.log("database loaded."); - * } - * }); - */ - Loki.prototype.loadDatabase = function (options, callback) { - var self=this; - - // if throttling disabled, just call internal - if (!this.throttledSaves) { - this.loadDatabaseInternal(options, callback); - return; - } - - // try to drain any pending saves in the queue to lock it for loading - this.throttledSaveDrain(function(success) { - if (success) { - // pause/throttle saving until loading is done - self.throttledSavePending = true; - - self.loadDatabaseInternal(options, function(err) { - // now that we are finished loading, if no saves were throttled, disable flag - if (self.throttledCallbacks.length === 0) { - self.throttledSavePending = false; - } - // if saves requests came in while loading, kick off new save to kick off resume saves - else { - self.saveDatabase(); - } - - if (typeof callback === 'function') { - callback(err); - } - }); - return; - } - else { - if (typeof callback === 'function') { - callback(new Error("Unable to pause save throttling long enough to read database")); - } - } - }, options); - }; - - /** - * Internal save logic, decoupled from save throttling logic - */ - Loki.prototype.saveDatabaseInternal = function (callback) { - var cFun = callback || function (err) { - if (err) { - throw err; - } - return; - }, - self = this; - - // the persistenceAdapter should be present if all is ok, but check to be sure. - if (this.persistenceAdapter !== null) { - // check if the adapter is requesting (and supports) a 'reference' mode export - if (this.persistenceAdapter.mode === "reference" && typeof this.persistenceAdapter.exportDatabase === "function") { - // filename may seem redundant but loadDatabase will need to expect this same filename - this.persistenceAdapter.exportDatabase(this.filename, this.copy({removeNonSerializable:true}), function exportDatabaseCallback(err) { - self.autosaveClearFlags(); - cFun(err); - }); - } - // otherwise just pass the serialized database to adapter - else { - // persistenceAdapter might be asynchronous, so we must clear `dirty` immediately - // or autosave won't work if an update occurs between here and the callback - self.autosaveClearFlags(); - this.persistenceAdapter.saveDatabase(this.filename, self.serialize(), function saveDatabasecallback(err) { - cFun(err); - }); - } - } else { - cFun(new Error('persistenceAdapter not configured')); - } - }; - - /** - * Handles manually saving to file system, local storage, or adapter (such as indexeddb) - * This method utilizes loki configuration options (if provided) to determine which - * persistence method to use, or environment detection (if configuration was not provided). - * - * If you are configured with autosave, you do not need to call this method yourself. - * - * @param {function=} callback - (Optional) user supplied async callback / error handler - * @memberof Loki - * @example - * db.saveDatabase(function(err) { - * if (err) { - * console.log("error : " + err); - * } - * else { - * console.log("database saved."); - * } - * }); - */ - Loki.prototype.saveDatabase = function (callback) { - if (!this.throttledSaves) { - this.saveDatabaseInternal(callback); - return; - } - - if (this.throttledSavePending) { - this.throttledCallbacks.push(callback); - return; - } - - var localCallbacks = this.throttledCallbacks; - this.throttledCallbacks = []; - localCallbacks.unshift(callback); - this.throttledSavePending = true; - - var self = this; - this.saveDatabaseInternal(function(err) { - self.throttledSavePending = false; - localCallbacks.forEach(function(pcb) { - if (typeof pcb === 'function') { - // Queue the callbacks so we first finish this method execution - setTimeout(function() { - pcb(err); - }, 1); - } - }); - - // since this is called async, future requests may have come in, if so.. kick off next save - if (self.throttledCallbacks.length > 0) { - self.saveDatabase(); - } - }); - }; - - // alias - Loki.prototype.save = Loki.prototype.saveDatabase; - - /** - * Handles deleting a database from file system, local - * storage, or adapter (indexeddb) - * This method utilizes loki configuration options (if provided) to determine which - * persistence method to use, or environment detection (if configuration was not provided). - * - * @param {function=} callback - (Optional) user supplied async callback / error handler - * @memberof Loki - */ - Loki.prototype.deleteDatabase = function (options, callback) { - var cFun = callback || function (err, data) { - if (err) { - throw err; - } - }; - - // we aren't even using options, so we will support syntax where - // callback is passed as first and only argument - if (typeof options === 'function' && !callback) { - cFun = options; - } - - // the persistenceAdapter should be present if all is ok, but check to be sure. - if (this.persistenceAdapter !== null) { - this.persistenceAdapter.deleteDatabase(this.filename, function deleteDatabaseCallback(err) { - cFun(err); - }); - } else { - cFun(new Error('persistenceAdapter not configured')); - } - }; - - /** - * autosaveDirty - check whether any collections are 'dirty' meaning we need to save (entire) database - * - * @returns {boolean} - true if database has changed since last autosave, false if not. - */ - Loki.prototype.autosaveDirty = function () { - for (var idx = 0; idx < this.collections.length; idx++) { - if (this.collections[idx].dirty) { - return true; - } - } - - return false; - }; - - /** - * autosaveClearFlags - resets dirty flags on all collections. - * Called from saveDatabase() after db is saved. - * - */ - Loki.prototype.autosaveClearFlags = function () { - for (var idx = 0; idx < this.collections.length; idx++) { - this.collections[idx].dirty = false; - } - }; - - /** - * autosaveEnable - begin a javascript interval to periodically save the database. - * - * @param {object} options - not currently used (remove or allow overrides?) - * @param {function=} callback - (Optional) user supplied async callback - */ - Loki.prototype.autosaveEnable = function (options, callback) { - this.autosave = true; - - var delay = 5000, - self = this; - - if (typeof (this.autosaveInterval) !== 'undefined' && this.autosaveInterval !== null) { - delay = this.autosaveInterval; - } - - this.autosaveHandle = setInterval(function autosaveHandleInterval() { - // use of dirty flag will need to be hierarchical since mods are done at collection level with no visibility of 'db' - // so next step will be to implement collection level dirty flags set on insert/update/remove - // along with loki level isdirty() function which iterates all collections to see if any are dirty - - if (self.autosaveDirty()) { - self.saveDatabase(callback); - } - }, delay); - }; - - /** - * autosaveDisable - stop the autosave interval timer. - * - */ - Loki.prototype.autosaveDisable = function () { - if (typeof (this.autosaveHandle) !== 'undefined' && this.autosaveHandle !== null) { - clearInterval(this.autosaveHandle); - this.autosaveHandle = null; - } - }; - - - /** - * Resultset class allowing chainable queries. Intended to be instanced internally. - * Collection.find(), Collection.where(), and Collection.chain() instantiate this. - * - * @example - * mycollection.chain() - * .find({ 'doors' : 4 }) - * .where(function(obj) { return obj.name === 'Toyota' }) - * .data(); - * - * @constructor Resultset - * @param {Collection} collection - The collection which this Resultset will query against. - */ - function Resultset(collection, options) { - options = options || {}; - - // retain reference to collection we are querying against - this.collection = collection; - this.filteredrows = []; - this.filterInitialized = false; - - return this; - } - - /** - * reset() - Reset the resultset to its initial state. - * - * @returns {Resultset} Reference to this resultset, for future chain operations. - */ - Resultset.prototype.reset = function () { - if (this.filteredrows.length > 0) { - this.filteredrows = []; - } - this.filterInitialized = false; - return this; - }; - - /** - * toJSON() - Override of toJSON to avoid circular references - * - */ - Resultset.prototype.toJSON = function () { - var copy = this.copy(); - copy.collection = null; - return copy; - }; - - /** - * Allows you to limit the number of documents passed to next chain operation. - * A resultset copy() is made to avoid altering original resultset. - * - * @param {int} qty - The number of documents to return. - * @returns {Resultset} Returns a copy of the resultset, limited by qty, for subsequent chain ops. - * @memberof Resultset - */ - Resultset.prototype.limit = function (qty) { - // if this has no filters applied, we need to populate filteredrows first - if (!this.filterInitialized && this.filteredrows.length === 0) { - this.filteredrows = this.collection.prepareFullDocIndex(); - } - - var rscopy = new Resultset(this.collection); - rscopy.filteredrows = this.filteredrows.slice(0, qty); - rscopy.filterInitialized = true; - return rscopy; - }; - - /** - * Used for skipping 'pos' number of documents in the resultset. - * - * @param {int} pos - Number of documents to skip; all preceding documents are filtered out. - * @returns {Resultset} Returns a copy of the resultset, containing docs starting at 'pos' for subsequent chain ops. - * @memberof Resultset - */ - Resultset.prototype.offset = function (pos) { - // if this has no filters applied, we need to populate filteredrows first - if (!this.filterInitialized && this.filteredrows.length === 0) { - this.filteredrows = this.collection.prepareFullDocIndex(); - } - - var rscopy = new Resultset(this.collection); - rscopy.filteredrows = this.filteredrows.slice(pos); - rscopy.filterInitialized = true; - return rscopy; - }; - - /** - * copy() - To support reuse of resultset in branched query situations. - * - * @returns {Resultset} Returns a copy of the resultset (set) but the underlying document references will be the same. - * @memberof Resultset - */ - Resultset.prototype.copy = function () { - var result = new Resultset(this.collection); - - if (this.filteredrows.length > 0) { - result.filteredrows = this.filteredrows.slice(); - } - result.filterInitialized = this.filterInitialized; - - return result; - }; - - /** - * Alias of copy() - * @memberof Resultset - */ - Resultset.prototype.branch = Resultset.prototype.copy; - - /** - * transform() - executes a named collection transform or raw array of transform steps against the resultset. - * - * @param transform {(string|array)} - name of collection transform or raw transform array - * @param parameters {object=} - (Optional) object property hash of parameters, if the transform requires them. - * @returns {Resultset} either (this) resultset or a clone of of this resultset (depending on steps) - * @memberof Resultset - */ - Resultset.prototype.transform = function (transform, parameters) { - var idx, - step, - rs = this; - - // if transform is name, then do lookup first - if (typeof transform === 'string') { - if (this.collection.transforms.hasOwnProperty(transform)) { - transform = this.collection.transforms[transform]; - } - } - - // either they passed in raw transform array or we looked it up, so process - if (typeof transform !== 'object' || !Array.isArray(transform)) { - throw new Error("Invalid transform"); - } - - if (typeof parameters !== 'undefined') { - transform = Utils.resolveTransformParams(transform, parameters); - } - - for (idx = 0; idx < transform.length; idx++) { - step = transform[idx]; - - switch (step.type) { - case "find": - rs.find(step.value); - break; - case "where": - rs.where(step.value); - break; - case "simplesort": - rs.simplesort(step.property, step.desc); - break; - case "compoundsort": - rs.compoundsort(step.value); - break; - case "sort": - rs.sort(step.value); - break; - case "limit": - rs = rs.limit(step.value); - break; // limit makes copy so update reference - case "offset": - rs = rs.offset(step.value); - break; // offset makes copy so update reference - case "map": - rs = rs.map(step.value, step.dataOptions); - break; - case "eqJoin": - rs = rs.eqJoin(step.joinData, step.leftJoinKey, step.rightJoinKey, step.mapFun, step.dataOptions); - break; - // following cases break chain by returning array data so make any of these last in transform steps - case "mapReduce": - rs = rs.mapReduce(step.mapFunction, step.reduceFunction); - break; - // following cases update documents in current filtered resultset (use carefully) - case "update": - rs.update(step.value); - break; - case "remove": - rs.remove(); - break; - default: - break; - } - } - - return rs; - }; - - /** - * User supplied compare function is provided two documents to compare. (chainable) - * @example - * rslt.sort(function(obj1, obj2) { - * if (obj1.name === obj2.name) return 0; - * if (obj1.name > obj2.name) return 1; - * if (obj1.name < obj2.name) return -1; - * }); - * - * @param {function} comparefun - A javascript compare function used for sorting. - * @returns {Resultset} Reference to this resultset, sorted, for future chain operations. - * @memberof Resultset - */ - Resultset.prototype.sort = function (comparefun) { - // if this has no filters applied, just we need to populate filteredrows first - if (!this.filterInitialized && this.filteredrows.length === 0) { - this.filteredrows = this.collection.prepareFullDocIndex(); - } - - var wrappedComparer = - (function (userComparer, data) { - return function (a, b) { - return userComparer(data[a], data[b]); - }; - })(comparefun, this.collection.data); - - this.filteredrows.sort(wrappedComparer); - - return this; - }; - - /** - * Simpler, loose evaluation for user to sort based on a property name. (chainable). - * Sorting based on the same lt/gt helper functions used for binary indices. - * - * @param {string} propname - name of property to sort by. - * @param {bool=} isdesc - (Optional) If true, the property will be sorted in descending order - * @returns {Resultset} Reference to this resultset, sorted, for future chain operations. - * @memberof Resultset - */ - Resultset.prototype.simplesort = function (propname, isdesc) { - if (typeof (isdesc) === 'undefined') { - isdesc = false; - } - - // if this has no filters applied, just we need to populate filteredrows first - if (!this.filterInitialized && this.filteredrows.length === 0) { - // if we have a binary index and no other filters applied, we can use that instead of sorting (again) - if (this.collection.binaryIndices.hasOwnProperty(propname)) { - // make sure index is up-to-date - this.collection.ensureIndex(propname); - // copy index values into filteredrows - this.filteredrows = this.collection.binaryIndices[propname].values.slice(0); - - if (isdesc) { - this.filteredrows.reverse(); - } - - // we are done, return this (resultset) for further chain ops - return this; - } - // otherwise initialize array for sort below - else { - this.filteredrows = this.collection.prepareFullDocIndex(); - } - } - - var wrappedComparer = - (function (prop, desc, data) { - var val1, val2, arr; - return function (a, b) { - if (~prop.indexOf('.')) { - arr = prop.split('.'); - val1 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, data[a]); - val2 = arr.reduce(function(obj, i) { return obj && obj[i] || undefined; }, data[b]); - } else { - val1 = data[a][prop]; - val2 = data[b][prop]; - } - return sortHelper(val1, val2, desc); - }; - })(propname, isdesc, this.collection.data); - - this.filteredrows.sort(wrappedComparer); - - return this; - }; - - // experimental version of simplesort which will always attempt array intersect - // if index is defined on sort property and results are already filtered - Resultset.prototype.simplesort2 = function (propname, isdesc) { - if (typeof (isdesc) === 'undefined') { - isdesc = false; - } - - // if already filtered, but we want to leverage binary index on sort - if (this.collection.binaryIndices.hasOwnProperty(propname) && this.filterInitialized) { - var idx, len=this.filteredrows.length, fr=this.filteredrows; - var io = {}; - // set up hashobject for simple 'inclusion test' with existing (filtered) results - for(idx=0; idx 1) { - return this.find({ '$and': filters }, firstOnly); - } - } - - // apply no filters if they want all - if (!property || queryObject === 'getAll') { - if (firstOnly) { - this.filteredrows = (this.collection.data.length > 0)?[0]: []; - this.filterInitialized = true; - } - - return this; - } - - // injecting $and and $or expression tree evaluation here. - if (property === '$and' || property === '$or') { - this[property](queryObjectOp); - - // for chained find with firstonly, - if (firstOnly && this.filteredrows.length > 1) { - this.filteredrows = this.filteredrows.slice(0, 1); - } - - return this; - } - - // see if query object is in shorthand mode (assuming eq operator) - if (queryObjectOp === null || (typeof queryObjectOp !== 'object' || queryObjectOp instanceof Date)) { - operator = '$eq'; - value = queryObjectOp; - } else if (typeof queryObjectOp === 'object') { - for (key in queryObjectOp) { - if (hasOwnProperty.call(queryObjectOp, key)) { - operator = key; - value = queryObjectOp[key]; - break; - } - } - } else { - throw new Error('Do not know what you want to do.'); - } - - // for regex ops, precompile - if (operator === '$regex') { - if (Array.isArray(value)) { - value = new RegExp(value[0], value[1]); - } else if (!(value instanceof RegExp)) { - value = new RegExp(value); - } - } - - // if user is deep querying the object such as find('name.first': 'odin') - var usingDotNotation = (property.indexOf('.') !== -1); - - // if an index exists for the property being queried against, use it - // for now only enabling where it is the first filter applied and prop is indexed - var doIndexCheck = !usingDotNotation && !this.filterInitialized; - - if (doIndexCheck && this.collection.binaryIndices[property] && indexedOps[operator]) { - // this is where our lazy index rebuilding will take place - // basically we will leave all indexes dirty until we need them - // so here we will rebuild only the index tied to this property - // ensureIndex() will only rebuild if flagged as dirty since we are not passing force=true param - if (this.collection.adaptiveBinaryIndices !== true) { - this.collection.ensureIndex(property); - } - - searchByIndex = true; - index = this.collection.binaryIndices[property]; - } - - // the comparison function - var fun = LokiOps[operator]; - - // "shortcut" for collection data - var t = this.collection.data; - // filter data length - var i = 0, - len = 0; - - // Query executed differently depending on : - // - whether the property being queried has an index defined - // - if chained, we handle first pass differently for initial filteredrows[] population - // - // For performance reasons, each case has its own if block to minimize in-loop calculations - - var filter, rowIdx = 0; - - // If the filteredrows[] is already initialized, use it - if (this.filterInitialized) { - filter = this.filteredrows; - len = filter.length; - - // currently supporting dot notation for non-indexed conditions only - if (usingDotNotation) { - property = property.split('.'); - for(i=0; i obj2.name) return 1; - * if (obj1.name < obj2.name) return -1; - * }); - * - * @param {function} comparefun - a javascript compare function used for sorting - * @returns {DynamicView} this DynamicView object, for further chain ops. - * @memberof DynamicView - */ - DynamicView.prototype.applySort = function (comparefun) { - this.sortFunction = comparefun; - this.sortCriteria = null; - - this.queueSortPhase(); - - return this; - }; - - /** - * applySimpleSort() - Used to specify a property used for view translation. - * @example - * dv.applySimpleSort("name"); - * - * @param {string} propname - Name of property by which to sort. - * @param {boolean=} isdesc - (Optional) If true, the sort will be in descending order. - * @returns {DynamicView} this DynamicView object, for further chain ops. - * @memberof DynamicView - */ - DynamicView.prototype.applySimpleSort = function (propname, isdesc) { - this.sortCriteria = [ - [propname, isdesc || false] - ]; - this.sortFunction = null; - - this.queueSortPhase(); - - return this; - }; - - /** - * applySortCriteria() - Allows sorting a resultset based on multiple columns. - * @example - * // to sort by age and then name (both ascending) - * dv.applySortCriteria(['age', 'name']); - * // to sort by age (ascending) and then by name (descending) - * dv.applySortCriteria(['age', ['name', true]); - * // to sort by age (descending) and then by name (descending) - * dv.applySortCriteria(['age', true], ['name', true]); - * - * @param {array} properties - array of property names or subarray of [propertyname, isdesc] used evaluate sort order - * @returns {DynamicView} Reference to this DynamicView, sorted, for future chain operations. - * @memberof DynamicView - */ - DynamicView.prototype.applySortCriteria = function (criteria) { - this.sortCriteria = criteria; - this.sortFunction = null; - - this.queueSortPhase(); - - return this; - }; - - /** - * startTransaction() - marks the beginning of a transaction. - * - * @returns {DynamicView} this DynamicView object, for further chain ops. - */ - DynamicView.prototype.startTransaction = function () { - this.cachedresultset = this.resultset.copy(); - - return this; - }; - - /** - * commit() - commits a transaction. - * - * @returns {DynamicView} this DynamicView object, for further chain ops. - */ - DynamicView.prototype.commit = function () { - this.cachedresultset = null; - - return this; - }; - - /** - * rollback() - rolls back a transaction. - * - * @returns {DynamicView} this DynamicView object, for further chain ops. - */ - DynamicView.prototype.rollback = function () { - this.resultset = this.cachedresultset; - - if (this.options.persistent) { - // for now just rebuild the persistent dynamic view data in this worst case scenario - // (a persistent view utilizing transactions which get rolled back), we already know the filter so not too bad. - this.resultdata = this.resultset.data(); - - this.emit('rebuild', this); - } - - return this; - }; - - - /** - * Implementation detail. - * _indexOfFilterWithId() - Find the index of a filter in the pipeline, by that filter's ID. - * - * @param {(string|number)} uid - The unique ID of the filter. - * @returns {number}: index of the referenced filter in the pipeline; -1 if not found. - */ - DynamicView.prototype._indexOfFilterWithId = function (uid) { - if (typeof uid === 'string' || typeof uid === 'number') { - for (var idx = 0, len = this.filterPipeline.length; idx < len; idx += 1) { - if (uid === this.filterPipeline[idx].uid) { - return idx; - } - } - } - return -1; - }; - - /** - * Implementation detail. - * _addFilter() - Add the filter object to the end of view's filter pipeline and apply the filter to the resultset. - * - * @param {object} filter - The filter object. Refer to applyFilter() for extra details. - */ - DynamicView.prototype._addFilter = function (filter) { - this.filterPipeline.push(filter); - this.resultset[filter.type](filter.val); - }; - - /** - * reapplyFilters() - Reapply all the filters in the current pipeline. - * - * @returns {DynamicView} this DynamicView object, for further chain ops. - */ - DynamicView.prototype.reapplyFilters = function () { - this.resultset.reset(); - - this.cachedresultset = null; - if (this.options.persistent) { - this.resultdata = []; - this.resultsdirty = true; - } - - var filters = this.filterPipeline; - this.filterPipeline = []; - - for (var idx = 0, len = filters.length; idx < len; idx += 1) { - this._addFilter(filters[idx]); - } - - if (this.sortFunction || this.sortCriteria) { - this.queueSortPhase(); - } else { - this.queueRebuildEvent(); - } - - return this; - }; - - /** - * applyFilter() - Adds or updates a filter in the DynamicView filter pipeline - * - * @param {object} filter - A filter object to add to the pipeline. - * The object is in the format { 'type': filter_type, 'val', filter_param, 'uid', optional_filter_id } - * @returns {DynamicView} this DynamicView object, for further chain ops. - * @memberof DynamicView - */ - DynamicView.prototype.applyFilter = function (filter) { - var idx = this._indexOfFilterWithId(filter.uid); - if (idx >= 0) { - this.filterPipeline[idx] = filter; - return this.reapplyFilters(); - } - - this.cachedresultset = null; - if (this.options.persistent) { - this.resultdata = []; - this.resultsdirty = true; - } - - this._addFilter(filter); - - if (this.sortFunction || this.sortCriteria) { - this.queueSortPhase(); - } else { - this.queueRebuildEvent(); - } - - return this; - }; - - /** - * applyFind() - Adds or updates a mongo-style query option in the DynamicView filter pipeline - * - * @param {object} query - A mongo-style query object to apply to pipeline - * @param {(string|number)=} uid - Optional: The unique ID of this filter, to reference it in the future. - * @returns {DynamicView} this DynamicView object, for further chain ops. - * @memberof DynamicView - */ - DynamicView.prototype.applyFind = function (query, uid) { - this.applyFilter({ - type: 'find', - val: query, - uid: uid - }); - return this; - }; - - /** - * applyWhere() - Adds or updates a javascript filter function in the DynamicView filter pipeline - * - * @param {function} fun - A javascript filter function to apply to pipeline - * @param {(string|number)=} uid - Optional: The unique ID of this filter, to reference it in the future. - * @returns {DynamicView} this DynamicView object, for further chain ops. - * @memberof DynamicView - */ - DynamicView.prototype.applyWhere = function (fun, uid) { - this.applyFilter({ - type: 'where', - val: fun, - uid: uid - }); - return this; - }; - - /** - * removeFilter() - Remove the specified filter from the DynamicView filter pipeline - * - * @param {(string|number)} uid - The unique ID of the filter to be removed. - * @returns {DynamicView} this DynamicView object, for further chain ops. - * @memberof DynamicView - */ - DynamicView.prototype.removeFilter = function (uid) { - var idx = this._indexOfFilterWithId(uid); - if (idx < 0) { - throw new Error("Dynamic view does not contain a filter with ID: " + uid); - } - - this.filterPipeline.splice(idx, 1); - this.reapplyFilters(); - return this; - }; - - /** - * count() - returns the number of documents representing the current DynamicView contents. - * - * @returns {number} The number of documents representing the current DynamicView contents. - * @memberof DynamicView - */ - DynamicView.prototype.count = function () { - // in order to be accurate we will pay the minimum cost (and not alter dv state management) - // recurring resultset data resolutions should know internally its already up to date. - // for persistent data this will not update resultdata nor fire rebuild event. - if (this.resultsdirty) { - this.resultdata = this.resultset.data(); - } - - return this.resultset.count(); - }; - - /** - * data() - resolves and pending filtering and sorting, then returns document array as result. - * - * @param {object=} options - optional parameters to pass to resultset.data() if non-persistent - * @param {boolean} options.forceClones - Allows forcing the return of cloned objects even when - * the collection is not configured for clone object. - * @param {string} options.forceCloneMethod - Allows overriding the default or collection specified cloning method. - * Possible values include 'parse-stringify', 'jquery-extend-deep', 'shallow', 'shallow-assign' - * @param {bool} options.removeMeta - Will force clones and strip $loki and meta properties from documents - * @returns {array} An array of documents representing the current DynamicView contents. - * @memberof DynamicView - */ - DynamicView.prototype.data = function (options) { - // using final sort phase as 'catch all' for a few use cases which require full rebuild - if (this.sortDirty || this.resultsdirty) { - this.performSortPhase({ - suppressRebuildEvent: true - }); - } - return (this.options.persistent) ? (this.resultdata) : (this.resultset.data(options)); - }; - - /** - * queueRebuildEvent() - When the view is not sorted we may still wish to be notified of rebuild events. - * This event will throttle and queue a single rebuild event when batches of updates affect the view. - */ - DynamicView.prototype.queueRebuildEvent = function () { - if (this.rebuildPending) { - return; - } - this.rebuildPending = true; - - var self = this; - setTimeout(function () { - if (self.rebuildPending) { - self.rebuildPending = false; - self.emit('rebuild', self); - } - }, this.options.minRebuildInterval); - }; - - /** - * queueSortPhase : If the view is sorted we will throttle sorting to either : - * (1) passive - when the user calls data(), or - * (2) active - once they stop updating and yield js thread control - */ - DynamicView.prototype.queueSortPhase = function () { - // already queued? exit without queuing again - if (this.sortDirty) { - return; - } - this.sortDirty = true; - - var self = this; - if (this.options.sortPriority === "active") { - // active sorting... once they are done and yield js thread, run async performSortPhase() - setTimeout(function () { - self.performSortPhase(); - }, this.options.minRebuildInterval); - } else { - // must be passive sorting... since not calling performSortPhase (until data call), lets use queueRebuildEvent to - // potentially notify user that data has changed. - this.queueRebuildEvent(); - } - }; - - /** - * performSortPhase() - invoked synchronously or asynchronously to perform final sort phase (if needed) - * - */ - DynamicView.prototype.performSortPhase = function (options) { - // async call to this may have been pre-empted by synchronous call to data before async could fire - if (!this.sortDirty && !this.resultsdirty) { - return; - } - - options = options || {}; - - if (this.sortDirty) { - if (this.sortFunction) { - this.resultset.sort(this.sortFunction); - } else if (this.sortCriteria) { - this.resultset.compoundsort(this.sortCriteria); - } - - this.sortDirty = false; - } - - if (this.options.persistent) { - // persistent view, rebuild local resultdata array - this.resultdata = this.resultset.data(); - this.resultsdirty = false; - } - - if (!options.suppressRebuildEvent) { - this.emit('rebuild', this); - } - }; - - /** - * evaluateDocument() - internal method for (re)evaluating document inclusion. - * Called by : collection.insert() and collection.update(). - * - * @param {int} objIndex - index of document to (re)run through filter pipeline. - * @param {bool} isNew - true if the document was just added to the collection. - */ - DynamicView.prototype.evaluateDocument = function (objIndex, isNew) { - // if no filter applied yet, the result 'set' should remain 'everything' - if (!this.resultset.filterInitialized) { - if (this.options.persistent) { - this.resultdata = this.resultset.data(); - } - // need to re-sort to sort new document - if (this.sortFunction || this.sortCriteria) { - this.queueSortPhase(); - } else { - this.queueRebuildEvent(); - } - return; - } - - var ofr = this.resultset.filteredrows; - var oldPos = (isNew) ? (-1) : (ofr.indexOf(+objIndex)); - var oldlen = ofr.length; - - // creating a 1-element resultset to run filter chain ops on to see if that doc passes filters; - // mostly efficient algorithm, slight stack overhead price (this function is called on inserts and updates) - var evalResultset = new Resultset(this.collection); - evalResultset.filteredrows = [objIndex]; - evalResultset.filterInitialized = true; - var filter; - for (var idx = 0, len = this.filterPipeline.length; idx < len; idx++) { - filter = this.filterPipeline[idx]; - evalResultset[filter.type](filter.val); - } - - // not a true position, but -1 if not pass our filter(s), 0 if passed filter(s) - var newPos = (evalResultset.filteredrows.length === 0) ? -1 : 0; - - // wasn't in old, shouldn't be now... do nothing - if (oldPos === -1 && newPos === -1) return; - - // wasn't in resultset, should be now... add - if (oldPos === -1 && newPos !== -1) { - ofr.push(objIndex); - - if (this.options.persistent) { - this.resultdata.push(this.collection.data[objIndex]); - } - - // need to re-sort to sort new document - if (this.sortFunction || this.sortCriteria) { - this.queueSortPhase(); - } else { - this.queueRebuildEvent(); - } - - return; - } - - // was in resultset, shouldn't be now... delete - if (oldPos !== -1 && newPos === -1) { - if (oldPos < oldlen - 1) { - ofr.splice(oldPos, 1); - - if (this.options.persistent) { - this.resultdata.splice(oldPos, 1); - } - } else { - ofr.length = oldlen - 1; - - if (this.options.persistent) { - this.resultdata.length = oldlen - 1; - } - } - - // in case changes to data altered a sort column - if (this.sortFunction || this.sortCriteria) { - this.queueSortPhase(); - } else { - this.queueRebuildEvent(); - } - - return; - } - - // was in resultset, should still be now... (update persistent only?) - if (oldPos !== -1 && newPos !== -1) { - if (this.options.persistent) { - // in case document changed, replace persistent view data with the latest collection.data document - this.resultdata[oldPos] = this.collection.data[objIndex]; - } - - // in case changes to data altered a sort column - if (this.sortFunction || this.sortCriteria) { - this.queueSortPhase(); - } else { - this.queueRebuildEvent(); - } - - return; - } - }; - - /** - * removeDocument() - internal function called on collection.delete() - */ - DynamicView.prototype.removeDocument = function (objIndex) { - // if no filter applied yet, the result 'set' should remain 'everything' - if (!this.resultset.filterInitialized) { - if (this.options.persistent) { - this.resultdata = this.resultset.data(); - } - // in case changes to data altered a sort column - if (this.sortFunction || this.sortCriteria) { - this.queueSortPhase(); - } else { - this.queueRebuildEvent(); - } - return; - } - - var ofr = this.resultset.filteredrows; - var oldPos = ofr.indexOf(+objIndex); - var oldlen = ofr.length; - var idx; - - if (oldPos !== -1) { - // if not last row in resultdata, swap last to hole and truncate last row - if (oldPos < oldlen - 1) { - ofr[oldPos] = ofr[oldlen - 1]; - ofr.length = oldlen - 1; - - if (this.options.persistent) { - this.resultdata[oldPos] = this.resultdata[oldlen - 1]; - this.resultdata.length = oldlen - 1; - } - } - // last row, so just truncate last row - else { - ofr.length = oldlen - 1; - - if (this.options.persistent) { - this.resultdata.length = oldlen - 1; - } - } - - // in case changes to data altered a sort column - if (this.sortFunction || this.sortCriteria) { - this.queueSortPhase(); - } else { - this.queueRebuildEvent(); - } - } - - // since we are using filteredrows to store data array positions - // if they remove a document (whether in our view or not), - // we need to adjust array positions -1 for all document array references after that position - oldlen = ofr.length; - for (idx = 0; idx < oldlen; idx++) { - if (ofr[idx] > objIndex) { - ofr[idx]--; - } - } - }; - - /** - * mapReduce() - data transformation via user supplied functions - * - * @param {function} mapFunction - this function accepts a single document for you to transform and return - * @param {function} reduceFunction - this function accepts many (array of map outputs) and returns single value - * @returns The output of your reduceFunction - * @memberof DynamicView - */ - DynamicView.prototype.mapReduce = function (mapFunction, reduceFunction) { - try { - return reduceFunction(this.data().map(mapFunction)); - } catch (err) { - throw err; - } - }; - - - /** - * Collection class that handles documents of same type - * @constructor Collection - * @implements LokiEventEmitter - * @param {string} name - collection name - * @param {(array|object)=} options - (optional) array of property names to be indicized OR a configuration object - * @param {array=} [options.unique=[]] - array of property names to define unique constraints for - * @param {array=} [options.exact=[]] - array of property names to define exact constraints for - * @param {array=} [options.indices=[]] - array property names to define binary indexes for - * @param {boolean} [options.adaptiveBinaryIndices=true] - collection indices will be actively rebuilt rather than lazily - * @param {boolean} [options.asyncListeners=false] - whether listeners are invoked asynchronously - * @param {boolean} [options.disableMeta=false] - set to true to disable meta property on documents - * @param {boolean} [options.disableChangesApi=true] - set to false to enable Changes API - * @param {boolean} [options.disableDeltaChangesApi=true] - set to false to enable Delta Changes API (requires Changes API, forces cloning) - * @param {boolean} [options.autoupdate=false] - use Object.observe to update objects automatically - * @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user - * @param {boolean} [options.serializableIndices=true[]] - converts date values on binary indexed properties to epoch time - * @param {string} [options.cloneMethod='parse-stringify'] - 'parse-stringify', 'jquery-extend-deep', 'shallow', 'shallow-assign' - * @param {int=} options.ttl - age of document (in ms.) before document is considered aged/stale. - * @param {int=} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default. - * @see {@link Loki#addCollection} for normal creation of collections - */ - function Collection(name, options) { - // the name of the collection - - this.name = name; - // the data held by the collection - this.data = []; - this.idIndex = []; // index of id - this.binaryIndices = {}; // user defined indexes - this.constraints = { - unique: {}, - exact: {} - }; - - // unique contraints contain duplicate object references, so they are not persisted. - // we will keep track of properties which have unique contraint applied here, and regenerate on load - this.uniqueNames = []; - - // transforms will be used to store frequently used query chains as a series of steps - // which itself can be stored along with the database. - this.transforms = {}; - - // the object type of the collection - this.objType = name; - - // in autosave scenarios we will use collection level dirty flags to determine whether save is needed. - // currently, if any collection is dirty we will autosave the whole database if autosave is configured. - // defaulting to true since this is called from addCollection and adding a collection should trigger save - this.dirty = true; - - // private holders for cached data - this.cachedIndex = null; - this.cachedBinaryIndex = null; - this.cachedData = null; - var self = this; - - /* OPTIONS */ - options = options || {}; - - // exact match and unique constraints - if (options.hasOwnProperty('unique')) { - if (!Array.isArray(options.unique)) { - options.unique = [options.unique]; - } - options.unique.forEach(function (prop) { - self.uniqueNames.push(prop); // used to regenerate on subsequent database loads - self.constraints.unique[prop] = new UniqueIndex(prop); - }); - } - - if (options.hasOwnProperty('exact')) { - options.exact.forEach(function (prop) { - self.constraints.exact[prop] = new ExactIndex(prop); - }); - } - - // if set to true we will optimally keep indices 'fresh' during insert/update/remove ops (never dirty/never needs rebuild) - // if you frequently intersperse insert/update/remove ops between find ops this will likely be significantly faster option. - this.adaptiveBinaryIndices = options.hasOwnProperty('adaptiveBinaryIndices') ? options.adaptiveBinaryIndices : true; - - // is collection transactional - this.transactional = options.hasOwnProperty('transactional') ? options.transactional : false; - - // options to clone objects when inserting them - this.cloneObjects = options.hasOwnProperty('clone') ? options.clone : false; - - // default clone method (if enabled) is parse-stringify - this.cloneMethod = options.hasOwnProperty('cloneMethod') ? options.cloneMethod : "parse-stringify"; - - // option to make event listeners async, default is sync - this.asyncListeners = options.hasOwnProperty('asyncListeners') ? options.asyncListeners : false; - - // if set to true we will not maintain a meta property for a document - this.disableMeta = options.hasOwnProperty('disableMeta') ? options.disableMeta : false; - - // disable track changes - this.disableChangesApi = options.hasOwnProperty('disableChangesApi') ? options.disableChangesApi : true; - - // disable delta update object style on changes - this.disableDeltaChangesApi = options.hasOwnProperty('disableDeltaChangesApi') ? options.disableDeltaChangesApi : true; - if (this.disableChangesApi) { this.disableDeltaChangesApi = true; } - - // option to observe objects and update them automatically, ignored if Object.observe is not supported - this.autoupdate = options.hasOwnProperty('autoupdate') ? options.autoupdate : false; - - // by default, if you insert a document into a collection with binary indices, if those indexed properties contain - // a DateTime we will convert to epoch time format so that (across serializations) its value position will be the - // same 'after' serialization as it was 'before'. - this.serializableIndices = options.hasOwnProperty('serializableIndices') ? options.serializableIndices : true; - - //option to activate a cleaner daemon - clears "aged" documents at set intervals. - this.ttl = { - age: null, - ttlInterval: null, - daemon: null - }; - this.setTTL(options.ttl || -1, options.ttlInterval); - - // currentMaxId - change manually at your own peril! - this.maxId = 0; - - this.DynamicViews = []; - - // events - this.events = { - 'insert': [], - 'update': [], - 'pre-insert': [], - 'pre-update': [], - 'close': [], - 'flushbuffer': [], - 'error': [], - 'delete': [], - 'warning': [] - }; - - // changes are tracked by collection and aggregated by the db - this.changes = []; - - // initialize the id index - this.ensureId(); - var indices = []; - // initialize optional user-supplied indices array ['age', 'lname', 'zip'] - if (options && options.indices) { - if (Object.prototype.toString.call(options.indices) === '[object Array]') { - indices = options.indices; - } else if (typeof options.indices === 'string') { - indices = [options.indices]; - } else { - throw new TypeError('Indices needs to be a string or an array of strings'); - } - } - - for (var idx = 0; idx < indices.length; idx++) { - this.ensureIndex(indices[idx]); - } - - function observerCallback(changes) { - - var changedObjects = typeof Set === 'function' ? new Set() : []; - - if (!changedObjects.add) - changedObjects.add = function (object) { - if (this.indexOf(object) === -1) - this.push(object); - return this; - }; - - changes.forEach(function (change) { - changedObjects.add(change.object); - }); - - changedObjects.forEach(function (object) { - if (!hasOwnProperty.call(object, '$loki')) - return self.removeAutoUpdateObserver(object); - try { - self.update(object); - } catch (err) {} - }); - } - - this.observerCallback = observerCallback; - - /* - * This method creates a clone of the current status of an object and associates operation and collection name, - * so the parent db can aggregate and generate a changes object for the entire db - */ - function createChange(name, op, obj, old) { - self.changes.push({ - name: name, - operation: op, - obj: op == 'U' && !self.disableDeltaChangesApi ? getChangeDelta(obj, old) : JSON.parse(JSON.stringify(obj)) - }); - } - - //Compare changed object (which is a forced clone) with existing object and return the delta - function getChangeDelta(obj, old) { - if (old) { - return getObjectDelta(old, obj); - } - else { - return JSON.parse(JSON.stringify(obj)); - } - } - - this.getChangeDelta = getChangeDelta; - - function getObjectDelta(oldObject, newObject) { - var propertyNames = newObject !== null && typeof newObject === 'object' ? Object.keys(newObject) : null; - if (propertyNames && propertyNames.length && ['string', 'boolean', 'number'].indexOf(typeof(newObject)) < 0) { - var delta = {}; - for (var i = 0; i < propertyNames.length; i++) { - var propertyName = propertyNames[i]; - if (newObject.hasOwnProperty(propertyName)) { - if (!oldObject.hasOwnProperty(propertyName) || self.uniqueNames.indexOf(propertyName) >= 0 || propertyName == '$loki' || propertyName == 'meta') { - delta[propertyName] = newObject[propertyName]; - } - else { - var propertyDelta = getObjectDelta(oldObject[propertyName], newObject[propertyName]); - if (typeof propertyDelta !== "undefined" && propertyDelta != {}) { - delta[propertyName] = propertyDelta; - } - } - } - } - return Object.keys(delta).length === 0 ? undefined : delta; - } - else { - return oldObject === newObject ? undefined : newObject; - } - } - - this.getObjectDelta = getObjectDelta; - - // clear all the changes - function flushChanges() { - self.changes = []; - } - - this.getChanges = function () { - return self.changes; - }; - - this.flushChanges = flushChanges; - - /** - * If the changes API is disabled make sure only metadata is added without re-evaluating everytime if the changesApi is enabled - */ - function insertMeta(obj) { - var len, idx; - - if (self.disableMeta || !obj) { - return; - } - - // if batch insert - if (Array.isArray(obj)) { - len = obj.length; - - for(idx=0; idx 0; - - if (adaptiveBatchOverride) { - this.adaptiveBinaryIndices = false; - } - - for (k; k < len; k += 1) { - this.update(doc[k]); - } - - if (adaptiveBatchOverride) { - this.ensureAllIndexes(); - this.adaptiveBinaryIndices = true; - } - - return; - } - - // verify object is a properly formed document - if (!hasOwnProperty.call(doc, '$loki')) { - throw new Error('Trying to update unsynced document. Please save the document first by using insert() or addMany()'); - } - try { - this.startTransaction(); - var arr = this.get(doc.$loki, true), - oldInternal, // ref to existing obj - newInternal, // ref to new internal obj - position, - self = this; - - if (!arr) { - throw new Error('Trying to update a document not in collection.'); - } - - oldInternal = arr[0]; // -internal- obj ref - position = arr[1]; // position in data array - - // if configured to clone, do so now... otherwise just use same obj reference - newInternal = this.cloneObjects || !this.disableDeltaChangesApi ? clone(doc, this.cloneMethod) : doc; - - this.emit('pre-update', doc); - - Object.keys(this.constraints.unique).forEach(function (key) { - self.constraints.unique[key].update(oldInternal, newInternal); - }); - - // operate the update - this.data[position] = newInternal; - - if (newInternal !== doc) { - this.addAutoUpdateObserver(doc); - } - - // now that we can efficiently determine the data[] position of newly added document, - // submit it for all registered DynamicViews to evaluate for inclusion/exclusion - for (var idx = 0; idx < this.DynamicViews.length; idx++) { - this.DynamicViews[idx].evaluateDocument(position, false); - } - - var key; - if (this.adaptiveBinaryIndices) { - // for each binary index defined in collection, immediately update rather than flag for lazy rebuild - var bIndices = this.binaryIndices; - for (key in bIndices) { - this.adaptiveBinaryIndexUpdate(position, key); - } - } - else { - this.flagBinaryIndexesDirty(); - } - - this.idIndex[position] = newInternal.$loki; - //this.flagBinaryIndexesDirty(); - - this.commit(); - this.dirty = true; // for autosave scenarios - - this.emit('update', doc, this.cloneObjects || !this.disableDeltaChangesApi ? clone(oldInternal, this.cloneMethod) : null); - return doc; - } catch (err) { - this.rollback(); - this.console.error(err.message); - this.emit('error', err); - throw (err); // re-throw error so user does not think it succeeded - } - }; - - /** - * Add object to collection - */ - Collection.prototype.add = function (obj) { - // if parameter isn't object exit with throw - if ('object' !== typeof obj) { - throw new TypeError('Object being added needs to be an object'); - } - // if object you are adding already has id column it is either already in the collection - // or the object is carrying its own 'id' property. If it also has a meta property, - // then this is already in collection so throw error, otherwise rename to originalId and continue adding. - if (typeof (obj.$loki) !== 'undefined') { - throw new Error('Document is already in collection, please use update()'); - } - - /* - * try adding object to collection - */ - try { - this.startTransaction(); - this.maxId++; - - if (isNaN(this.maxId)) { - this.maxId = (this.data[this.data.length - 1].$loki + 1); - } - - obj.$loki = this.maxId; - - if (!this.disableMeta) { - obj.meta.version = 0; - } - - var key, constrUnique = this.constraints.unique; - for (key in constrUnique) { - if (hasOwnProperty.call(constrUnique, key)) { - constrUnique[key].set(obj); - } - } - - // add new obj id to idIndex - this.idIndex.push(obj.$loki); - - // add the object - this.data.push(obj); - - var addedPos = this.data.length - 1; - - // now that we can efficiently determine the data[] position of newly added document, - // submit it for all registered DynamicViews to evaluate for inclusion/exclusion - var dvlen = this.DynamicViews.length; - for (var i = 0; i < dvlen; i++) { - this.DynamicViews[i].evaluateDocument(addedPos, true); - } - - if (this.adaptiveBinaryIndices) { - // for each binary index defined in collection, immediately update rather than flag for lazy rebuild - var bIndices = this.binaryIndices; - for (key in bIndices) { - this.adaptiveBinaryIndexInsert(addedPos, key); - } - } - else { - this.flagBinaryIndexesDirty(); - } - - this.commit(); - this.dirty = true; // for autosave scenarios - - return (this.cloneObjects) ? (clone(obj, this.cloneMethod)) : (obj); - } catch (err) { - this.rollback(); - this.console.error(err.message); - this.emit('error', err); - throw (err); // re-throw error so user does not think it succeeded - } - }; - - /** - * Applies a filter function and passes all results to an update function. - * - * @param {function} filterFunction - filter function whose results will execute update - * @param {function} updateFunction - update function to run against filtered documents - * @memberof Collection - */ - Collection.prototype.updateWhere = function(filterFunction, updateFunction) { - var results = this.where(filterFunction), - i = 0, - obj; - try { - for (i; i < results.length; i++) { - obj = updateFunction(results[i]); - this.update(obj); - } - - } catch (err) { - this.rollback(); - this.console.error(err.message); - } - }; - - /** - * Remove all documents matching supplied filter function. - * For 'mongo-like' querying you should migrate to [findAndRemove()]{@link Collection#findAndRemove}. - * @param {function|object} query - query object to filter on - * @memberof Collection - */ - Collection.prototype.removeWhere = function (query) { - var list; - if (typeof query === 'function') { - list = this.data.filter(query); - this.remove(list); - } else { - this.chain().find(query).remove(); - } - }; - - Collection.prototype.removeDataOnly = function () { - this.remove(this.data.slice()); - }; - - /** - * Remove a document from the collection - * @param {object} doc - document to remove from collection - * @memberof Collection - */ - Collection.prototype.remove = function (doc) { - if (typeof doc === 'number') { - doc = this.get(doc); - } - - if ('object' !== typeof doc) { - throw new Error('Parameter is not an object'); - } - if (Array.isArray(doc)) { - var k = 0, - len = doc.length; - for (k; k < len; k += 1) { - this.remove(doc[k]); - } - return; - } - - if (!hasOwnProperty.call(doc, '$loki')) { - throw new Error('Object is not a document stored in the collection'); - } - - try { - this.startTransaction(); - var arr = this.get(doc.$loki, true), - // obj = arr[0], - position = arr[1]; - var self = this; - Object.keys(this.constraints.unique).forEach(function (key) { - if (doc[key] !== null && typeof doc[key] !== 'undefined') { - self.constraints.unique[key].remove(doc[key]); - } - }); - // now that we can efficiently determine the data[] position of newly added document, - // submit it for all registered DynamicViews to remove - for (var idx = 0; idx < this.DynamicViews.length; idx++) { - this.DynamicViews[idx].removeDocument(position); - } - - if (this.adaptiveBinaryIndices) { - // for each binary index defined in collection, immediately update rather than flag for lazy rebuild - var key, bIndices = this.binaryIndices; - for (key in bIndices) { - this.adaptiveBinaryIndexRemove(position, key); - } - } - else { - this.flagBinaryIndexesDirty(); - } - - this.data.splice(position, 1); - this.removeAutoUpdateObserver(doc); - - // remove id from idIndex - this.idIndex.splice(position, 1); - - this.commit(); - this.dirty = true; // for autosave scenarios - this.emit('delete', arr[0]); - delete doc.$loki; - delete doc.meta; - return doc; - - } catch (err) { - this.rollback(); - this.console.error(err.message); - this.emit('error', err); - return null; - } - }; - - /*---------------------+ - | Finding methods | - +----------------------*/ - - /** - * Get by Id - faster than other methods because of the searching algorithm - * @param {int} id - $loki id of document you want to retrieve - * @param {boolean} returnPosition - if 'true' we will return [object, position] - * @returns {(object|array|null)} Object reference if document was found, null if not, - * or an array if 'returnPosition' was passed. - * @memberof Collection - */ - Collection.prototype.get = function (id, returnPosition) { - var retpos = returnPosition || false, - data = this.idIndex, - max = data.length - 1, - min = 0, - mid = (min + max) >> 1; - - id = typeof id === 'number' ? id : parseInt(id, 10); - - if (isNaN(id)) { - throw new TypeError('Passed id is not an integer'); - } - - while (data[min] < data[max]) { - mid = (min + max) >> 1; - - if (data[mid] < id) { - min = mid + 1; - } else { - max = mid; - } - } - - if (max === min && data[min] === id) { - if (retpos) { - return [this.data[min], min]; - } - return this.data[min]; - } - return null; - - }; - - /** - * Perform binary range lookup for the data[dataPosition][binaryIndexName] property value - * Since multiple documents may contain the same value (which the index is sorted on), - * we hone in on range and then linear scan range to find exact index array position. - * @param {int} dataPosition : coll.data array index/position - * @param {string} binaryIndexName : index to search for dataPosition in - */ - Collection.prototype.getBinaryIndexPosition = function(dataPosition, binaryIndexName) { - var val = this.data[dataPosition][binaryIndexName]; - var index = this.binaryIndices[binaryIndexName].values; - - // i think calculateRange can probably be moved to collection - // as it doesn't seem to need resultset. need to verify - //var rs = new Resultset(this, null, null); - var range = this.calculateRange("$eq", binaryIndexName, val); - - if (range[0] === 0 && range[1] === -1) { - // uhoh didn't find range - return null; - } - - var min = range[0]; - var max = range[1]; - - // narrow down the sub-segment of index values - // where the indexed property value exactly matches our - // value and then linear scan to find exact -index- position - for(var idx = min; idx <= max; idx++) { - if (index[idx] === dataPosition) return idx; - } - - // uhoh - return null; - }; - - /** - * Adaptively insert a selected item to the index. - * @param {int} dataPosition : coll.data array index/position - * @param {string} binaryIndexName : index to search for dataPosition in - */ - Collection.prototype.adaptiveBinaryIndexInsert = function(dataPosition, binaryIndexName) { - var index = this.binaryIndices[binaryIndexName].values; - var val = this.data[dataPosition][binaryIndexName]; - - // If you are inserting a javascript Date value into a binary index, convert to epoch time - if (this.serializableIndices === true && val instanceof Date) { - this.data[dataPosition][binaryIndexName] = val.getTime(); - val = this.data[dataPosition][binaryIndexName]; - } - - var idxPos = (index.length === 0)?0:this.calculateRangeStart(binaryIndexName, val, true); - - // insert new data index into our binary index at the proper sorted location for relevant property calculated by idxPos. - // doing this after adjusting dataPositions so no clash with previous item at that position. - this.binaryIndices[binaryIndexName].values.splice(idxPos, 0, dataPosition); - }; - - /** - * Adaptively update a selected item within an index. - * @param {int} dataPosition : coll.data array index/position - * @param {string} binaryIndexName : index to search for dataPosition in - */ - Collection.prototype.adaptiveBinaryIndexUpdate = function(dataPosition, binaryIndexName) { - // linear scan needed to find old position within index unless we optimize for clone scenarios later - // within (my) node 5.6.0, the following for() loop with strict compare is -much- faster than indexOf() - var idxPos, - index = this.binaryIndices[binaryIndexName].values, - len=index.length; - - for(idxPos=0; idxPos < len; idxPos++) { - if (index[idxPos] === dataPosition) break; - } - - //var idxPos = this.binaryIndices[binaryIndexName].values.indexOf(dataPosition); - this.binaryIndices[binaryIndexName].values.splice(idxPos, 1); - - //this.adaptiveBinaryIndexRemove(dataPosition, binaryIndexName, true); - this.adaptiveBinaryIndexInsert(dataPosition, binaryIndexName); - }; - - /** - * Adaptively remove a selected item from the index. - * @param {int} dataPosition : coll.data array index/position - * @param {string} binaryIndexName : index to search for dataPosition in - */ - Collection.prototype.adaptiveBinaryIndexRemove = function(dataPosition, binaryIndexName, removedFromIndexOnly) { - var idxPos = this.getBinaryIndexPosition(dataPosition, binaryIndexName); - var index = this.binaryIndices[binaryIndexName].values; - var len, - idx; - - if (idxPos === null) { - // throw new Error('unable to determine binary index position'); - return null; - } - - // remove document from index - this.binaryIndices[binaryIndexName].values.splice(idxPos, 1); - - // if we passed this optional flag parameter, we are calling from adaptiveBinaryIndexUpdate, - // in which case data positions stay the same. - if (removedFromIndexOnly === true) { - return; - } - - // since index stores data array positions, if we remove a document - // we need to adjust array positions -1 for all document positions greater than removed position - len = index.length; - for (idx = 0; idx < len; idx++) { - if (index[idx] > dataPosition) { - index[idx]--; - } - } - }; - - /** - * Internal method used for index maintenance and indexed searching. - * Calculates the beginning of an index range for a given value. - * For index maintainance (adaptive:true), we will return a valid index position to insert to. - * For querying (adaptive:false/undefined), we will : - * return lower bound/index of range of that value (if found) - * return next lower index position if not found (hole) - * If index is empty it is assumed to be handled at higher level, so - * this method assumes there is at least 1 document in index. - * - * @param {string} prop - name of property which has binary index - * @param {any} val - value to find within index - * @param {bool?} adaptive - if true, we will return insert position - */ - Collection.prototype.calculateRangeStart = function (prop, val, adaptive) { - var rcd = this.data; - var index = this.binaryIndices[prop].values; - var min = 0; - var max = index.length - 1; - var mid = 0; - - if (index.length === 0) { - return -1; - } - - var minVal = rcd[index[min]][prop]; - var maxVal = rcd[index[max]][prop]; - - // hone in on start position of value - while (min < max) { - mid = (min + max) >> 1; - - if (ltHelper(rcd[index[mid]][prop], val, false)) { - min = mid + 1; - } else { - max = mid; - } - } - - var lbound = min; - - // found it... return it - if (aeqHelper(val, rcd[index[lbound]][prop])) { - return lbound; - } - - // if not in index and our value is less than the found one - if (ltHelper(val, rcd[index[lbound]][prop], false)) { - return adaptive?lbound:lbound-1; - } - - // not in index and our value is greater than the found one - return adaptive?lbound+1:lbound; - }; - - /** - * Internal method used for indexed $between. Given a prop (index name), and a value - * (which may or may not yet exist) this will find the final position of that upper range value. - */ - Collection.prototype.calculateRangeEnd = function (prop, val) { - var rcd = this.data; - var index = this.binaryIndices[prop].values; - var min = 0; - var max = index.length - 1; - var mid = 0; - - if (index.length === 0) { - return -1; - } - - var minVal = rcd[index[min]][prop]; - var maxVal = rcd[index[max]][prop]; - - // hone in on start position of value - while (min < max) { - mid = (min + max) >> 1; - - if (ltHelper(val, rcd[index[mid]][prop], false)) { - max = mid; - } else { - min = mid + 1; - } - } - - var ubound = max; - - // only eq if last element in array is our val - if (aeqHelper(val, rcd[index[ubound]][prop])) { - return ubound; - } - - // if not in index and our value is less than the found one - if (gtHelper(val, rcd[index[ubound]][prop], false)) { - return ubound+1; - } - - // either hole or first nonmatch - if (aeqHelper(val, rcd[index[ubound-1]][prop])) { - return ubound-1; - } - - // hole, so ubound if nearest gt than the val we were looking for - return ubound; - }; - - /** - * calculateRange() - Binary Search utility method to find range/segment of values matching criteria. - * this is used for collection.find() and first find filter of resultset/dynview - * slightly different than get() binary search in that get() hones in on 1 value, - * but we have to hone in on many (range) - * @param {string} op - operation, such as $eq - * @param {string} prop - name of property to calculate range for - * @param {object} val - value to use for range calculation. - * @returns {array} [start, end] index array positions - */ - Collection.prototype.calculateRange = function (op, prop, val) { - var rcd = this.data; - var index = this.binaryIndices[prop].values; - var min = 0; - var max = index.length - 1; - var mid = 0; - var lbound, lval; - var ubound, uval; - - // when no documents are in collection, return empty range condition - if (rcd.length === 0) { - return [0, -1]; - } - - var minVal = rcd[index[min]][prop]; - var maxVal = rcd[index[max]][prop]; - - // if value falls outside of our range return [0, -1] to designate no results - switch (op) { - case '$eq': - case '$aeq': - if (ltHelper(val, minVal, false) || gtHelper(val, maxVal, false)) { - return [0, -1]; - } - break; - case '$dteq': - if (ltHelper(val, minVal, false) || gtHelper(val, maxVal, false)) { - return [0, -1]; - } - break; - case '$gt': - // none are within range - if (gtHelper(val, maxVal, true)) { - return [0, -1]; - } - // all are within range - if (gtHelper(minVal, val, false)) { - return [min, max]; - } - break; - case '$gte': - // none are within range - if (gtHelper(val, maxVal, false)) { - return [0, -1]; - } - // all are within range - if (gtHelper(minVal, val, true)) { - return [min, max]; - } - break; - case '$lt': - // none are within range - if (ltHelper(val, minVal, true)) { - return [0, -1]; - } - // all are within range - if (ltHelper(maxVal, val, false)) { - return [min, max]; - } - break; - case '$lte': - // none are within range - if (ltHelper(val, minVal, false)) { - return [0, -1]; - } - // all are within range - if (ltHelper(maxVal, val, true)) { - return [min, max]; - } - break; - case '$between': - // none are within range (low range is greater) - if (gtHelper(val[0], maxVal, false)) { - return [0, -1]; - } - // none are within range (high range lower) - if (ltHelper(val[1], minVal, false)) { - return [0, -1]; - } - - lbound = this.calculateRangeStart(prop, val[0]); - ubound = this.calculateRangeEnd(prop, val[1]); - - if (lbound < 0) lbound++; - if (ubound > max) ubound--; - - if (!gtHelper(rcd[index[lbound]][prop], val[0], true)) lbound++; - if (!ltHelper(rcd[index[ubound]][prop], val[1], true)) ubound--; - - if (ubound < lbound) return [0, -1]; - - return ([lbound, ubound]); - case '$in': - var idxset = [], - segResult = []; - // query each value '$eq' operator and merge the seqment results. - for (var j = 0, len = val.length; j < len; j++) { - var seg = this.calculateRange('$eq', prop, val[j]); - - for (var i = seg[0]; i <= seg[1]; i++) { - if (idxset[i] === undefined) { - idxset[i] = true; - segResult.push(i); - } - } - } - return segResult; - } - - // determine lbound where needed - switch (op) { - case '$eq': - case '$aeq': - case '$dteq': - case '$gte': - case '$lt': - lbound = this.calculateRangeStart(prop, val); - lval = rcd[index[lbound]][prop]; - break; - default: break; - } - - // determine ubound where needed - switch (op) { - case '$eq': - case '$aeq': - case '$dteq': - case '$lte': - case '$gt': - ubound = this.calculateRangeEnd(prop, val); - uval = rcd[index[ubound]][prop]; - break; - default: break; - } - - - switch (op) { - case '$eq': - case '$aeq': - case '$dteq': - // if hole (not found) - //if (ltHelper(lval, val, false) || gtHelper(lval, val, false)) { - // return [0, -1]; - //} - if (!aeqHelper(lval, val)) { - return [0, -1]; - } - - return [lbound, ubound]; - - //case '$dteq': - // if hole (not found) - // if (lval > val || lval < val) { - // return [0, -1]; - // } - - // return [lbound, ubound]; - - case '$gt': - // (an eqHelper would probably be better test) - // if hole (not found) ub position is already greater - if (!aeqHelper(rcd[index[ubound]][prop], val)) { - //if (gtHelper(rcd[index[ubound]][prop], val, false)) { - return [ubound, max]; - } - // otherwise (found) so ubound is still equal, get next - return [ubound+1, max]; - - case '$gte': - // if hole (not found) lb position marks left outside of range - if (!aeqHelper(rcd[index[lbound]][prop], val)) { - //if (ltHelper(rcd[index[lbound]][prop], val, false)) { - return [lbound+1, max]; - } - // otherwise (found) so lb is first position where its equal - return [lbound, max]; - - case '$lt': - // if hole (not found) position already is less than - if (!aeqHelper(rcd[index[lbound]][prop], val)) { - //if (ltHelper(rcd[index[lbound]][prop], val, false)) { - return [min, lbound]; - } - // otherwise (found) so lb marks left inside of eq range, get previous - return [min, lbound-1]; - - case '$lte': - // if hole (not found) ub position marks right outside so get previous - if (!aeqHelper(rcd[index[ubound]][prop], val)) { - //if (gtHelper(rcd[index[ubound]][prop], val, false)) { - return [min, ubound-1]; - } - // otherwise (found) so ub is last position where its still equal - return [min, ubound]; - - default: - return [0, rcd.length - 1]; - } - }; - - /** - * Retrieve doc by Unique index - * @param {string} field - name of uniquely indexed property to use when doing lookup - * @param {value} value - unique value to search for - * @returns {object} document matching the value passed - * @memberof Collection - */ - Collection.prototype.by = function (field, value) { - var self; - if (value === undefined) { - self = this; - return function (value) { - return self.by(field, value); - }; - } - - var result = this.constraints.unique[field].get(value); - if (!this.cloneObjects) { - return result; - } else { - return clone(result, this.cloneMethod); - } - }; - - /** - * Find one object by index property, by property equal to value - * @param {object} query - query object used to perform search with - * @returns {(object|null)} First matching document, or null if none - * @memberof Collection - */ - Collection.prototype.findOne = function (query) { - query = query || {}; - - // Instantiate Resultset and exec find op passing firstOnly = true param - var result = this.chain().find(query,true).data(); - - if (Array.isArray(result) && result.length === 0) { - return null; - } else { - if (!this.cloneObjects) { - return result[0]; - } else { - return clone(result[0], this.cloneMethod); - } - } - }; - - /** - * Chain method, used for beginning a series of chained find() and/or view() operations - * on a collection. - * - * @param {array=} transform - Ordered array of transform step objects similar to chain - * @param {object=} parameters - Object containing properties representing parameters to substitute - * @returns {Resultset} (this) resultset, or data array if any map or join functions where called - * @memberof Collection - */ - Collection.prototype.chain = function (transform, parameters) { - var rs = new Resultset(this); - - if (typeof transform === 'undefined') { - return rs; - } - - return rs.transform(transform, parameters); - }; - - /** - * Find method, api is similar to mongodb. - * for more complex queries use [chain()]{@link Collection#chain} or [where()]{@link Collection#where}. - * @example {@tutorial Query Examples} - * @param {object} query - 'mongo-like' query object - * @returns {array} Array of matching documents - * @memberof Collection - */ - Collection.prototype.find = function (query) { - return this.chain().find(query).data(); - }; - - /** - * Find object by unindexed field by property equal to value, - * simply iterates and returns the first element matching the query - */ - Collection.prototype.findOneUnindexed = function (prop, value) { - var i = this.data.length, - doc; - while (i--) { - if (this.data[i][prop] === value) { - doc = this.data[i]; - return doc; - } - } - return null; - }; - - /** - * Transaction methods - */ - - /** start the transation */ - Collection.prototype.startTransaction = function () { - if (this.transactional) { - this.cachedData = clone(this.data, this.cloneMethod); - this.cachedIndex = this.idIndex; - this.cachedBinaryIndex = this.binaryIndices; - - // propagate startTransaction to dynamic views - for (var idx = 0; idx < this.DynamicViews.length; idx++) { - this.DynamicViews[idx].startTransaction(); - } - } - }; - - /** commit the transation */ - Collection.prototype.commit = function () { - if (this.transactional) { - this.cachedData = null; - this.cachedIndex = null; - this.cachedBinaryIndex = null; - - // propagate commit to dynamic views - for (var idx = 0; idx < this.DynamicViews.length; idx++) { - this.DynamicViews[idx].commit(); - } - } - }; - - /** roll back the transation */ - Collection.prototype.rollback = function () { - if (this.transactional) { - if (this.cachedData !== null && this.cachedIndex !== null) { - this.data = this.cachedData; - this.idIndex = this.cachedIndex; - this.binaryIndices = this.cachedBinaryIndex; - } - - // propagate rollback to dynamic views - for (var idx = 0; idx < this.DynamicViews.length; idx++) { - this.DynamicViews[idx].rollback(); - } - } - }; - - // async executor. This is only to enable callbacks at the end of the execution. - Collection.prototype.async = function (fun, callback) { - setTimeout(function () { - if (typeof fun === 'function') { - fun(); - callback(); - } else { - throw new TypeError('Argument passed for async execution is not a function'); - } - }, 0); - }; - - /** - * Query the collection by supplying a javascript filter function. - * @example - * var results = coll.where(function(obj) { - * return obj.legs === 8; - * }); - * - * @param {function} fun - filter function to run against all collection docs - * @returns {array} all documents which pass your filter function - * @memberof Collection - */ - Collection.prototype.where = function (fun) { - return this.chain().where(fun).data(); - }; - - /** - * Map Reduce operation - * - * @param {function} mapFunction - function to use as map function - * @param {function} reduceFunction - function to use as reduce function - * @returns {data} The result of your mapReduce operation - * @memberof Collection - */ - Collection.prototype.mapReduce = function (mapFunction, reduceFunction) { - try { - return reduceFunction(this.data.map(mapFunction)); - } catch (err) { - throw err; - } - }; - - /** - * Join two collections on specified properties - * - * @param {array|Resultset|Collection} joinData - array of documents to 'join' to this collection - * @param {string} leftJoinProp - property name in collection - * @param {string} rightJoinProp - property name in joinData - * @param {function=} mapFun - (Optional) map function to use - * @param {object=} dataOptions - options to data() before input to your map function - * @param {bool} dataOptions.removeMeta - allows removing meta before calling mapFun - * @param {boolean} dataOptions.forceClones - forcing the return of cloned objects to your map object - * @param {string} dataOptions.forceCloneMethod - Allows overriding the default or collection specified cloning method. - * @returns {Resultset} Result of the mapping operation - * @memberof Collection - */ - Collection.prototype.eqJoin = function (joinData, leftJoinProp, rightJoinProp, mapFun, dataOptions) { - // logic in Resultset class - return new Resultset(this).eqJoin(joinData, leftJoinProp, rightJoinProp, mapFun, dataOptions); - }; - - /* ------ STAGING API -------- */ - /** - * stages: a map of uniquely identified 'stages', which hold copies of objects to be - * manipulated without affecting the data in the original collection - */ - Collection.prototype.stages = {}; - - /** - * (Staging API) create a stage and/or retrieve it - * @memberof Collection - */ - Collection.prototype.getStage = function (name) { - if (!this.stages[name]) { - this.stages[name] = {}; - } - return this.stages[name]; - }; - /** - * a collection of objects recording the changes applied through a commmitStage - */ - Collection.prototype.commitLog = []; - - /** - * (Staging API) create a copy of an object and insert it into a stage - * @memberof Collection - */ - Collection.prototype.stage = function (stageName, obj) { - var copy = JSON.parse(JSON.stringify(obj)); - this.getStage(stageName)[obj.$loki] = copy; - return copy; - }; - - /** - * (Staging API) re-attach all objects to the original collection, so indexes and views can be rebuilt - * then create a message to be inserted in the commitlog - * @param {string} stageName - name of stage - * @param {string} message - * @memberof Collection - */ - Collection.prototype.commitStage = function (stageName, message) { - var stage = this.getStage(stageName), - prop, - timestamp = new Date().getTime(); - - for (prop in stage) { - - this.update(stage[prop]); - this.commitLog.push({ - timestamp: timestamp, - message: message, - data: JSON.parse(JSON.stringify(stage[prop])) - }); - } - this.stages[stageName] = {}; - }; - - Collection.prototype.no_op = function () { - return; - }; - - /** - * @memberof Collection - */ - Collection.prototype.extract = function (field) { - var i = 0, - len = this.data.length, - isDotNotation = isDeepProperty(field), - result = []; - for (i; i < len; i += 1) { - result.push(deepProperty(this.data[i], field, isDotNotation)); - } - return result; - }; - - /** - * @memberof Collection - */ - Collection.prototype.max = function (field) { - return Math.max.apply(null, this.extract(field)); - }; - - /** - * @memberof Collection - */ - Collection.prototype.min = function (field) { - return Math.min.apply(null, this.extract(field)); - }; - - /** - * @memberof Collection - */ - Collection.prototype.maxRecord = function (field) { - var i = 0, - len = this.data.length, - deep = isDeepProperty(field), - result = { - index: 0, - value: undefined - }, - max; - - for (i; i < len; i += 1) { - if (max !== undefined) { - if (max < deepProperty(this.data[i], field, deep)) { - max = deepProperty(this.data[i], field, deep); - result.index = this.data[i].$loki; - } - } else { - max = deepProperty(this.data[i], field, deep); - result.index = this.data[i].$loki; - } - } - result.value = max; - return result; - }; - - /** - * @memberof Collection - */ - Collection.prototype.minRecord = function (field) { - var i = 0, - len = this.data.length, - deep = isDeepProperty(field), - result = { - index: 0, - value: undefined - }, - min; - - for (i; i < len; i += 1) { - if (min !== undefined) { - if (min > deepProperty(this.data[i], field, deep)) { - min = deepProperty(this.data[i], field, deep); - result.index = this.data[i].$loki; - } - } else { - min = deepProperty(this.data[i], field, deep); - result.index = this.data[i].$loki; - } - } - result.value = min; - return result; - }; - - /** - * @memberof Collection - */ - Collection.prototype.extractNumerical = function (field) { - return this.extract(field).map(parseBase10).filter(Number).filter(function (n) { - return !(isNaN(n)); - }); - }; - - /** - * Calculates the average numerical value of a property - * - * @param {string} field - name of property in docs to average - * @returns {number} average of property in all docs in the collection - * @memberof Collection - */ - Collection.prototype.avg = function (field) { - return average(this.extractNumerical(field)); - }; - - /** - * Calculate standard deviation of a field - * @memberof Collection - * @param {string} field - */ - Collection.prototype.stdDev = function (field) { - return standardDeviation(this.extractNumerical(field)); - }; - - /** - * @memberof Collection - * @param {string} field - */ - Collection.prototype.mode = function (field) { - var dict = {}, - data = this.extract(field); - data.forEach(function (obj) { - if (dict[obj]) { - dict[obj] += 1; - } else { - dict[obj] = 1; - } - }); - var max, - prop, mode; - for (prop in dict) { - if (max) { - if (max < dict[prop]) { - mode = prop; - } - } else { - mode = prop; - max = dict[prop]; - } - } - return mode; - }; - - /** - * @memberof Collection - * @param {string} field - property name - */ - Collection.prototype.median = function (field) { - var values = this.extractNumerical(field); - values.sort(sub); - - var half = Math.floor(values.length / 2); - - if (values.length % 2) { - return values[half]; - } else { - return (values[half - 1] + values[half]) / 2.0; - } - }; - - /** - * General utils, including statistical functions - */ - function isDeepProperty(field) { - return field.indexOf('.') !== -1; - } - - function parseBase10(num) { - return parseFloat(num, 10); - } - - function isNotUndefined(obj) { - return obj !== undefined; - } - - function add(a, b) { - return a + b; - } - - function sub(a, b) { - return a - b; - } - - function median(values) { - values.sort(sub); - var half = Math.floor(values.length / 2); - return (values.length % 2) ? values[half] : ((values[half - 1] + values[half]) / 2.0); - } - - function average(array) { - return (array.reduce(add, 0)) / array.length; - } - - function standardDeviation(values) { - var avg = average(values); - var squareDiffs = values.map(function (value) { - var diff = value - avg; - var sqrDiff = diff * diff; - return sqrDiff; - }); - - var avgSquareDiff = average(squareDiffs); - - var stdDev = Math.sqrt(avgSquareDiff); - return stdDev; - } - - function deepProperty(obj, property, isDeep) { - if (isDeep === false) { - // pass without processing - return obj[property]; - } - var pieces = property.split('.'), - root = obj; - while (pieces.length > 0) { - root = root[pieces.shift()]; - } - return root; - } - - function binarySearch(array, item, fun) { - var lo = 0, - hi = array.length, - compared, - mid; - while (lo < hi) { - mid = (lo + hi) >> 1; - compared = fun.apply(null, [item, array[mid]]); - if (compared === 0) { - return { - found: true, - index: mid - }; - } else if (compared < 0) { - hi = mid; - } else { - lo = mid + 1; - } - } - return { - found: false, - index: hi - }; - } - - function BSonSort(fun) { - return function (array, item) { - return binarySearch(array, item, fun); - }; - } - - function KeyValueStore() {} - - KeyValueStore.prototype = { - keys: [], - values: [], - sort: function (a, b) { - return (a < b) ? -1 : ((a > b) ? 1 : 0); - }, - setSort: function (fun) { - this.bs = new BSonSort(fun); - }, - bs: function () { - return new BSonSort(this.sort); - }, - set: function (key, value) { - var pos = this.bs(this.keys, key); - if (pos.found) { - this.values[pos.index] = value; - } else { - this.keys.splice(pos.index, 0, key); - this.values.splice(pos.index, 0, value); - } - }, - get: function (key) { - return this.values[binarySearch(this.keys, key, this.sort).index]; - } - }; - - function UniqueIndex(uniqueField) { - this.field = uniqueField; - this.keyMap = {}; - this.lokiMap = {}; - } - UniqueIndex.prototype.keyMap = {}; - UniqueIndex.prototype.lokiMap = {}; - UniqueIndex.prototype.set = function (obj) { - var fieldValue = obj[this.field]; - if (fieldValue !== null && typeof (fieldValue) !== 'undefined') { - if (this.keyMap[fieldValue]) { - throw new Error('Duplicate key for property ' + this.field + ': ' + fieldValue); - } else { - this.keyMap[fieldValue] = obj; - this.lokiMap[obj.$loki] = fieldValue; - } - } - }; - UniqueIndex.prototype.get = function (key) { - return this.keyMap[key]; - }; - - UniqueIndex.prototype.byId = function (id) { - return this.keyMap[this.lokiMap[id]]; - }; - /** - * Updates a document's unique index given an updated object. - * @param {Object} obj Original document object - * @param {Object} doc New document object (likely the same as obj) - */ - UniqueIndex.prototype.update = function (obj, doc) { - if (this.lokiMap[obj.$loki] !== doc[this.field]) { - var old = this.lokiMap[obj.$loki]; - this.set(doc); - // make the old key fail bool test, while avoiding the use of delete (mem-leak prone) - this.keyMap[old] = undefined; - } else { - this.keyMap[obj[this.field]] = doc; - } - }; - UniqueIndex.prototype.remove = function (key) { - var obj = this.keyMap[key]; - if (obj !== null && typeof obj !== 'undefined') { - this.keyMap[key] = undefined; - this.lokiMap[obj.$loki] = undefined; - } else { - throw new Error('Key is not in unique index: ' + this.field); - } - }; - UniqueIndex.prototype.clear = function () { - this.keyMap = {}; - this.lokiMap = {}; - }; - - function ExactIndex(exactField) { - this.index = {}; - this.field = exactField; - } - - // add the value you want returned to the key in the index - ExactIndex.prototype = { - set: function add(key, val) { - if (this.index[key]) { - this.index[key].push(val); - } else { - this.index[key] = [val]; - } - }, - - // remove the value from the index, if the value was the last one, remove the key - remove: function remove(key, val) { - var idxSet = this.index[key]; - for (var i in idxSet) { - if (idxSet[i] == val) { - idxSet.splice(i, 1); - } - } - if (idxSet.length < 1) { - this.index[key] = undefined; - } - }, - - // get the values related to the key, could be more than one - get: function get(key) { - return this.index[key]; - }, - - // clear will zap the index - clear: function clear(key) { - this.index = {}; - } - }; - - function SortedIndex(sortedField) { - this.field = sortedField; - } - - SortedIndex.prototype = { - keys: [], - values: [], - // set the default sort - sort: function (a, b) { - return (a < b) ? -1 : ((a > b) ? 1 : 0); - }, - bs: function () { - return new BSonSort(this.sort); - }, - // and allow override of the default sort - setSort: function (fun) { - this.bs = new BSonSort(fun); - }, - // add the value you want returned to the key in the index - set: function (key, value) { - var pos = binarySearch(this.keys, key, this.sort); - if (pos.found) { - this.values[pos.index].push(value); - } else { - this.keys.splice(pos.index, 0, key); - this.values.splice(pos.index, 0, [value]); - } - }, - // get all values which have a key == the given key - get: function (key) { - var bsr = binarySearch(this.keys, key, this.sort); - if (bsr.found) { - return this.values[bsr.index]; - } else { - return []; - } - }, - // get all values which have a key < the given key - getLt: function (key) { - var bsr = binarySearch(this.keys, key, this.sort); - var pos = bsr.index; - if (bsr.found) pos--; - return this.getAll(key, 0, pos); - }, - // get all values which have a key > the given key - getGt: function (key) { - var bsr = binarySearch(this.keys, key, this.sort); - var pos = bsr.index; - if (bsr.found) pos++; - return this.getAll(key, pos, this.keys.length); - }, - - // get all vals from start to end - getAll: function (key, start, end) { - var results = []; - for (var i = start; i < end; i++) { - results = results.concat(this.values[i]); - } - return results; - }, - // just in case someone wants to do something smart with ranges - getPos: function (key) { - return binarySearch(this.keys, key, this.sort); - }, - // remove the value from the index, if the value was the last one, remove the key - remove: function (key, value) { - var pos = binarySearch(this.keys, key, this.sort).index; - var idxSet = this.values[pos]; - for (var i in idxSet) { - if (idxSet[i] == value) idxSet.splice(i, 1); - } - if (idxSet.length < 1) { - this.keys.splice(pos, 1); - this.values.splice(pos, 1); - } - }, - // clear will zap the index - clear: function () { - this.keys = []; - this.values = []; - } - }; - - - Loki.LokiOps = LokiOps; - Loki.Collection = Collection; - Loki.KeyValueStore = KeyValueStore; - Loki.LokiMemoryAdapter = LokiMemoryAdapter; - Loki.LokiPartitioningAdapter = LokiPartitioningAdapter; - Loki.LokiLocalStorageAdapter = LokiLocalStorageAdapter; - Loki.LokiFsAdapter = LokiFsAdapter; - Loki.persistenceAdapters = { - fs: LokiFsAdapter, - localStorage: LokiLocalStorageAdapter - }; - Loki.aeq = aeqHelper; - Loki.lt = ltHelper; - Loki.gt = gtHelper; - return Loki; - }()); - -})); diff --git a/build/lokijs.min.js b/build/lokijs.min.js index 36d4058c..f0682c77 100644 --- a/build/lokijs.min.js +++ b/build/lokijs.min.js @@ -1,3 +1,3 @@ (function(root,factory){if(typeof define==="function"&&define.amd){define([],factory)}else if(typeof exports==="object"){module.exports=factory()}else{root.loki=factory()}})(this,function(){return function(){"use strict";var hasOwnProperty=Object.prototype.hasOwnProperty;var Utils={copyProperties:function(src,dest){var prop;for(prop in src){dest[prop]=src[prop]}},resolveTransformObject:function(subObj,params,depth){var prop,pname;if(typeof depth!=="number"){depth=0}if(++depth>=10)return subObj;for(prop in subObj){if(typeof subObj[prop]==="string"&&subObj[prop].indexOf("[%lktxp]")===0){pname=subObj[prop].substring(8);if(params.hasOwnProperty(pname)){subObj[prop]=params[pname]}}else if(typeof subObj[prop]==="object"){subObj[prop]=Utils.resolveTransformObject(subObj[prop],params,depth)}}return subObj},resolveTransformParams:function(transform,params){var idx,clonedStep,resolvedTransform=[];if(typeof params==="undefined")return transform;for(idx=0;idxcv2)return false;return equal}if(cv1===cv1&&cv2!==cv2){return true}if(cv2===cv2&&cv1!==cv1){return false}if(prop1prop2)return false;if(prop1==prop2)return equal;cv1=prop1.toString();cv2=prop2.toString();if(cv1t2}}cv1=Number(prop1);cv2=Number(prop2);if(cv1===cv1&&cv2===cv2){if(cv1>cv2)return true;if(cv1prop2)return true;if(prop1cv2){return true}if(cv1==cv2){return equal}return false}function sortHelper(prop1,prop2,desc){if(aeqHelper(prop1,prop2))return 0;if(ltHelper(prop1,prop2,false)){return desc?1:-1}if(gtHelper(prop1,prop2,false)){return desc?-1:1}return 0}function compoundeval(properties,obj1,obj2){var res=0;var prop,field,val1,val2,arr;for(var i=0,len=properties.length;i=paths.length){valueFound=fun(element,value)}else if(Array.isArray(element)){for(var index=0,len=element.length;index0){throw new Error("disableMeta option cannot be passed as true when ttl is enabled")}}for(i=0;i=0){return this.serializeCollection({delimited:options.delimited,delimiter:options.delimiter,collectionIndex:options.partition})}dbcopy=new Loki(this.filename);dbcopy.loadJSONObject(this);for(idx=0;idxcollCount){done=true}}else{currObject=JSON.parse(workarray[lineIndex]);cdb.collections[collIndex].data.push(currObject)}workarray[lineIndex++]=null}return cdb};Loki.prototype.deserializeCollection=function(destructuredSource,options){var workarray=[];var idx,len;options=options||{};if(!options.hasOwnProperty("partitioned")){options.partitioned=false}if(!options.hasOwnProperty("delimited")){options.delimited=true}if(!options.hasOwnProperty("delimiter")){options.delimiter=this.options.destructureDelimiter}if(options.delimited){workarray=destructuredSource.split(options.delimiter);workarray.pop()}else{workarray=destructuredSource}len=workarray.length;for(idx=0;idx=cdlen)doneWithPartition=true}if(pageLen>=this.options.pageSize)doneWithPage=true;if(!doneWithPage||doneWithPartition){pageBuilder+=this.options.delimiter;pageLen+=delimlen}if(doneWithPartition||doneWithPage){this.adapter.saveDatabase(keyname,pageBuilder,pageSaveCallback);return}}};function LokiFsAdapter(){this.fs=require("fs")}LokiFsAdapter.prototype.loadDatabase=function loadDatabase(dbname,callback){var self=this;this.fs.stat(dbname,function(err,stats){if(!err&&stats.isFile()){self.fs.readFile(dbname,{encoding:"utf8"},function readFileCallback(err,data){if(err){callback(new Error(err))}else{callback(data)}})}else{callback(null)}})};LokiFsAdapter.prototype.saveDatabase=function saveDatabase(dbname,dbstring,callback){var self=this;var tmpdbname=dbname+"~";this.fs.writeFile(tmpdbname,dbstring,function writeFileCallback(err){if(err){callback(new Error(err))}else{self.fs.rename(tmpdbname,dbname,callback)}})};LokiFsAdapter.prototype.deleteDatabase=function deleteDatabase(dbname,callback){this.fs.unlink(dbname,function deleteDatabaseCallback(err){if(err){callback(new Error(err))}else{callback()}})};function LokiLocalStorageAdapter(){}LokiLocalStorageAdapter.prototype.loadDatabase=function loadDatabase(dbname,callback){if(localStorageAvailable()){callback(localStorage.getItem(dbname))}else{callback(new Error("localStorage is not available"))}};LokiLocalStorageAdapter.prototype.saveDatabase=function saveDatabase(dbname,dbstring,callback){if(localStorageAvailable()){localStorage.setItem(dbname,dbstring);callback(null)}else{callback(new Error("localStorage is not available"))}};LokiLocalStorageAdapter.prototype.deleteDatabase=function deleteDatabase(dbname,callback){if(localStorageAvailable()){localStorage.removeItem(dbname);callback(null)}else{callback(new Error("localStorage is not available"))}};Loki.prototype.throttledSaveDrain=function(callback,options){var self=this;var now=(new Date).getTime();if(!this.throttledSaves){callback(true)}options=options||{};if(!options.hasOwnProperty("recursiveWait")){options.recursiveWait=true} -if(!options.hasOwnProperty("recursiveWaitLimit")){options.recursiveWaitLimit=false}if(!options.hasOwnProperty("recursiveWaitLimitDuration")){options.recursiveWaitLimitDuration=2e3}if(!options.hasOwnProperty("started")){options.started=(new Date).getTime()}if(this.throttledSaves&&this.throttledSavePending){if(options.recursiveWait){this.throttledCallbacks.push(function(){if(self.throttledSavePending){if(options.recursiveWaitLimit&&now-options.started>options.recursiveWaitLimitDuration){callback(false);return}self.throttledSaveDrain(callback,options);return}else{callback(true);return}})}else{this.throttledCallbacks.push(callback);return}}else{callback(true)}};Loki.prototype.loadDatabaseInternal=function(options,callback){var cFun=callback||function(err,data){if(err){throw err}},self=this;if(this.persistenceAdapter!==null){this.persistenceAdapter.loadDatabase(this.filename,function loadDatabaseCallback(dbString){if(typeof dbString==="string"){var parseSuccess=false;try{self.loadJSON(dbString,options||{});parseSuccess=true}catch(err){cFun(err)}if(parseSuccess){cFun(null);self.emit("loaded","database "+self.filename+" loaded")}}else{if(!dbString){cFun(null);self.emit("loaded","empty database "+self.filename+" loaded");return}if(dbString instanceof Error){cFun(dbString);return}if(typeof dbString==="object"){self.loadJSONObject(dbString,options||{});cFun(null);self.emit("loaded","database "+self.filename+" loaded");return}cFun("unexpected adapter response : "+dbString)}})}else{cFun(new Error("persistenceAdapter not configured"))}};Loki.prototype.loadDatabase=function(options,callback){var self=this;if(!this.throttledSaves){this.loadDatabaseInternal(options,callback);return}this.throttledSaveDrain(function(success){if(success){self.throttledSavePending=true;self.loadDatabaseInternal(options,function(err){if(self.throttledCallbacks.length===0){self.throttledSavePending=false}else{self.saveDatabase()}if(typeof callback==="function"){callback(err)}});return}else{if(typeof callback==="function"){callback(new Error("Unable to pause save throttling long enough to read database"))}}},options)};Loki.prototype.saveDatabaseInternal=function(callback){var cFun=callback||function(err){if(err){throw err}return},self=this;if(this.persistenceAdapter!==null){if(this.persistenceAdapter.mode==="reference"&&typeof this.persistenceAdapter.exportDatabase==="function"){this.persistenceAdapter.exportDatabase(this.filename,this.copy({removeNonSerializable:true}),function exportDatabaseCallback(err){self.autosaveClearFlags();cFun(err)})}else{self.autosaveClearFlags();this.persistenceAdapter.saveDatabase(this.filename,self.serialize(),function saveDatabasecallback(err){cFun(err)})}}else{cFun(new Error("persistenceAdapter not configured"))}};Loki.prototype.saveDatabase=function(callback){if(!this.throttledSaves){this.saveDatabaseInternal(callback);return}if(this.throttledSavePending){this.throttledCallbacks.push(callback);return}var localCallbacks=this.throttledCallbacks;this.throttledCallbacks=[];localCallbacks.unshift(callback);this.throttledSavePending=true;var self=this;this.saveDatabaseInternal(function(err){self.throttledSavePending=false;localCallbacks.forEach(function(pcb){if(typeof pcb==="function"){setTimeout(function(){pcb(err)},1)}});if(self.throttledCallbacks.length>0){self.saveDatabase()}})};Loki.prototype.save=Loki.prototype.saveDatabase;Loki.prototype.deleteDatabase=function(options,callback){var cFun=callback||function(err,data){if(err){throw err}};if(typeof options==="function"&&!callback){cFun=options}if(this.persistenceAdapter!==null){this.persistenceAdapter.deleteDatabase(this.filename,function deleteDatabaseCallback(err){cFun(err)})}else{cFun(new Error("persistenceAdapter not configured"))}};Loki.prototype.autosaveDirty=function(){for(var idx=0;idx0){this.filteredrows=[]}this.filterInitialized=false;return this};Resultset.prototype.toJSON=function(){var copy=this.copy();copy.collection=null;return copy};Resultset.prototype.limit=function(qty){if(!this.filterInitialized&&this.filteredrows.length===0){this.filteredrows=this.collection.prepareFullDocIndex()}var rscopy=new Resultset(this.collection);rscopy.filteredrows=this.filteredrows.slice(0,qty);rscopy.filterInitialized=true;return rscopy};Resultset.prototype.offset=function(pos){if(!this.filterInitialized&&this.filteredrows.length===0){this.filteredrows=this.collection.prepareFullDocIndex()}var rscopy=new Resultset(this.collection);rscopy.filteredrows=this.filteredrows.slice(pos);rscopy.filterInitialized=true;return rscopy};Resultset.prototype.copy=function(){var result=new Resultset(this.collection);if(this.filteredrows.length>0){result.filteredrows=this.filteredrows.slice()}result.filterInitialized=this.filterInitialized;return result};Resultset.prototype.branch=Resultset.prototype.copy;Resultset.prototype.transform=function(transform,parameters){var idx,step,rs=this;if(typeof transform==="string"){if(this.collection.transforms.hasOwnProperty(transform)){transform=this.collection.transforms[transform]}}if(typeof transform!=="object"||!Array.isArray(transform)){throw new Error("Invalid transform")}if(typeof parameters!=="undefined"){transform=Utils.resolveTransformParams(transform,parameters)}for(idx=0;idx1){return this.find({$and:filters},firstOnly)}}if(!property||queryObject==="getAll"){if(firstOnly){this.filteredrows=this.collection.data.length>0?[0]:[];this.filterInitialized=true}return this}if(property==="$and"||property==="$or"){this[property](queryObjectOp);if(firstOnly&&this.filteredrows.length>1){this.filteredrows=this.filteredrows.slice(0,1)}return this}if(queryObjectOp===null||(typeof queryObjectOp!=="object"||queryObjectOp instanceof Date)){operator="$eq";value=queryObjectOp}else if(typeof queryObjectOp==="object"){for(key in queryObjectOp){if(hasOwnProperty.call(queryObjectOp,key)){operator=key;value=queryObjectOp[key];break}}}else{throw new Error("Do not know what you want to do.")}if(operator==="$regex"){if(Array.isArray(value)){value=new RegExp(value[0],value[1])}else if(!(value instanceof RegExp)){value=new RegExp(value)}}var usingDotNotation=property.indexOf(".")!==-1;var doIndexCheck=!usingDotNotation&&!this.filterInitialized;if(doIndexCheck&&this.collection.binaryIndices[property]&&indexedOps[operator]){if(this.collection.adaptiveBinaryIndices!==true){this.collection.ensureIndex(property)}searchByIndex=true;index=this.collection.binaryIndices[property]}var fun=LokiOps[operator];var t=this.collection.data;var i=0,len=0;var filter,rowIdx=0;if(this.filterInitialized){filter=this.filteredrows;len=filter.length;if(usingDotNotation){property=property.split(".");for(i=0;i=0){this.filterPipeline[idx]=filter;return this.reapplyFilters()}this.cachedresultset=null;if(this.options.persistent){this.resultdata=[];this.resultsdirty=true}this._addFilter(filter);if(this.sortFunction||this.sortCriteria){this.queueSortPhase()}else{this.queueRebuildEvent()}return this};DynamicView.prototype.applyFind=function(query,uid){this.applyFilter({type:"find",val:query,uid:uid});return this};DynamicView.prototype.applyWhere=function(fun,uid){this.applyFilter({type:"where",val:fun,uid:uid});return this};DynamicView.prototype.removeFilter=function(uid){var idx=this._indexOfFilterWithId(uid);if(idx<0){throw new Error("Dynamic view does not contain a filter with ID: "+uid)}this.filterPipeline.splice(idx,1);this.reapplyFilters();return this};DynamicView.prototype.count=function(){if(this.resultsdirty){this.resultdata=this.resultset.data()}return this.resultset.count()};DynamicView.prototype.data=function(options){if(this.sortDirty||this.resultsdirty){this.performSortPhase({suppressRebuildEvent:true})}return this.options.persistent?this.resultdata:this.resultset.data(options)};DynamicView.prototype.queueRebuildEvent=function(){if(this.rebuildPending){return}this.rebuildPending=true;var self=this;setTimeout(function(){if(self.rebuildPending){self.rebuildPending=false;self.emit("rebuild",self)}},this.options.minRebuildInterval)};DynamicView.prototype.queueSortPhase=function(){if(this.sortDirty){return}this.sortDirty=true;var self=this;if(this.options.sortPriority==="active"){setTimeout(function(){self.performSortPhase()},this.options.minRebuildInterval)}else{this.queueRebuildEvent()}};DynamicView.prototype.performSortPhase=function(options){if(!this.sortDirty&&!this.resultsdirty){return}options=options||{};if(this.sortDirty){if(this.sortFunction){this.resultset.sort(this.sortFunction)}else if(this.sortCriteria){this.resultset.compoundsort(this.sortCriteria)}this.sortDirty=false}if(this.options.persistent){this.resultdata=this.resultset.data();this.resultsdirty=false}if(!options.suppressRebuildEvent){this.emit("rebuild",this)}};DynamicView.prototype.evaluateDocument=function(objIndex,isNew){if(!this.resultset.filterInitialized){if(this.options.persistent){this.resultdata=this.resultset.data()}if(this.sortFunction||this.sortCriteria){this.queueSortPhase()}else{this.queueRebuildEvent()}return}var ofr=this.resultset.filteredrows;var oldPos=isNew?-1:ofr.indexOf(+objIndex);var oldlen=ofr.length;var evalResultset=new Resultset(this.collection);evalResultset.filteredrows=[objIndex];evalResultset.filterInitialized=true;var filter;for(var idx=0,len=this.filterPipeline.length;idxobjIndex){ofr[idx]--}}};DynamicView.prototype.mapReduce=function(mapFunction,reduceFunction){try{return reduceFunction(this.data().map(mapFunction))}catch(err){throw err}};function Collection(name,options){this.name=name;this.data=[];this.idIndex=[];this.binaryIndices={};this.constraints={unique:{},exact:{}};this.uniqueNames=[];this.transforms={};this.objType=name;this.dirty=true;this.cachedIndex=null;this.cachedBinaryIndex=null;this.cachedData=null;var self=this;options=options||{};if(options.hasOwnProperty("unique")){if(!Array.isArray(options.unique)){options.unique=[options.unique]}options.unique.forEach(function(prop){self.uniqueNames.push(prop);self.constraints.unique[prop]=new UniqueIndex(prop)})}if(options.hasOwnProperty("exact")){options.exact.forEach(function(prop){self.constraints.exact[prop]=new ExactIndex(prop)})}this.adaptiveBinaryIndices=options.hasOwnProperty("adaptiveBinaryIndices")?options.adaptiveBinaryIndices:true;this.transactional=options.hasOwnProperty("transactional")?options.transactional:false;this.cloneObjects=options.hasOwnProperty("clone")?options.clone:false;this.cloneMethod=options.hasOwnProperty("cloneMethod")?options.cloneMethod:"parse-stringify";this.asyncListeners=options.hasOwnProperty("asyncListeners")?options.asyncListeners:false;this.disableMeta=options.hasOwnProperty("disableMeta")?options.disableMeta:false;this.disableChangesApi=options.hasOwnProperty("disableChangesApi")?options.disableChangesApi:true;this.disableDeltaChangesApi=options.hasOwnProperty("disableDeltaChangesApi")?options.disableDeltaChangesApi:true;if(this.disableChangesApi){this.disableDeltaChangesApi=true}this.autoupdate=options.hasOwnProperty("autoupdate")?options.autoupdate:false;this.serializableIndices=options.hasOwnProperty("serializableIndices")?options.serializableIndices:true;this.ttl={age:null,ttlInterval:null,daemon:null};this.setTTL(options.ttl||-1,options.ttlInterval);this.maxId=0;this.DynamicViews=[];this.events={insert:[],update:[],"pre-insert":[],"pre-update":[],close:[],flushbuffer:[],error:[],delete:[],warning:[]};this.changes=[];this.ensureId();var indices=[];if(options&&options.indices){if(Object.prototype.toString.call(options.indices)==="[object Array]"){indices=options.indices}else if(typeof options.indices==="string"){indices=[options.indices]}else{throw new TypeError("Indices needs to be a string or an array of strings")}}for(var idx=0;idx=0||propertyName=="$loki"||propertyName=="meta"){delta[propertyName]=newObject[propertyName]}else{var propertyDelta=getObjectDelta(oldObject[propertyName],newObject[propertyName]);if(typeof propertyDelta!=="undefined"&&propertyDelta!={}){delta[propertyName]=propertyDelta}}}}return Object.keys(delta).length===0?undefined:delta}else{return oldObject===newObject?undefined:newObject}}this.getObjectDelta=getObjectDelta;function flushChanges(){self.changes=[]}this.getChanges=function(){return self.changes};this.flushChanges=flushChanges;function insertMeta(obj){var len,idx;if(self.disableMeta||!obj){return}if(Array.isArray(obj)){len=obj.length;for(idx=0;idx0;if(adaptiveBatchOverride){this.adaptiveBinaryIndices=false}for(k;k>1;id=typeof id==="number"?id:parseInt(id,10);if(isNaN(id)){throw new TypeError("Passed id is not an integer")}while(data[min]>1;if(data[mid]dataPosition){index[idx]--}}};Collection.prototype.calculateRangeStart=function(prop,val,adaptive){var rcd=this.data;var index=this.binaryIndices[prop].values;var min=0;var max=index.length-1;var mid=0;if(index.length===0){return-1}var minVal=rcd[index[min]][prop];var maxVal=rcd[index[max]][prop];while(min>1;if(ltHelper(rcd[index[mid]][prop],val,false)){min=mid+1}else{max=mid}}var lbound=min;if(aeqHelper(val,rcd[index[lbound]][prop])){return lbound}if(ltHelper(val,rcd[index[lbound]][prop],false)){return adaptive?lbound:lbound-1}return adaptive?lbound+1:lbound};Collection.prototype.calculateRangeEnd=function(prop,val){var rcd=this.data;var index=this.binaryIndices[prop].values;var min=0;var max=index.length-1;var mid=0;if(index.length===0){return-1}var minVal=rcd[index[min]][prop];var maxVal=rcd[index[max]][prop];while(min>1;if(ltHelper(val,rcd[index[mid]][prop],false)){max=mid}else{min=mid+1}}var ubound=max;if(aeqHelper(val,rcd[index[ubound]][prop])){return ubound}if(gtHelper(val,rcd[index[ubound]][prop],false)){return ubound+1}if(aeqHelper(val,rcd[index[ubound-1]][prop])){return ubound-1}return ubound};Collection.prototype.calculateRange=function(op,prop,val){var rcd=this.data;var index=this.binaryIndices[prop].values;var min=0;var max=index.length-1;var mid=0;var lbound,lval;var ubound,uval;if(rcd.length===0){return[0,-1]}var minVal=rcd[index[min]][prop];var maxVal=rcd[index[max]][prop];switch(op){case"$eq":case"$aeq":if(ltHelper(val,minVal,false)||gtHelper(val,maxVal,false)){return[0,-1]}break;case"$dteq":if(ltHelper(val,minVal,false)||gtHelper(val,maxVal,false)){return[0,-1]}break;case"$gt":if(gtHelper(val,maxVal,true)){return[0,-1]}if(gtHelper(minVal,val,false)){return[min,max]}break;case"$gte":if(gtHelper(val,maxVal,false)){return[0,-1]}if(gtHelper(minVal,val,true)){return[min,max]}break;case"$lt":if(ltHelper(val,minVal,true)){return[0,-1]}if(ltHelper(maxVal,val,false)){return[min,max]}break;case"$lte":if(ltHelper(val,minVal,false)){return[0,-1]}if(ltHelper(maxVal,val,true)){return[min,max]}break;case"$between":if(gtHelper(val[0],maxVal,false)){return[0,-1]}if(ltHelper(val[1],minVal,false)){return[0,-1]}lbound=this.calculateRangeStart(prop,val[0]);ubound=this.calculateRangeEnd(prop,val[1]);if(lbound<0)lbound++;if(ubound>max)ubound--;if(!gtHelper(rcd[index[lbound]][prop],val[0],true))lbound++;if(!ltHelper(rcd[index[ubound]][prop],val[1],true))ubound--;if(ubounddeepProperty(this.data[i],field,deep)){min=deepProperty(this.data[i],field,deep);result.index=this.data[i].$loki}}else{min=deepProperty(this.data[i],field,deep);result.index=this.data[i].$loki}}result.value=min;return result};Collection.prototype.extractNumerical=function(field){return this.extract(field).map(parseBase10).filter(Number).filter(function(n){return!isNaN(n)})};Collection.prototype.avg=function(field){return average(this.extractNumerical(field))};Collection.prototype.stdDev=function(field){return standardDeviation(this.extractNumerical(field))};Collection.prototype.mode=function(field){var dict={},data=this.extract(field);data.forEach(function(obj){if(dict[obj]){dict[obj]+=1}else{dict[obj]=1}});var max,prop,mode;for(prop in dict){if(max){if(max0){root=root[pieces.shift()]}return root}function binarySearch(array,item,fun){var lo=0,hi=array.length,compared,mid;while(lo>1;compared=fun.apply(null,[item,array[mid]]);if(compared===0){return{found:true,index:mid}}else if(compared<0){hi=mid}else{lo=mid+1}}return{found:false,index:hi}}function BSonSort(fun){return function(array,item){return binarySearch(array,item,fun)}}function KeyValueStore(){}KeyValueStore.prototype={keys:[],values:[],sort:function(a,b){return ab?1:0},setSort:function(fun){this.bs=new BSonSort(fun)},bs:function(){return new BSonSort(this.sort)},set:function(key,value){var pos=this.bs(this.keys,key);if(pos.found){this.values[pos.index]=value}else{this.keys.splice(pos.index,0,key);this.values.splice(pos.index,0,value)}},get:function(key){return this.values[binarySearch(this.keys,key,this.sort).index]}};function UniqueIndex(uniqueField){this.field=uniqueField;this.keyMap={};this.lokiMap={}}UniqueIndex.prototype.keyMap={};UniqueIndex.prototype.lokiMap={};UniqueIndex.prototype.set=function(obj){var fieldValue=obj[this.field];if(fieldValue!==null&&typeof fieldValue!=="undefined"){if(this.keyMap[fieldValue]){throw new Error("Duplicate key for property "+this.field+": "+fieldValue)}else{this.keyMap[fieldValue]=obj;this.lokiMap[obj.$loki]=fieldValue}}};UniqueIndex.prototype.get=function(key){return this.keyMap[key]};UniqueIndex.prototype.byId=function(id){return this.keyMap[this.lokiMap[id]]};UniqueIndex.prototype.update=function(obj,doc){if(this.lokiMap[obj.$loki]!==doc[this.field]){var old=this.lokiMap[obj.$loki];this.set(doc);this.keyMap[old]=undefined}else{this.keyMap[obj[this.field]]=doc}};UniqueIndex.prototype.remove=function(key){var obj=this.keyMap[key];if(obj!==null&&typeof obj!=="undefined"){this.keyMap[key]=undefined;this.lokiMap[obj.$loki]=undefined}else{throw new Error("Key is not in unique index: "+this.field)}};UniqueIndex.prototype.clear=function(){this.keyMap={};this.lokiMap={}};function ExactIndex(exactField){this.index={};this.field=exactField}ExactIndex.prototype={set:function add(key,val){if(this.index[key]){this.index[key].push(val)}else{this.index[key]=[val]}},remove:function remove(key,val){var idxSet=this.index[key];for(var i in idxSet){if(idxSet[i]==val){idxSet.splice(i,1)}}if(idxSet.length<1){this.index[key]=undefined}},get:function get(key){return this.index[key]},clear:function clear(key){this.index={}}};function SortedIndex(sortedField){this.field=sortedField}SortedIndex.prototype={keys:[],values:[],sort:function(a,b){return ab?1:0},bs:function(){return new BSonSort(this.sort)},setSort:function(fun){this.bs=new BSonSort(fun)},set:function(key,value){var pos=binarySearch(this.keys,key,this.sort);if(pos.found){this.values[pos.index].push(value)}else{this.keys.splice(pos.index,0,key);this.values.splice(pos.index,0,[value])}},get:function(key){var bsr=binarySearch(this.keys,key,this.sort);if(bsr.found){return this.values[bsr.index]}else{return[]}},getLt:function(key){var bsr=binarySearch(this.keys,key,this.sort);var pos=bsr.index;if(bsr.found)pos--;return this.getAll(key,0,pos)},getGt:function(key){var bsr=binarySearch(this.keys,key,this.sort);var pos=bsr.index;if(bsr.found)pos++;return this.getAll(key,pos,this.keys.length)},getAll:function(key,start,end){var results=[];for(var i=start;ioptions.recursiveWaitLimitDuration){callback(false);return}self.throttledSaveDrain(callback,options);return}else{callback(true);return}})}else{this.throttledCallbacks.push(callback);return}}else{callback(true)}};Loki.prototype.loadDatabaseInternal=function(options,callback){var cFun=callback||function(err,data){if(err){throw err}},self=this;if(this.persistenceAdapter!==null){this.persistenceAdapter.loadDatabase(this.filename,function loadDatabaseCallback(dbString){if(typeof dbString==="string"){var parseSuccess=false;try{self.loadJSON(dbString,options||{});parseSuccess=true}catch(err){cFun(err)}if(parseSuccess){cFun(null);self.emit("loaded","database "+self.filename+" loaded")}}else{if(!dbString){cFun(null);self.emit("loaded","empty database "+self.filename+" loaded");return}if(dbString instanceof Error){cFun(dbString);return}if(typeof dbString==="object"){self.loadJSONObject(dbString,options||{});cFun(null);self.emit("loaded","database "+self.filename+" loaded");return}cFun("unexpected adapter response : "+dbString)}})}else{cFun(new Error("persistenceAdapter not configured"))}};Loki.prototype.loadDatabase=function(options,callback){var self=this;if(!this.throttledSaves){this.loadDatabaseInternal(options,callback);return}this.throttledSaveDrain(function(success){if(success){self.throttledSavePending=true;self.loadDatabaseInternal(options,function(err){if(self.throttledCallbacks.length===0){self.throttledSavePending=false}else{self.saveDatabase()}if(typeof callback==="function"){callback(err)}});return}else{if(typeof callback==="function"){callback(new Error("Unable to pause save throttling long enough to read database"))}}},options)};Loki.prototype.saveDatabaseInternal=function(callback){var cFun=callback||function(err){if(err){throw err}return},self=this;if(this.persistenceAdapter!==null){if(this.persistenceAdapter.mode==="reference"&&typeof this.persistenceAdapter.exportDatabase==="function"){this.persistenceAdapter.exportDatabase(this.filename,this.copy({removeNonSerializable:true}),function exportDatabaseCallback(err){self.autosaveClearFlags();cFun(err)})}else{self.autosaveClearFlags();this.persistenceAdapter.saveDatabase(this.filename,self.serialize(),function saveDatabasecallback(err){cFun(err)})}}else{cFun(new Error("persistenceAdapter not configured"))}};Loki.prototype.saveDatabase=function(callback){if(!this.throttledSaves){this.saveDatabaseInternal(callback);return}if(this.throttledSavePending){this.throttledCallbacks.push(callback);return}var localCallbacks=this.throttledCallbacks;this.throttledCallbacks=[];localCallbacks.unshift(callback);this.throttledSavePending=true;var self=this;this.saveDatabaseInternal(function(err){self.throttledSavePending=false;localCallbacks.forEach(function(pcb){if(typeof pcb==="function"){setTimeout(function(){pcb(err)},1)}});if(self.throttledCallbacks.length>0){self.saveDatabase()}})};Loki.prototype.save=Loki.prototype.saveDatabase;Loki.prototype.deleteDatabase=function(options,callback){var cFun=callback||function(err,data){if(err){throw err}};if(typeof options==="function"&&!callback){cFun=options}if(this.persistenceAdapter!==null){this.persistenceAdapter.deleteDatabase(this.filename,function deleteDatabaseCallback(err){cFun(err)})}else{cFun(new Error("persistenceAdapter not configured"))}};Loki.prototype.autosaveDirty=function(){for(var idx=0;idx0){this.filteredrows=[]}this.filterInitialized=false;return this};Resultset.prototype.toJSON=function(){var copy=this.copy();copy.collection=null;return copy};Resultset.prototype.limit=function(qty){if(!this.filterInitialized&&this.filteredrows.length===0){this.filteredrows=this.collection.prepareFullDocIndex()}var rscopy=new Resultset(this.collection);rscopy.filteredrows=this.filteredrows.slice(0,qty);rscopy.filterInitialized=true;return rscopy};Resultset.prototype.offset=function(pos){if(!this.filterInitialized&&this.filteredrows.length===0){this.filteredrows=this.collection.prepareFullDocIndex()}var rscopy=new Resultset(this.collection);rscopy.filteredrows=this.filteredrows.slice(pos);rscopy.filterInitialized=true;return rscopy};Resultset.prototype.copy=function(){var result=new Resultset(this.collection);if(this.filteredrows.length>0){result.filteredrows=this.filteredrows.slice()}result.filterInitialized=this.filterInitialized;return result};Resultset.prototype.branch=Resultset.prototype.copy;Resultset.prototype.transform=function(transform,parameters){var idx,step,rs=this;if(typeof transform==="string"){if(this.collection.transforms.hasOwnProperty(transform)){transform=this.collection.transforms[transform]}}if(typeof transform!=="object"||!Array.isArray(transform)){throw new Error("Invalid transform")}if(typeof parameters!=="undefined"){transform=Utils.resolveTransformParams(transform,parameters)}for(idx=0;idxobj2[propname])return 1;if(obj1[propname]1){return this.find({$and:filters},firstOnly)}}if(!property||queryObject==="getAll"){if(firstOnly){this.filteredrows=this.collection.data.length>0?[0]:[];this.filterInitialized=true}return this}if(property==="$and"||property==="$or"){this[property](queryObjectOp);if(firstOnly&&this.filteredrows.length>1){this.filteredrows=this.filteredrows.slice(0,1)}return this}if(queryObjectOp===null||(typeof queryObjectOp!=="object"||queryObjectOp instanceof Date)){operator="$eq";value=queryObjectOp}else if(typeof queryObjectOp==="object"){for(key in queryObjectOp){if(hasOwnProperty.call(queryObjectOp,key)){operator=key;value=queryObjectOp[key];break}}}else{throw new Error("Do not know what you want to do.")}if(operator==="$regex"){if(Array.isArray(value)){value=new RegExp(value[0],value[1])}else if(!(value instanceof RegExp)){value=new RegExp(value)}}var usingDotNotation=property.indexOf(".")!==-1;var doIndexCheck=!usingDotNotation&&!this.filterInitialized;if(doIndexCheck&&this.collection.binaryIndices[property]&&indexedOps[operator]){if(this.collection.adaptiveBinaryIndices!==true){this.collection.ensureIndex(property)}searchByIndex=true;index=this.collection.binaryIndices[property]}var fun=LokiOps[operator];var t=this.collection.data;var i=0,len=0;var filter,rowIdx=0;if(this.filterInitialized){filter=this.filteredrows;len=filter.length;if(usingDotNotation){property=property.split(".");for(i=0;i=0){this.filterPipeline[idx]=filter;return this.reapplyFilters()}this.cachedresultset=null;if(this.options.persistent){this.resultdata=[];this.resultsdirty=true}this._addFilter(filter);if(this.sortFunction||this.sortCriteria){this.queueSortPhase()}else{this.queueRebuildEvent()}return this};DynamicView.prototype.applyFind=function(query,uid){this.applyFilter({type:"find",val:query,uid:uid});return this};DynamicView.prototype.applyWhere=function(fun,uid){this.applyFilter({type:"where",val:fun,uid:uid});return this};DynamicView.prototype.removeFilter=function(uid){var idx=this._indexOfFilterWithId(uid);if(idx<0){throw new Error("Dynamic view does not contain a filter with ID: "+uid)}this.filterPipeline.splice(idx,1);this.reapplyFilters();return this};DynamicView.prototype.count=function(){if(this.resultsdirty){this.resultdata=this.resultset.data()}return this.resultset.count()};DynamicView.prototype.data=function(options){if(this.sortDirty||this.resultsdirty){this.performSortPhase({suppressRebuildEvent:true})}return this.options.persistent?this.resultdata:this.resultset.data(options)};DynamicView.prototype.queueRebuildEvent=function(){if(this.rebuildPending){return}this.rebuildPending=true;var self=this;setTimeout(function(){if(self.rebuildPending){self.rebuildPending=false;self.emit("rebuild",self)}},this.options.minRebuildInterval)};DynamicView.prototype.queueSortPhase=function(){if(this.sortDirty){return}this.sortDirty=true;var self=this;if(this.options.sortPriority==="active"){setTimeout(function(){self.performSortPhase()},this.options.minRebuildInterval)}else{this.queueRebuildEvent()}};DynamicView.prototype.performSortPhase=function(options){if(!this.sortDirty&&!this.resultsdirty){return}options=options||{};if(this.sortDirty){if(this.sortFunction){this.resultset.sort(this.sortFunction)}else if(this.sortCriteria){this.resultset.compoundsort(this.sortCriteria)}this.sortDirty=false}if(this.options.persistent){this.resultdata=this.resultset.data();this.resultsdirty=false}if(!options.suppressRebuildEvent){this.emit("rebuild",this)}};DynamicView.prototype.evaluateDocument=function(objIndex,isNew){if(!this.resultset.filterInitialized){if(this.options.persistent){this.resultdata=this.resultset.data()}if(this.sortFunction||this.sortCriteria){this.queueSortPhase()}else{this.queueRebuildEvent()}return}var ofr=this.resultset.filteredrows;var oldPos=isNew?-1:ofr.indexOf(+objIndex);var oldlen=ofr.length;var evalResultset=new Resultset(this.collection);evalResultset.filteredrows=[objIndex];evalResultset.filterInitialized=true;var filter;for(var idx=0,len=this.filterPipeline.length;idxobjIndex){ofr[idx]--}}};DynamicView.prototype.mapReduce=function(mapFunction,reduceFunction){try{return reduceFunction(this.data().map(mapFunction))}catch(err){throw err}};function Collection(name,options){this.name=name;this.data=[];this.idIndex=[];this.binaryIndices={};this.constraints={unique:{},exact:{}};this.uniqueNames=[];this.transforms={};this.objType=name;this.dirty=true;this.cachedIndex=null;this.cachedBinaryIndex=null;this.cachedData=null;var self=this;options=options||{};if(options.hasOwnProperty("unique")){if(!Array.isArray(options.unique)){options.unique=[options.unique]}options.unique.forEach(function(prop){self.uniqueNames.push(prop);self.constraints.unique[prop]=new UniqueIndex(prop)})}if(options.hasOwnProperty("exact")){options.exact.forEach(function(prop){self.constraints.exact[prop]=new ExactIndex(prop)})}this.adaptiveBinaryIndices=options.hasOwnProperty("adaptiveBinaryIndices")?options.adaptiveBinaryIndices:true;this.transactional=options.hasOwnProperty("transactional")?options.transactional:false;this.cloneObjects=options.hasOwnProperty("clone")?options.clone:false;this.cloneMethod=options.hasOwnProperty("cloneMethod")?options.cloneMethod:"parse-stringify";this.asyncListeners=options.hasOwnProperty("asyncListeners")?options.asyncListeners:false;this.disableMeta=options.hasOwnProperty("disableMeta")?options.disableMeta:false;this.disableChangesApi=options.hasOwnProperty("disableChangesApi")?options.disableChangesApi:true;this.disableDeltaChangesApi=options.hasOwnProperty("disableDeltaChangesApi")?options.disableDeltaChangesApi:true;if(this.disableChangesApi){this.disableDeltaChangesApi=true}this.autoupdate=options.hasOwnProperty("autoupdate")?options.autoupdate:false;this.serializableIndices=options.hasOwnProperty("serializableIndices")?options.serializableIndices:true;this.ttl={age:null,ttlInterval:null,daemon:null};this.setTTL(options.ttl||-1,options.ttlInterval);this.maxId=0;this.DynamicViews=[];this.events={insert:[],update:[],"pre-insert":[],"pre-update":[],close:[],flushbuffer:[],error:[],delete:[],warning:[]};this.changes=[];this.ensureId();var indices=[];if(options&&options.indices){if(Object.prototype.toString.call(options.indices)==="[object Array]"){indices=options.indices}else if(typeof options.indices==="string"){indices=[options.indices]}else{throw new TypeError("Indices needs to be a string or an array of strings")}}for(var idx=0;idx=0||propertyName=="$loki"||propertyName=="meta"){delta[propertyName]=newObject[propertyName]}else{var propertyDelta=getObjectDelta(oldObject[propertyName],newObject[propertyName]);if(typeof propertyDelta!=="undefined"&&propertyDelta!={}){delta[propertyName]=propertyDelta}}}}return Object.keys(delta).length===0?undefined:delta}else{return oldObject===newObject?undefined:newObject}}this.getObjectDelta=getObjectDelta;function flushChanges(){self.changes=[]}this.getChanges=function(){return self.changes};this.flushChanges=flushChanges;function insertMeta(obj){var len,idx;if(self.disableMeta||!obj){return}if(Array.isArray(obj)){len=obj.length;for(idx=0;idx0;if(adaptiveBatchOverride){this.adaptiveBinaryIndices=false}for(k;k>1;id=typeof id==="number"?id:parseInt(id,10);if(isNaN(id)){throw new TypeError("Passed id is not an integer")}while(data[min]>1;if(data[mid]dataPosition){index[idx]--}}};Collection.prototype.calculateRangeStart=function(prop,val,adaptive){var rcd=this.data;var index=this.binaryIndices[prop].values;var min=0;var max=index.length-1;var mid=0;if(index.length===0){return-1}var minVal=rcd[index[min]][prop];var maxVal=rcd[index[max]][prop];while(min>1;if(ltHelper(rcd[index[mid]][prop],val,false)){min=mid+1}else{max=mid}}var lbound=min;if(aeqHelper(val,rcd[index[lbound]][prop])){return lbound}if(ltHelper(val,rcd[index[lbound]][prop],false)){return adaptive?lbound:lbound-1}return adaptive?lbound+1:lbound};Collection.prototype.calculateRangeEnd=function(prop,val){var rcd=this.data;var index=this.binaryIndices[prop].values;var min=0;var max=index.length-1;var mid=0;if(index.length===0){return-1}var minVal=rcd[index[min]][prop];var maxVal=rcd[index[max]][prop];while(min>1;if(ltHelper(val,rcd[index[mid]][prop],false)){max=mid}else{min=mid+1}}var ubound=max;if(aeqHelper(val,rcd[index[ubound]][prop])){return ubound}if(gtHelper(val,rcd[index[ubound]][prop],false)){return ubound+1}if(aeqHelper(val,rcd[index[ubound-1]][prop])){return ubound-1}return ubound};Collection.prototype.calculateRange=function(op,prop,val){var rcd=this.data;var index=this.binaryIndices[prop].values;var min=0;var max=index.length-1;var mid=0;var lbound,lval;var ubound,uval;if(rcd.length===0){return[0,-1]}var minVal=rcd[index[min]][prop];var maxVal=rcd[index[max]][prop];switch(op){case"$eq":case"$aeq":if(ltHelper(val,minVal,false)||gtHelper(val,maxVal,false)){return[0,-1]}break;case"$dteq":if(ltHelper(val,minVal,false)||gtHelper(val,maxVal,false)){return[0,-1]}break;case"$gt":if(gtHelper(val,maxVal,true)){return[0,-1]}if(gtHelper(minVal,val,false)){return[min,max]}break;case"$gte":if(gtHelper(val,maxVal,false)){return[0,-1]}if(gtHelper(minVal,val,true)){return[min,max]}break;case"$lt":if(ltHelper(val,minVal,true)){return[0,-1]}if(ltHelper(maxVal,val,false)){return[min,max]}break;case"$lte":if(ltHelper(val,minVal,false)){return[0,-1]}if(ltHelper(maxVal,val,true)){return[min,max]}break;case"$between":if(gtHelper(val[0],maxVal,false)){return[0,-1]}if(ltHelper(val[1],minVal,false)){return[0,-1]}lbound=this.calculateRangeStart(prop,val[0]);ubound=this.calculateRangeEnd(prop,val[1]);if(lbound<0)lbound++;if(ubound>max)ubound--;if(!gtHelper(rcd[index[lbound]][prop],val[0],true))lbound++;if(!ltHelper(rcd[index[ubound]][prop],val[1],true))ubound--;if(ubounddeepProperty(this.data[i],field,deep)){min=deepProperty(this.data[i],field,deep);result.index=this.data[i].$loki}}else{min=deepProperty(this.data[i],field,deep);result.index=this.data[i].$loki}}result.value=min;return result};Collection.prototype.extractNumerical=function(field){return this.extract(field).map(parseBase10).filter(Number).filter(function(n){return!isNaN(n)})};Collection.prototype.avg=function(field){return average(this.extractNumerical(field))};Collection.prototype.stdDev=function(field){return standardDeviation(this.extractNumerical(field))};Collection.prototype.mode=function(field){var dict={},data=this.extract(field);data.forEach(function(obj){if(dict[obj]){dict[obj]+=1}else{dict[obj]=1}});var max,prop,mode;for(prop in dict){if(max){if(max0){root=root[pieces.shift()]}return root}function binarySearch(array,item,fun){var lo=0,hi=array.length,compared,mid;while(lo>1;compared=fun.apply(null,[item,array[mid]]);if(compared===0){return{found:true,index:mid}}else if(compared<0){hi=mid}else{lo=mid+1}}return{found:false,index:hi}}function BSonSort(fun){return function(array,item){return binarySearch(array,item,fun)}}function KeyValueStore(){}KeyValueStore.prototype={keys:[],values:[],sort:function(a,b){return ab?1:0},setSort:function(fun){this.bs=new BSonSort(fun)},bs:function(){return new BSonSort(this.sort)},set:function(key,value){var pos=this.bs(this.keys,key);if(pos.found){this.values[pos.index]=value}else{this.keys.splice(pos.index,0,key);this.values.splice(pos.index,0,value)}},get:function(key){return this.values[binarySearch(this.keys,key,this.sort).index]}};function UniqueIndex(uniqueField){this.field=uniqueField;this.keyMap={};this.lokiMap={}}UniqueIndex.prototype.keyMap={};UniqueIndex.prototype.lokiMap={};UniqueIndex.prototype.set=function(obj){var fieldValue=obj[this.field];if(fieldValue!==null&&typeof fieldValue!=="undefined"){if(this.keyMap[fieldValue]){throw new Error("Duplicate key for property "+this.field+": "+fieldValue)}else{this.keyMap[fieldValue]=obj;this.lokiMap[obj.$loki]=fieldValue}}};UniqueIndex.prototype.get=function(key){return this.keyMap[key]};UniqueIndex.prototype.byId=function(id){return this.keyMap[this.lokiMap[id]]};UniqueIndex.prototype.update=function(obj,doc){if(this.lokiMap[obj.$loki]!==doc[this.field]){var old=this.lokiMap[obj.$loki];this.set(doc);this.keyMap[old]=undefined}else{this.keyMap[obj[this.field]]=doc}};UniqueIndex.prototype.remove=function(key){var obj=this.keyMap[key];if(obj!==null&&typeof obj!=="undefined"){this.keyMap[key]=undefined;this.lokiMap[obj.$loki]=undefined}else{throw new Error("Key is not in unique index: "+this.field)}};UniqueIndex.prototype.clear=function(){this.keyMap={};this.lokiMap={}};function ExactIndex(exactField){this.index={};this.field=exactField}ExactIndex.prototype={set:function add(key,val){if(this.index[key]){this.index[key].push(val)}else{this.index[key]=[val]}},remove:function remove(key,val){var idxSet=this.index[key];for(var i in idxSet){if(idxSet[i]==val){idxSet.splice(i,1)}}if(idxSet.length<1){this.index[key]=undefined}},get:function get(key){return this.index[key]},clear:function clear(key){this.index={}}};function SortedIndex(sortedField){this.field=sortedField}SortedIndex.prototype={keys:[],values:[],sort:function(a,b){return ab?1:0},bs:function(){return new BSonSort(this.sort)},setSort:function(fun){this.bs=new BSonSort(fun)},set:function(key,value){var pos=binarySearch(this.keys,key,this.sort);if(pos.found){this.values[pos.index].push(value)}else{this.keys.splice(pos.index,0,key);this.values.splice(pos.index,0,[value])}},get:function(key){var bsr=binarySearch(this.keys,key,this.sort);if(bsr.found){return this.values[bsr.index]}else{return[]}},getLt:function(key){var bsr=binarySearch(this.keys,key,this.sort);var pos=bsr.index;if(bsr.found)pos--;return this.getAll(key,0,pos)},getGt:function(key){var bsr=binarySearch(this.keys,key,this.sort);var pos=bsr.index;if(bsr.found)pos++;return this.getAll(key,pos,this.keys.length)},getAll:function(key,start,end){var results=[];for(var i=start;i obj2[propname]) return 1; + if (obj1[propname] < obj2[propname]) return -1; + }); + } + // if this has no filters applied, just we need to populate filteredrows first if (!this.filterInitialized && this.filteredrows.length === 0) { // if we have a binary index and no other filters applied, we can use that instead of sorting (again) @@ -3926,13 +3935,17 @@ * dv.applySimpleSort("name"); * * @param {string} propname - Name of property by which to sort. - * @param {boolean=} isdesc - (Optional) If true, the sort will be in descending order. + * @param {object|boolean=} options - boolean for sort descending or options object + * @param {boolean} [options.desc=false] - whether we should sort descending. + * @param {boolean} [options.disableIndexIntersect=false] - whether we should explicity not use array intersection. + * @param {boolean} [options.forceIndexIntersect=false] - force array intersection (if binary index exists). + * @param {boolean} [options.useJavascriptSorting=false] - whether results are sorted via basic javascript sort. * @returns {DynamicView} this DynamicView object, for further chain ops. * @memberof DynamicView */ - DynamicView.prototype.applySimpleSort = function (propname, isdesc) { + DynamicView.prototype.applySimpleSort = function (propname, options) { this.sortCriteria = [ - [propname, isdesc || false] + [propname, options || false] ]; this.sortFunction = null;