UNPKG

203 kBJavaScriptView Raw
1/**
2 * LokiJS
3 * @author Joe Minichino <joe.minichino@gmail.com>
4 *
5 * A lightweight document oriented javascript database
6 */
7(function (root, factory) {
8 if (typeof define === 'function' && define.amd) {
9 // AMD
10 define([], factory);
11 } else if (typeof exports === 'object') {
12 // CommonJS
13 module.exports = factory();
14 } else {
15 // Browser globals
16 root.loki = factory();
17 }
18}(this, function () {
19
20 return (function () {
21 'use strict';
22
23 var hasOwnProperty = Object.prototype.hasOwnProperty;
24
25 var Utils = {
26 copyProperties: function (src, dest) {
27 var prop;
28 for (prop in src) {
29 dest[prop] = src[prop];
30 }
31 },
32 // used to recursively scan hierarchical transform step object for param substitution
33 resolveTransformObject: function (subObj, params, depth) {
34 var prop,
35 pname;
36
37 if (typeof depth !== 'number') {
38 depth = 0;
39 }
40
41 if (++depth >= 10) return subObj;
42
43 for (prop in subObj) {
44 if (typeof subObj[prop] === 'string' && subObj[prop].indexOf("[%lktxp]") === 0) {
45 pname = subObj[prop].substring(8);
46 if (params.hasOwnProperty(pname)) {
47 subObj[prop] = params[pname];
48 }
49 } else if (typeof subObj[prop] === "object") {
50 subObj[prop] = Utils.resolveTransformObject(subObj[prop], params, depth);
51 }
52 }
53
54 return subObj;
55 },
56 // top level utility to resolve an entire (single) transform (array of steps) for parameter substitution
57 resolveTransformParams: function (transform, params) {
58 var idx,
59 clonedStep,
60 resolvedTransform = [];
61
62 if (typeof params === 'undefined') return transform;
63
64 // iterate all steps in the transform array
65 for (idx = 0; idx < transform.length; idx++) {
66 // clone transform so our scan and replace can operate directly on cloned transform
67 clonedStep = JSON.parse(JSON.stringify(transform[idx]));
68 resolvedTransform.push(Utils.resolveTransformObject(clonedStep, params));
69 }
70
71 return resolvedTransform;
72 }
73 };
74
75 /** Helper function for determining 'less-than' conditions for ops, sorting, and binary indices.
76 * In the future we might want $lt and $gt ops to use their own functionality/helper.
77 * Since binary indices on a property might need to index [12, NaN, new Date(), Infinity], we
78 * need this function (as well as gtHelper) to always ensure one value is LT, GT, or EQ to another.
79 */
80 function ltHelper(prop1, prop2, equal) {
81 var cv1, cv2;
82
83 // 'falsy' and Boolean handling
84 if (!prop1 || !prop2 || prop1 === true || prop2 === true) {
85 if ((prop1 === true || prop1 === false) && (prop2 === true || prop2 === false)) {
86 if (equal) {
87 return prop1 === prop2;
88 } else {
89 if (prop1) {
90 return false;
91 } else {
92 return prop2;
93 }
94 }
95 }
96
97 if (prop2 === undefined || prop2 === null || prop1 === true || prop2 === false) {
98 return equal;
99 }
100 if (prop1 === undefined || prop1 === null || prop1 === false || prop2 === true) {
101 return true;
102 }
103 }
104
105 if (prop1 === prop2) {
106 return equal;
107 }
108
109 if (prop1 < prop2) {
110 return true;
111 }
112
113 if (prop1 > prop2) {
114 return false;
115 }
116
117 // not strict equal nor less than nor gt so must be mixed types, convert to string and use that to compare
118 cv1 = prop1.toString();
119 cv2 = prop2.toString();
120
121 if (cv1 == cv2) {
122 return equal;
123 }
124
125 if (cv1 < cv2) {
126 return true;
127 }
128
129 return false;
130 }
131
132 function gtHelper(prop1, prop2, equal) {
133 var cv1, cv2;
134
135 // 'falsy' and Boolean handling
136 if (!prop1 || !prop2 || prop1 === true || prop2 === true) {
137 if ((prop1 === true || prop1 === false) && (prop2 === true || prop2 === false)) {
138 if (equal) {
139 return prop1 === prop2;
140 } else {
141 if (prop1) {
142 return !prop2;
143 } else {
144 return false;
145 }
146 }
147 }
148
149 if (prop1 === undefined || prop1 === null || prop1 === false || prop2 === true) {
150 return equal;
151 }
152 if (prop2 === undefined || prop2 === null || prop1 === true || prop2 === false) {
153 return true;
154 }
155 }
156
157 if (prop1 === prop2) {
158 return equal;
159 }
160
161 if (prop1 > prop2) {
162 return true;
163 }
164
165 if (prop1 < prop2) {
166 return false;
167 }
168
169 // not strict equal nor less than nor gt so must be mixed types, convert to string and use that to compare
170 cv1 = prop1.toString();
171 cv2 = prop2.toString();
172
173 if (cv1 == cv2) {
174 return equal;
175 }
176
177 if (cv1 > cv2) {
178 return true;
179 }
180
181 return false;
182 }
183
184 function sortHelper(prop1, prop2, desc) {
185 if (prop1 === prop2) {
186 return 0;
187 }
188
189 if (ltHelper(prop1, prop2, false)) {
190 return (desc) ? (1) : (-1);
191 }
192
193 if (gtHelper(prop1, prop2, false)) {
194 return (desc) ? (-1) : (1);
195 }
196
197 // not lt, not gt so implied equality-- date compatible
198 return 0;
199 }
200
201 /**
202 * compoundeval() - helper function for compoundsort(), performing individual object comparisons
203 *
204 * @param {array} properties - array of property names, in order, by which to evaluate sort order
205 * @param {object} obj1 - first object to compare
206 * @param {object} obj2 - second object to compare
207 * @returns {integer} 0, -1, or 1 to designate if identical (sortwise) or which should be first
208 */
209 function compoundeval(properties, obj1, obj2) {
210 var res = 0;
211 var prop, field;
212 for (var i = 0, len = properties.length; i < len; i++) {
213 prop = properties[i];
214 field = prop[0];
215 res = sortHelper(obj1[field], obj2[field], prop[1]);
216 if (res !== 0) {
217 return res;
218 }
219 }
220 return 0;
221 }
222
223 /**
224 * dotSubScan - helper function used for dot notation queries.
225 *
226 * @param {object} root - object to traverse
227 * @param {array} paths - array of properties to drill into
228 * @param {function} fun - evaluation function to test with
229 * @param {any} value - comparative value to also pass to (compare) fun
230 * @param {number} poffset - index of the item in 'paths' to start the sub-scan from
231 */
232 function dotSubScan(root, paths, fun, value, poffset) {
233 var pathOffset = poffset || 0;
234 var path = paths[pathOffset];
235 if (root === undefined || root === null || !hasOwnProperty.call(root, path)) {
236 return false;
237 }
238
239 var valueFound = false;
240 var element = root[path];
241 if (pathOffset + 1 >= paths.length) {
242 // if we have already expanded out the dot notation,
243 // then just evaluate the test function and value on the element
244 valueFound = fun(element, value);
245 } else if (Array.isArray(element)) {
246 for (var index = 0, len = element.length; index < len; index += 1) {
247 valueFound = dotSubScan(element[index], paths, fun, value, pathOffset + 1);
248 if (valueFound === true) {
249 break;
250 }
251 }
252 } else {
253 valueFound = dotSubScan(element, paths, fun, value, pathOffset + 1);
254 }
255
256 return valueFound;
257 }
258
259 function containsCheckFn(a) {
260 if (typeof a === 'string' || Array.isArray(a)) {
261 return function (b) {
262 return a.indexOf(b) !== -1;
263 };
264 } else if (typeof a === 'object' && a !== null) {
265 return function (b) {
266 return hasOwnProperty.call(a, b);
267 };
268 }
269 return null;
270 }
271
272 function doQueryOp(val, op) {
273 for (var p in op) {
274 if (hasOwnProperty.call(op, p)) {
275 return LokiOps[p](val, op[p]);
276 }
277 }
278 return false;
279 }
280
281 var LokiOps = {
282 // comparison operators
283 // a is the value in the collection
284 // b is the query value
285 $eq: function (a, b) {
286 return a === b;
287 },
288
289 // abstract/loose equality
290 $aeq: function (a, b) {
291 return a == b;
292 },
293
294 $ne: function (a, b) {
295 // ecma 5 safe test for NaN
296 if (b !== b) {
297 // ecma 5 test value is not NaN
298 return (a === a);
299 }
300
301 return a !== b;
302 },
303
304 $dteq: function (a, b) {
305 if (ltHelper(a, b, false)) {
306 return false;
307 }
308 return !gtHelper(a, b, false);
309 },
310
311 $gt: function (a, b) {
312 return gtHelper(a, b, false);
313 },
314
315 $gte: function (a, b) {
316 return gtHelper(a, b, true);
317 },
318
319 $lt: function (a, b) {
320 return ltHelper(a, b, false);
321 },
322
323 $lte: function (a, b) {
324 return ltHelper(a, b, true);
325 },
326
327 // ex : coll.find({'orderCount': {$between: [10, 50]}});
328 $between: function (a, vals) {
329 if (a === undefined || a === null) return false;
330 return (gtHelper(a, vals[0], true) && ltHelper(a, vals[1], true));
331 },
332
333 $in: function (a, b) {
334 return b.indexOf(a) !== -1;
335 },
336
337 $nin: function (a, b) {
338 return b.indexOf(a) === -1;
339 },
340
341 $keyin: function (a, b) {
342 return a in b;
343 },
344
345 $nkeyin: function (a, b) {
346 return !(a in b);
347 },
348
349 $definedin: function (a, b) {
350 return b[a] !== undefined;
351 },
352
353 $undefinedin: function (a, b) {
354 return b[a] === undefined;
355 },
356
357 $regex: function (a, b) {
358 return b.test(a);
359 },
360
361 $containsString: function (a, b) {
362 return (typeof a === 'string') && (a.indexOf(b) !== -1);
363 },
364
365 $containsNone: function (a, b) {
366 return !LokiOps.$containsAny(a, b);
367 },
368
369 $containsAny: function (a, b) {
370 var checkFn = containsCheckFn(a);
371 if (checkFn !== null) {
372 return (Array.isArray(b)) ? (b.some(checkFn)) : (checkFn(b));
373 }
374 return false;
375 },
376
377 $contains: function (a, b) {
378 var checkFn = containsCheckFn(a);
379 if (checkFn !== null) {
380 return (Array.isArray(b)) ? (b.every(checkFn)) : (checkFn(b));
381 }
382 return false;
383 },
384
385 $type: function (a, b) {
386 var type = typeof a;
387 if (type === 'object') {
388 if (Array.isArray(a)) {
389 type = 'array';
390 } else if (a instanceof Date) {
391 type = 'date';
392 }
393 }
394 return (typeof b !== 'object') ? (type === b) : doQueryOp(type, b);
395 },
396
397 $size: function (a, b) {
398 if (Array.isArray(a)) {
399 return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b);
400 }
401 return false;
402 },
403
404 $len: function (a, b) {
405 if (typeof a === 'string') {
406 return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b);
407 }
408 return false;
409 },
410
411 $where: function (a, b) {
412 return b(a) === true;
413 },
414
415 // field-level logical operators
416 // a is the value in the collection
417 // b is the nested query operation (for '$not')
418 // or an array of nested query operations (for '$and' and '$or')
419 $not: function (a, b) {
420 return !doQueryOp(a, b);
421 },
422
423 $and: function (a, b) {
424 for (var idx = 0, len = b.length; idx < len; idx += 1) {
425 if (!doQueryOp(a, b[idx])) {
426 return false;
427 }
428 }
429 return true;
430 },
431
432 $or: function (a, b) {
433 for (var idx = 0, len = b.length; idx < len; idx += 1) {
434 if (doQueryOp(a, b[idx])) {
435 return true;
436 }
437 }
438 return false;
439 }
440 };
441
442 // making indexing opt-in... our range function knows how to deal with these ops :
443 var indexedOpsList = ['$eq', '$aeq', '$dteq', '$gt', '$gte', '$lt', '$lte', '$in', '$between'];
444
445 function clone(data, method) {
446 var cloneMethod = method || 'parse-stringify',
447 cloned;
448
449 switch (cloneMethod) {
450 case "parse-stringify":
451 cloned = JSON.parse(JSON.stringify(data));
452 break;
453 case "jquery-extend-deep":
454 cloned = jQuery.extend(true, {}, data);
455 break;
456 case "shallow":
457 cloned = Object.create(data.prototype || null);
458 Object.keys(data).map(function (i) {
459 cloned[i] = data[i];
460 });
461 break;
462 default:
463 break;
464 }
465
466 return cloned;
467 }
468
469 function cloneObjectArray(objarray, method) {
470 var i,
471 result = [];
472
473 if (method == "parse-stringify") {
474 return clone(objarray, method);
475 }
476
477 i = objarray.length - 1;
478
479 for (; i <= 0; i--) {
480 result.push(clone(objarray[i], method));
481 }
482
483 return result;
484 }
485
486 function localStorageAvailable() {
487 try {
488 return (window && window.localStorage !== undefined && window.localStorage !== null);
489 } catch (e) {
490 return false;
491 }
492 }
493
494
495 /**
496 * LokiEventEmitter is a minimalist version of EventEmitter. It enables any
497 * constructor that inherits EventEmitter to emit events and trigger
498 * listeners that have been added to the event through the on(event, callback) method
499 *
500 * @constructor LokiEventEmitter
501 */
502 function LokiEventEmitter() {}
503
504 /**
505 * @prop {hashmap} events - a hashmap, with each property being an array of callbacks
506 * @memberof LokiEventEmitter
507 */
508 LokiEventEmitter.prototype.events = {};
509
510 /**
511 * @prop {boolean} asyncListeners - boolean determines whether or not the callbacks associated with each event
512 * should happen in an async fashion or not
513 * Default is false, which means events are synchronous
514 * @memberof LokiEventEmitter
515 */
516 LokiEventEmitter.prototype.asyncListeners = false;
517
518 /**
519 * on(eventName, listener) - adds a listener to the queue of callbacks associated to an event
520 * @param {string|string[]} eventName - the name(s) of the event(s) to listen to
521 * @param {function} listener - callback function of listener to attach
522 * @returns {int} the index of the callback in the array of listeners for a particular event
523 * @memberof LokiEventEmitter
524 */
525 LokiEventEmitter.prototype.on = function (eventName, listener) {
526 var event;
527 var self = this;
528
529 if (Array.isArray(eventName)) {
530 eventName.forEach(function(currentEventName) {
531 self.on(currentEventName, listener);
532 });
533 return listener;
534 }
535
536 event = this.events[eventName];
537 if (!event) {
538 event = this.events[eventName] = [];
539 }
540 event.push(listener);
541 return listener;
542 };
543
544 /**
545 * emit(eventName, data) - emits a particular event
546 * with the option of passing optional parameters which are going to be processed by the callback
547 * provided signatures match (i.e. if passing emit(event, arg0, arg1) the listener should take two parameters)
548 * @param {string} eventName - the name of the event
549 * @param {object=} data - optional object passed with the event
550 * @memberof LokiEventEmitter
551 */
552 LokiEventEmitter.prototype.emit = function (eventName, data) {
553 var self = this;
554 if (eventName && this.events[eventName]) {
555 this.events[eventName].forEach(function (listener) {
556 if (self.asyncListeners) {
557 setTimeout(function () {
558 listener(data);
559 }, 1);
560 } else {
561 listener(data);
562 }
563
564 });
565 } else {
566 throw new Error('No event ' + eventName + ' defined');
567 }
568 };
569
570 /**
571 * Alias of LokiEventEmitter.prototype.on
572 * addListener(eventName, listener) - adds a listener to the queue of callbacks associated to an event
573 * @param {string|string[]} eventName - the name(s) of the event(s) to listen to
574 * @param {function} listener - callback function of listener to attach
575 * @returns {int} the index of the callback in the array of listeners for a particular event
576 * @memberof LokiEventEmitter
577 */
578 LokiEventEmitter.prototype.addListener = LokiEventEmitter.prototype.on;
579
580 /**
581 * removeListener() - removes the listener at position 'index' from the event 'eventName'
582 * @param {string|string[]} eventName - the name(s) of the event(s) which the listener is attached to
583 * @param {function} listener - the listener callback function to remove from emitter
584 * @memberof LokiEventEmitter
585 */
586 LokiEventEmitter.prototype.removeListener = function (eventName, listener) {
587 var self = this;
588 if (Array.isArray(eventName)) {
589 eventName.forEach(function(currentEventName) {
590 self.removeListener(currentEventName, listen);
591 });
592 }
593
594 if (this.events[eventName]) {
595 var listeners = this.events[eventName];
596 listeners.splice(listeners.indexOf(listener), 1);
597 }
598 };
599
600 /**
601 * Loki: The main database class
602 * @constructor Loki
603 * @implements LokiEventEmitter
604 * @param {string} filename - name of the file to be saved to
605 * @param {object=} options - (Optional) config options object
606 * @param {string} options.env - override environment detection as 'NODEJS', 'BROWSER', 'CORDOVA'
607 * @param {boolean} options.verbose - enable console output (default is 'false')
608 * @param {boolean} options.autosave - enables autosave
609 * @param {int} options.autosaveInterval - time interval (in milliseconds) between saves (if dirty)
610 * @param {boolean} options.autoload - enables autoload on loki instantiation
611 * @param {function} options.autoloadCallback - user callback called after database load
612 * @param {adapter} options.adapter - an instance of a loki persistence adapter
613 * @param {string} options.serializationMethod - ['normal', 'pretty', 'destructured']
614 * @param {string} options.destructureDelimiter - string delimiter used for destructured serialization
615 */
616 function Loki(filename, options) {
617 this.filename = filename || 'loki.db';
618 this.collections = [];
619
620 // persist version of code which created the database to the database.
621 // could use for upgrade scenarios
622 this.databaseVersion = 1.1;
623 this.engineVersion = 1.1;
624
625 // autosave support (disabled by default)
626 // pass autosave: true, autosaveInterval: 6000 in options to set 6 second autosave
627 this.autosave = false;
628 this.autosaveInterval = 5000;
629 this.autosaveHandle = null;
630
631 this.options = {};
632
633 // currently keeping persistenceMethod and persistenceAdapter as loki level properties that
634 // will not or cannot be deserialized. You are required to configure persistence every time
635 // you instantiate a loki object (or use default environment detection) in order to load the database anyways.
636
637 // persistenceMethod could be 'fs', 'localStorage', or 'adapter'
638 // this is optional option param, otherwise environment detection will be used
639 // if user passes their own adapter we will force this method to 'adapter' later, so no need to pass method option.
640 this.persistenceMethod = null;
641
642 // retain reference to optional (non-serializable) persistenceAdapter 'instance'
643 this.persistenceAdapter = null;
644
645 // enable console output if verbose flag is set (disabled by default)
646 this.verbose = options && options.hasOwnProperty('verbose') ? options.verbose : false;
647
648 this.events = {
649 'init': [],
650 'loaded': [],
651 'flushChanges': [],
652 'close': [],
653 'changes': [],
654 'warning': []
655 };
656
657 var getENV = function () {
658 // if (typeof global !== 'undefined' && (global.android || global.NSObject)) {
659 // //If no adapter is set use the default nativescript adapter
660 // if (!options.adapter) {
661 // var LokiNativescriptAdapter = require('./loki-nativescript-adapter');
662 // options.adapter=new LokiNativescriptAdapter();
663 // }
664 // return 'NATIVESCRIPT'; //nativescript
665 // }
666
667 if (typeof window === 'undefined') {
668 return 'NODEJS';
669 }
670
671 if (typeof global !== 'undefined' && global.window) {
672 return 'NODEJS'; //node-webkit
673 }
674
675 if (typeof document !== 'undefined') {
676 if (document.URL.indexOf('http://') === -1 && document.URL.indexOf('https://') === -1) {
677 return 'CORDOVA';
678 }
679 return 'BROWSER';
680 }
681 return 'CORDOVA';
682 };
683
684 // refactored environment detection due to invalid detection for browser environments.
685 // if they do not specify an options.env we want to detect env rather than default to nodejs.
686 // currently keeping two properties for similar thing (options.env and options.persistenceMethod)
687 // might want to review whether we can consolidate.
688 if (options && options.hasOwnProperty('env')) {
689 this.ENV = options.env;
690 } else {
691 this.ENV = getENV();
692 }
693
694 // not sure if this is necessary now that i have refactored the line above
695 if (this.ENV === 'undefined') {
696 this.ENV = 'NODEJS';
697 }
698
699 //if (typeof (options) !== 'undefined') {
700 this.configureOptions(options, true);
701 //}
702
703 this.on('init', this.clearChanges);
704
705 }
706
707 // db class is an EventEmitter
708 Loki.prototype = new LokiEventEmitter();
709 Loki.prototype.constructor = Loki;
710
711 // experimental support for browserify's abstract syntax scan to pick up dependency of indexed adapter.
712 // Hopefully, once this hits npm a browserify require of lokijs should scan the main file and detect this indexed adapter reference.
713 Loki.prototype.getIndexedAdapter = function () {
714 var adapter;
715
716 if (typeof require === 'function') {
717 adapter = require("./loki-indexed-adapter.js");
718 }
719
720 return adapter;
721 };
722
723
724 /**
725 * Allows reconfiguring database options
726 *
727 * @param {object} options - configuration options to apply to loki db object
728 * @param {string} options.env - override environment detection as 'NODEJS', 'BROWSER', 'CORDOVA'
729 * @param {boolean} options.verbose - enable console output (default is 'false')
730 * @param {boolean} options.autosave - enables autosave
731 * @param {int} options.autosaveInterval - time interval (in milliseconds) between saves (if dirty)
732 * @param {boolean} options.autoload - enables autoload on loki instantiation
733 * @param {function} options.autoloadCallback - user callback called after database load
734 * @param {adapter} options.adapter - an instance of a loki persistence adapter
735 * @param {string} options.serializationMethod - ['normal', 'pretty', 'destructured']
736 * @param {string} options.destructureDelimiter - string delimiter used for destructured serialization
737 * @param {boolean} initialConfig - (internal) true is passed when loki ctor is invoking
738 * @memberof Loki
739 */
740 Loki.prototype.configureOptions = function (options, initialConfig) {
741 var defaultPersistence = {
742 'NODEJS': 'fs',
743 'BROWSER': 'localStorage',
744 'CORDOVA': 'localStorage'
745 },
746 persistenceMethods = {
747 'fs': LokiFsAdapter,
748 'localStorage': LokiLocalStorageAdapter
749 };
750
751 this.options = {};
752
753 this.persistenceMethod = null;
754 // retain reference to optional persistence adapter 'instance'
755 // currently keeping outside options because it can't be serialized
756 this.persistenceAdapter = null;
757
758 // process the options
759 if (typeof (options) !== 'undefined') {
760 this.options = options;
761
762 if (this.options.hasOwnProperty('persistenceMethod')) {
763 // check if the specified persistence method is known
764 if (typeof (persistenceMethods[options.persistenceMethod]) == 'function') {
765 this.persistenceMethod = options.persistenceMethod;
766 this.persistenceAdapter = new persistenceMethods[options.persistenceMethod]();
767 }
768 // should be throw an error here, or just fall back to defaults ??
769 }
770
771 // if user passes adapter, set persistence mode to adapter and retain persistence adapter instance
772 if (this.options.hasOwnProperty('adapter')) {
773 this.persistenceMethod = 'adapter';
774 this.persistenceAdapter = options.adapter;
775 this.options.adapter = null;
776 }
777
778
779 // if they want to load database on loki instantiation, now is a good time to load... after adapter set and before possible autosave initiation
780 if (options.autoload && initialConfig) {
781 // for autoload, let the constructor complete before firing callback
782 var self = this;
783 setTimeout(function () {
784 self.loadDatabase(options, options.autoloadCallback);
785 }, 1);
786 }
787
788 if (this.options.hasOwnProperty('autosaveInterval')) {
789 this.autosaveDisable();
790 this.autosaveInterval = parseInt(this.options.autosaveInterval, 10);
791 }
792
793 if (this.options.hasOwnProperty('autosave') && this.options.autosave) {
794 this.autosaveDisable();
795 this.autosave = true;
796
797 if (this.options.hasOwnProperty('autosaveCallback')) {
798 this.autosaveEnable(options, options.autosaveCallback);
799 } else {
800 this.autosaveEnable();
801 }
802 }
803 } // end of options processing
804
805 // ensure defaults exists for options which were not set
806 if (!this.options.hasOwnProperty('serializationMethod')) {
807 this.options.serializationMethod = 'normal';
808 }
809
810 // ensure passed or default option exists
811 if (!this.options.hasOwnProperty('destructureDelimiter')) {
812 this.options.destructureDelimiter = '$<\n';
813 }
814
815 // if by now there is no adapter specified by user nor derived from persistenceMethod: use sensible defaults
816 if (this.persistenceAdapter === null) {
817 this.persistenceMethod = defaultPersistence[this.ENV];
818 if (this.persistenceMethod) {
819 this.persistenceAdapter = new persistenceMethods[this.persistenceMethod]();
820 }
821 }
822
823 };
824
825 /**
826 * Copies 'this' database into a new Loki instance. Object references are shared to make lightweight.
827 *
828 * @param {object} options - apply or override collection level settings
829 * @param {bool} options.removeNonSerializable - nulls properties not safe for serialization.
830 * @memberof Loki
831 */
832 Loki.prototype.copy = function(options) {
833 var databaseCopy = new Loki(this.filename);
834 var clen, idx;
835
836 options = options || {};
837
838 // currently inverting and letting loadJSONObject do most of the work
839 databaseCopy.loadJSONObject(this, { retainDirtyFlags: true });
840
841 // since our JSON serializeReplacer is not invoked for reference database adapters, this will let us mimic
842 if(options.hasOwnProperty("removeNonSerializable") && options.removeNonSerializable === true) {
843 databaseCopy.autosaveHandle = null;
844 databaseCopy.persistenceAdapter = null;
845
846 clen = databaseCopy.collections.length;
847 for (idx=0; idx<clen; idx++) {
848 databaseCopy.collections[idx].constraints = null;
849 databaseCopy.collections[idx].ttl = null;
850 }
851 }
852
853 return databaseCopy;
854 };
855
856 /**
857 * Shorthand method for quickly creating and populating an anonymous collection.
858 * This collection is not referenced internally so upon losing scope it will be garbage collected.
859 *
860 * @example
861 * var results = new loki().anonym(myDocArray).find({'age': {'$gt': 30} });
862 *
863 * @param {Array} docs - document array to initialize the anonymous collection with
864 * @param {object} options - configuration object, see {@link Loki#addCollection} options
865 * @returns {Collection} New collection which you can query or chain
866 * @memberof Loki
867 */
868 Loki.prototype.anonym = function (docs, options) {
869 var collection = new Collection('anonym', options);
870 collection.insert(docs);
871
872 if (this.verbose)
873 collection.console = console;
874
875 return collection;
876 };
877
878 /**
879 * Adds a collection to the database.
880 * @param {string} name - name of collection to add
881 * @param {object=} options - (optional) options to configure collection with.
882 * @param {array} options.unique - array of property names to define unique constraints for
883 * @param {array} options.exact - array of property names to define exact constraints for
884 * @param {array} options.indices - array property names to define binary indexes for
885 * @param {boolean} options.asyncListeners - default is false
886 * @param {boolean} options.disableChangesApi - default is true
887 * @param {boolean} options.autoupdate - use Object.observe to update objects automatically (default: false)
888 * @param {boolean} options.clone - specify whether inserts and queries clone to/from user
889 * @param {string} options.cloneMethod - 'parse-stringify' (default), 'jquery-extend-deep', 'shallow'
890 * @param {int} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default.
891 * @returns {Collection} a reference to the collection which was just added
892 * @memberof Loki
893 */
894 Loki.prototype.addCollection = function (name, options) {
895 var collection = new Collection(name, options);
896 this.collections.push(collection);
897
898 if (this.verbose)
899 collection.console = console;
900
901 return collection;
902 };
903
904 Loki.prototype.loadCollection = function (collection) {
905 if (!collection.name) {
906 throw new Error('Collection must have a name property to be loaded');
907 }
908 this.collections.push(collection);
909 };
910
911 /**
912 * Retrieves reference to a collection by name.
913 * @param {string} collectionName - name of collection to look up
914 * @returns {Collection} Reference to collection in database by that name, or null if not found
915 * @memberof Loki
916 */
917 Loki.prototype.getCollection = function (collectionName) {
918 var i,
919 len = this.collections.length;
920
921 for (i = 0; i < len; i += 1) {
922 if (this.collections[i].name === collectionName) {
923 return this.collections[i];
924 }
925 }
926
927 // no such collection
928 this.emit('warning', 'collection ' + collectionName + ' not found');
929 return null;
930 };
931
932 Loki.prototype.listCollections = function () {
933
934 var i = this.collections.length,
935 colls = [];
936
937 while (i--) {
938 colls.push({
939 name: this.collections[i].name,
940 type: this.collections[i].objType,
941 count: this.collections[i].data.length
942 });
943 }
944 return colls;
945 };
946
947 /**
948 * Removes a collection from the database.
949 * @param {string} collectionName - name of collection to remove
950 * @memberof Loki
951 */
952 Loki.prototype.removeCollection = function (collectionName) {
953 var i,
954 len = this.collections.length;
955
956 for (i = 0; i < len; i += 1) {
957 if (this.collections[i].name === collectionName) {
958 var tmpcol = new Collection(collectionName, {});
959 var curcol = this.collections[i];
960 for (var prop in curcol) {
961 if (curcol.hasOwnProperty(prop) && tmpcol.hasOwnProperty(prop)) {
962 curcol[prop] = tmpcol[prop];
963 }
964 }
965 this.collections.splice(i, 1);
966 return;
967 }
968 }
969 };
970
971 Loki.prototype.getName = function () {
972 return this.name;
973 };
974
975 /**
976 * serializeReplacer - used to prevent certain properties from being serialized
977 *
978 */
979 Loki.prototype.serializeReplacer = function (key, value) {
980 switch (key) {
981 case 'autosaveHandle':
982 case 'persistenceAdapter':
983 case 'constraints':
984 case 'ttl':
985 return null;
986 default:
987 return value;
988 }
989 };
990
991 /**
992 * Serialize database to a string which can be loaded via {@link Loki#loadJSON}
993 *
994 * @returns {string} Stringified representation of the loki database.
995 * @memberof Loki
996 */
997 Loki.prototype.serialize = function (options) {
998 options = options || {};
999
1000 if (!options.hasOwnProperty("serializationMethod")) {
1001 options.serializationMethod = this.options.serializationMethod;
1002 }
1003
1004 switch(options.serializationMethod) {
1005 case "normal": return JSON.stringify(this, this.serializeReplacer);
1006 case "pretty": return JSON.stringify(this, this.serializeReplacer, 2);
1007 case "destructured": return this.serializeDestructured(); // use default options
1008 default: return JSON.stringify(this, this.serializeReplacer);
1009 }
1010 };
1011
1012 // alias of serialize
1013 Loki.prototype.toJson = Loki.prototype.serialize;
1014
1015 /**
1016 * Destructured JSON serialization routine to allow alternate serialization methods.
1017 * Internally, Loki supports destructuring via loki "serializationMethod' option and
1018 * the optional LokiPartitioningAdapter class. It is also available if you wish to do
1019 * your own structured persistence or data exchange.
1020 *
1021 * @param {object=} options - output format options for use externally to loki
1022 * @param {bool=} options.partitioned - (default: false) whether db and each collection are separate
1023 * @param {int=} options.partition - can be used to only output an individual collection or db (-1)
1024 * @param {bool=} options.delimited - (default: true) whether subitems are delimited or subarrays
1025 * @param {string=} options.delimiter - override default delimiter
1026 *
1027 * @returns {string|array} A custom, restructured aggregation of independent serializations.
1028 * @memberof Loki
1029 */
1030 Loki.prototype.serializeDestructured = function(options) {
1031 var idx, sidx, result, resultlen;
1032 var reconstruct = [];
1033 var dbcopy;
1034
1035 options = options || {};
1036
1037 if (!options.hasOwnProperty("partitioned")) {
1038 options.partitioned = false;
1039 }
1040
1041 if (!options.hasOwnProperty("delimited")) {
1042 options.delimited = true;
1043 }
1044
1045 if (!options.hasOwnProperty("delimiter")) {
1046 options.delimiter = this.options.destructureDelimiter;
1047 }
1048
1049 // 'partitioned' along with 'partition' of 0 or greater is a request for single collection serialization
1050 if (options.partitioned === true && options.hasOwnProperty("partition") && options.partition >= 0) {
1051 return this.serializeCollection({
1052 delimited: options.delimited,
1053 delimiter: options.delimiter,
1054 collectionIndex: options.partition
1055 });
1056 }
1057
1058 // not just an individual collection, so we will need to serialize db container via shallow copy
1059 dbcopy = new Loki(this.filename);
1060 dbcopy.loadJSONObject(this);
1061
1062 for(idx=0; idx < dbcopy.collections.length; idx++) {
1063 dbcopy.collections[idx].data = [];
1064 }
1065
1066 // if we -only- wanted the db container portion, return it now
1067 if (options.partitioned === true && options.partition === -1) {
1068 // since we are deconstructing, override serializationMethod to normal for here
1069 return dbcopy.serialize({
1070 serializationMethod: "normal"
1071 });
1072 }
1073
1074 // at this point we must be deconstructing the entire database
1075 // start by pushing db serialization into first array element
1076 reconstruct.push(dbcopy.serialize({
1077 serializationMethod: "normal"
1078 }));
1079
1080 dbcopy = null;
1081
1082 // push collection data into subsequent elements
1083 for(idx=0; idx < this.collections.length; idx++) {
1084 result = this.serializeCollection({
1085 delimited: options.delimited,
1086 delimiter: options.delimiter,
1087 collectionIndex: idx
1088 });
1089
1090 // NDA : Non-Delimited Array : one iterable concatenated array with empty string collection partitions
1091 if (options.partitioned === false && options.delimited === false) {
1092 if (!Array.isArray(result)) {
1093 throw new Error("a nondelimited, non partitioned collection serialization did not return an expected array");
1094 }
1095
1096 // Array.concat would probably duplicate memory overhead for copying strings.
1097 // Instead copy each individually, and clear old value after each copy.
1098 // Hopefully this will allow g.c. to reduce memory pressure, if needed.
1099 resultlen = result.length;
1100
1101 for (sidx=0; sidx < resultlen; sidx++) {
1102 reconstruct.push(result[sidx]);
1103 result[sidx] = null;
1104 }
1105
1106 reconstruct.push("");
1107 }
1108 else {
1109 reconstruct.push(result);
1110 }
1111 }
1112
1113 // Reconstruct / present results according to four combinations : D, DA, NDA, NDAA
1114 if (options.partitioned) {
1115 // DA : Delimited Array of strings [0] db [1] collection [n] collection { partitioned: true, delimited: true }
1116 // useful for simple future adaptations of existing persistence adapters to save collections separately
1117 if (options.delimited) {
1118 return reconstruct;
1119 }
1120 // NDAA : Non-Delimited Array with subArrays. db at [0] and collection subarrays at [n] { partitioned: true, delimited : false }
1121 // This format might be the most versatile for 'rolling your own' partitioned sync or save.
1122 // Memory overhead can be reduced by specifying a specific partition, but at this code path they did not, so its all.
1123 else {
1124 return reconstruct;
1125 }
1126 }
1127 else {
1128 // D : one big Delimited string { partitioned: false, delimited : true }
1129 // This is the method Loki will use internally if 'destructured'.
1130 // Little memory overhead improvements but does not require multiple asynchronous adapter call scheduling
1131 if (options.delimited) {
1132 // indicate no more collections
1133 reconstruct.push("");
1134
1135 return reconstruct.join(options.delimiter);
1136 }
1137 // NDA : Non-Delimited Array : one iterable array with empty string collection partitions { partitioned: false, delimited: false }
1138 // This format might be best candidate for custom synchronous syncs or saves
1139 else {
1140 // indicate no more collections
1141 reconstruct.push("");
1142
1143 return reconstruct;
1144 }
1145 }
1146
1147 reconstruct.push("");
1148
1149 return reconstruct.join(delim);
1150 };
1151
1152 /**
1153 * Utility method to serialize a collection in a 'destructured' format
1154 *
1155 * @param {object} options - used to determine output of method
1156 * @param {int=} options.delimited - whether to return single delimited string or an array
1157 * @param {string=} options.delimiter - (optional) if delimited, this is delimiter to use
1158 * @param {int} options.collectionIndex - specify which collection to serialize data for
1159 *
1160 * @returns {string|array} A custom, restructured aggregation of independent serializations for a single collection.
1161 * @memberof Loki
1162 */
1163 Loki.prototype.serializeCollection = function(options) {
1164 var doccount,
1165 docidx,
1166 resultlines = [];
1167
1168 options = options || {};
1169
1170 if (!options.hasOwnProperty("delimited")) {
1171 options.delimited = true;
1172 }
1173
1174 if (!options.hasOwnProperty("collectionIndex")) {
1175 throw new Error("serializeCollection called without 'collectionIndex' option");
1176 }
1177
1178 doccount = this.collections[options.collectionIndex].data.length;
1179
1180 resultlines = [];
1181
1182 for(docidx=0; docidx<doccount; docidx++) {
1183 resultlines.push(JSON.stringify(this.collections[options.collectionIndex].data[docidx]));
1184 }
1185
1186 // D and DA
1187 if (options.delimited) {
1188 // indicate no more documents in collection (via empty delimited string)
1189 resultlines.push("");
1190
1191 return resultlines.join(options.delimiter);
1192 }
1193 else {
1194 // NDAA and NDA
1195 return resultlines;
1196 }
1197 };
1198
1199 /**
1200 * Destructured JSON deserialization routine to minimize memory overhead.
1201 * Internally, Loki supports destructuring via loki "serializationMethod' option and
1202 * the optional LokiPartitioningAdapter class. It is also available if you wish to do
1203 * your own structured persistence or data exchange.
1204 *
1205 * @param {string|array} destructuredSource - destructured json or array to deserialize from
1206 * @param {object=} options - source format options
1207 * @param {bool=} options.partitioned - (default: false) whether db and each collection are separate
1208 * @param {int=} options.partition - can be used to deserialize only a single partition
1209 * @param {bool=} options.delimited - (default: true) whether subitems are delimited or subarrays
1210 * @param {string=} options.delimiter - override default delimiter
1211 *
1212 * @returns {object|array} An object representation of the deserialized database, not yet applied to 'this' db or document array
1213 * @memberof Loki
1214 */
1215 Loki.prototype.deserializeDestructured = function(destructuredSource, options) {
1216 var workarray=[];
1217 var len, cdb;
1218 var idx, collIndex=0, collCount, lineIndex=1, done=false;
1219 var currLine, currObject;
1220
1221 options = options || {};
1222
1223 if (!options.hasOwnProperty("partitioned")) {
1224 options.partitioned = false;
1225 }
1226
1227 if (!options.hasOwnProperty("delimited")) {
1228 options.delimited = true;
1229 }
1230
1231 if (!options.hasOwnProperty("delimiter")) {
1232 options.delimiter = this.options.destructureDelimiter;
1233 }
1234
1235 // Partitioned
1236 // DA : Delimited Array of strings [0] db [1] collection [n] collection { partitioned: true, delimited: true }
1237 // NDAA : Non-Delimited Array with subArrays. db at [0] and collection subarrays at [n] { partitioned: true, delimited : false }
1238 // -or- single partition
1239 if (options.partitioned) {
1240 // handle single partition
1241 if (options.hasOwnProperty('partition')) {
1242 // db only
1243 if (options.partition === -1) {
1244 cdb = JSON.parse(destructuredSource[0]);
1245
1246 return cdb;
1247 }
1248
1249 // single collection, return doc array
1250 return this.deserializeCollection(destructuredSource[options.partition+1], options);
1251 }
1252
1253 // Otherwise we are restoring an entire partitioned db
1254 cdb = JSON.parse(destructuredSource[0]);
1255 collCount = cdb.collections.length;
1256 for(collIndex=0; collIndex<collCount; collIndex++) {
1257 // attach each collection docarray to container collection data, add 1 to collection array index since db is at 0
1258 cdb.collections[collIndex].data = this.deserializeCollection(destructuredSource[collIndex+1], options);
1259 }
1260
1261 return cdb;
1262 }
1263
1264 // Non-Partitioned
1265 // D : one big Delimited string { partitioned: false, delimited : true }
1266 // NDA : Non-Delimited Array : one iterable array with empty string collection partitions { partitioned: false, delimited: false }
1267
1268 // D
1269 if (options.delimited) {
1270 workarray = destructuredSource.split(options.delimiter);
1271 destructuredSource = null; // lower memory pressure
1272 len = workarray.length;
1273
1274 if (len === 0) {
1275 return null;
1276 }
1277 }
1278 // NDA
1279 else {
1280 workarray = destructuredSource;
1281 }
1282
1283 // first line is database and collection shells
1284 cdb = JSON.parse(workarray[0]);
1285 collCount = cdb.collections.length;
1286 workarray[0] = null;
1287
1288 while (!done) {
1289 currLine = workarray[lineIndex];
1290
1291 // empty string indicates either end of collection or end of file
1292 if (workarray[lineIndex] === "") {
1293 // if no more collections to load into, we are done
1294 if (++collIndex > collCount) {
1295 done = true;
1296 }
1297 }
1298 else {
1299 currObject = JSON.parse(workarray[lineIndex]);
1300 cdb.collections[collIndex].data.push(currObject);
1301 }
1302
1303 // lower memory pressure and advance iterator
1304 workarray[lineIndex++] = null;
1305 }
1306
1307 return cdb;
1308 };
1309
1310 /**
1311 * Deserializes a destructured collection.
1312 *
1313 * @param {string|array} destructuredSource - destructured representation of collection to inflate
1314 * @param {object} options - used to describe format of destructuredSource input
1315 * @param {int} options.delimited - whether source is delimited string or an array
1316 * @param {string} options.delimiter - (optional) if delimited, this is delimiter to use
1317 *
1318 * @returns {array} an array of documents to attach to collection.data.
1319 * @memberof Loki
1320 */
1321 Loki.prototype.deserializeCollection = function(destructuredSource, options) {
1322 var workarray=[];
1323 var idx, len;
1324
1325 options = options || {};
1326
1327 if (!options.hasOwnProperty("partitioned")) {
1328 options.partitioned = false;
1329 }
1330
1331 if (!options.hasOwnProperty("delimited")) {
1332 options.delimited = true;
1333 }
1334
1335 if (!options.hasOwnProperty("delimiter")) {
1336 options.delimiter = this.options.destructureDelimiter;
1337 }
1338
1339 if (options.delimited) {
1340 workarray = destructuredSource.split(options.delimiter);
1341 workarray.pop();
1342 }
1343 else {
1344 workarray = destructuredSource;
1345 }
1346
1347 len = workarray.length;
1348 for (idx=0; idx < len; idx++) {
1349 workarray[idx] = JSON.parse(workarray[idx]);
1350 }
1351
1352 return workarray;
1353 };
1354
1355 /**
1356 * Inflates a loki database from a serialized JSON string
1357 *
1358 * @param {string} serializedDb - a serialized loki database string
1359 * @param {object} options - apply or override collection level settings
1360 * @memberof Loki
1361 */
1362 Loki.prototype.loadJSON = function (serializedDb, options) {
1363 var dbObject;
1364 if (serializedDb.length === 0) {
1365 dbObject = {};
1366 } else {
1367 // using option defined in instantiated db not what was in serialized db
1368 switch (this.options.serializationMethod) {
1369 case "normal":
1370 case "pretty": dbObject = JSON.parse(serializedDb); break;
1371 case "destructured": dbObject = this.deserializeDestructured(serializedDb); break;
1372 default: dbObject = JSON.parse(serializedDb); break;
1373 }
1374 }
1375
1376 this.loadJSONObject(dbObject, options);
1377 };
1378
1379 /**
1380 * Inflates a loki database from a JS object
1381 *
1382 * @param {object} dbObject - a serialized loki database string
1383 * @param {object} options - apply or override collection level settings
1384 * @param {bool?} options.retainDirtyFlags - whether collection dirty flags will be preserved
1385 * @memberof Loki
1386 */
1387 Loki.prototype.loadJSONObject = function (dbObject, options) {
1388 var i = 0,
1389 len = dbObject.collections ? dbObject.collections.length : 0,
1390 coll,
1391 copyColl,
1392 clen,
1393 j,
1394 loader,
1395 collObj;
1396
1397 this.name = dbObject.name;
1398
1399 // restore database version
1400 this.databaseVersion = 1.0;
1401 if (dbObject.hasOwnProperty('databaseVersion')) {
1402 this.databaseVersion = dbObject.databaseVersion;
1403 }
1404
1405 this.collections = [];
1406
1407 function makeLoader(coll) {
1408 var collOptions = options[coll.name];
1409 var inflater;
1410
1411 if(collOptions.proto) {
1412 inflater = collOptions.inflate || Utils.copyProperties;
1413
1414 return function(data) {
1415 var collObj = new(collOptions.proto)();
1416 inflater(data, collObj);
1417 return collObj;
1418 };
1419 }
1420
1421 return collOptions.inflate;
1422 }
1423
1424 for (i; i < len; i += 1) {
1425 coll = dbObject.collections[i];
1426 copyColl = this.addCollection(coll.name);
1427
1428 copyColl.adaptiveBinaryIndices = coll.hasOwnProperty('adaptiveBinaryIndices')?(coll.adaptiveBinaryIndices === true): false;
1429 copyColl.transactional = coll.transactional;
1430 copyColl.asyncListeners = coll.asyncListeners;
1431 copyColl.disableChangesApi = coll.disableChangesApi;
1432 copyColl.cloneObjects = coll.cloneObjects;
1433 copyColl.cloneMethod = coll.cloneMethod || "parse-stringify";
1434 copyColl.autoupdate = coll.autoupdate;
1435 copyColl.changes = coll.changes;
1436
1437 if (options && options.retainDirtyFlags === true) {
1438 copyColl.dirty = coll.dirty;
1439 }
1440 else {
1441 copyColl.dirty = false;
1442 }
1443
1444 // load each element individually
1445 clen = coll.data.length;
1446 j = 0;
1447 if (options && options.hasOwnProperty(coll.name)) {
1448 loader = makeLoader(coll);
1449
1450 for (j; j < clen; j++) {
1451 collObj = loader(coll.data[j]);
1452 copyColl.data[j] = collObj;
1453 copyColl.addAutoUpdateObserver(collObj);
1454 }
1455 } else {
1456
1457 for (j; j < clen; j++) {
1458 copyColl.data[j] = coll.data[j];
1459 copyColl.addAutoUpdateObserver(copyColl.data[j]);
1460 }
1461 }
1462
1463 copyColl.maxId = (coll.data.length === 0) ? 0 : coll.maxId;
1464 copyColl.idIndex = coll.idIndex;
1465 if (typeof (coll.binaryIndices) !== 'undefined') {
1466 copyColl.binaryIndices = coll.binaryIndices;
1467 }
1468 if (typeof coll.transforms !== 'undefined') {
1469 copyColl.transforms = coll.transforms;
1470 }
1471
1472 copyColl.ensureId();
1473
1474 // regenerate unique indexes
1475 copyColl.uniqueNames = [];
1476 if (coll.hasOwnProperty("uniqueNames")) {
1477 copyColl.uniqueNames = coll.uniqueNames;
1478 for (j = 0; j < copyColl.uniqueNames.length; j++) {
1479 copyColl.ensureUniqueIndex(copyColl.uniqueNames[j]);
1480 }
1481 }
1482
1483 // in case they are loading a database created before we added dynamic views, handle undefined
1484 if (typeof (coll.DynamicViews) === 'undefined') continue;
1485
1486 // reinflate DynamicViews and attached Resultsets
1487 for (var idx = 0; idx < coll.DynamicViews.length; idx++) {
1488 var colldv = coll.DynamicViews[idx];
1489
1490 var dv = copyColl.addDynamicView(colldv.name, colldv.options);
1491 dv.resultdata = colldv.resultdata;
1492 dv.resultsdirty = colldv.resultsdirty;
1493 dv.filterPipeline = colldv.filterPipeline;
1494
1495 dv.sortCriteria = colldv.sortCriteria;
1496 dv.sortFunction = null;
1497
1498 dv.sortDirty = colldv.sortDirty;
1499 dv.resultset.filteredrows = colldv.resultset.filteredrows;
1500 dv.resultset.searchIsChained = colldv.resultset.searchIsChained;
1501 dv.resultset.filterInitialized = colldv.resultset.filterInitialized;
1502
1503 dv.rematerialize({
1504 removeWhereFilters: true
1505 });
1506 }
1507 }
1508 };
1509
1510 /**
1511 * Emits the close event. In autosave scenarios, if the database is dirty, this will save and disable timer.
1512 * Does not actually destroy the db.
1513 *
1514 * @param {function=} callback - (Optional) if supplied will be registered with close event before emitting.
1515 * @memberof Loki
1516 */
1517 Loki.prototype.close = function (callback) {
1518 // for autosave scenarios, we will let close perform final save (if dirty)
1519 // For web use, you might call from window.onbeforeunload to shutdown database, saving pending changes
1520 if (this.autosave) {
1521 this.autosaveDisable();
1522 if (this.autosaveDirty()) {
1523 this.saveDatabase(callback);
1524 callback = undefined;
1525 }
1526 }
1527
1528 if (callback) {
1529 this.on('close', callback);
1530 }
1531 this.emit('close');
1532 };
1533
1534 /**-------------------------+
1535 | Changes API |
1536 +--------------------------*/
1537
1538 /**
1539 * The Changes API enables the tracking the changes occurred in the collections since the beginning of the session,
1540 * so it's possible to create a differential dataset for synchronization purposes (possibly to a remote db)
1541 */
1542
1543 /**
1544 * (Changes API) : takes all the changes stored in each
1545 * collection and creates a single array for the entire database. If an array of names
1546 * of collections is passed then only the included collections will be tracked.
1547 *
1548 * @param {array=} optional array of collection names. No arg means all collections are processed.
1549 * @returns {array} array of changes
1550 * @see private method createChange() in Collection
1551 * @memberof Loki
1552 */
1553 Loki.prototype.generateChangesNotification = function (arrayOfCollectionNames) {
1554 function getCollName(coll) {
1555 return coll.name;
1556 }
1557 var changes = [],
1558 selectedCollections = arrayOfCollectionNames || this.collections.map(getCollName);
1559
1560 this.collections.forEach(function (coll) {
1561 if (selectedCollections.indexOf(getCollName(coll)) !== -1) {
1562 changes = changes.concat(coll.getChanges());
1563 }
1564 });
1565 return changes;
1566 };
1567
1568 /**
1569 * (Changes API) - stringify changes for network transmission
1570 * @returns {string} string representation of the changes
1571 * @memberof Loki
1572 */
1573 Loki.prototype.serializeChanges = function (collectionNamesArray) {
1574 return JSON.stringify(this.generateChangesNotification(collectionNamesArray));
1575 };
1576
1577 /**
1578 * (Changes API) : clears all the changes in all collections.
1579 * @memberof Loki
1580 */
1581 Loki.prototype.clearChanges = function () {
1582 this.collections.forEach(function (coll) {
1583 if (coll.flushChanges) {
1584 coll.flushChanges();
1585 }
1586 });
1587 };
1588
1589 /*------------------+
1590 | PERSISTENCE |
1591 -------------------*/
1592
1593 /** there are two build in persistence adapters for internal use
1594 * fs for use in Nodejs type environments
1595 * localStorage for use in browser environment
1596 * defined as helper classes here so its easy and clean to use
1597 */
1598
1599 /**
1600 * In in-memory persistence adapter for an in-memory database.
1601 * This simple 'key/value' adapter is intended for unit testing and diagnostics.
1602 *
1603 * @constructor LokiMemoryAdapter
1604 */
1605 function LokiMemoryAdapter() {
1606 this.hashStore = {};
1607 }
1608
1609 /**
1610 * Loads a serialized database from its in-memory store.
1611 * (Loki persistence adapter interface function)
1612 *
1613 * @param {string} dbname - name of the database (filename/keyname)
1614 * @param {function} callback - adapter callback to return load result to caller
1615 * @memberof LokiMemoryAdapter
1616 */
1617 LokiMemoryAdapter.prototype.loadDatabase = function (dbname, callback) {
1618 if (this.hashStore.hasOwnProperty(dbname)) {
1619 callback(this.hashStore[dbname].value);
1620 }
1621 else {
1622 callback (new Error("unable to load database, " + dbname + " was not found in memory adapter"));
1623 }
1624 };
1625
1626 /**
1627 * Saves a serialized database to its in-memory store.
1628 * (Loki persistence adapter interface function)
1629 *
1630 * @param {string} dbname - name of the database (filename/keyname)
1631 * @param {function} callback - adapter callback to return load result to caller
1632 * @memberof LokiMemoryAdapter
1633 */
1634 LokiMemoryAdapter.prototype.saveDatabase = function (dbname, dbstring, callback) {
1635 var saveCount = (this.hashStore.hasOwnProperty(dbname)?this.hashStore[dbname].savecount:0);
1636
1637 this.hashStore[dbname] = {
1638 savecount: saveCount+1,
1639 lastsave: new Date(),
1640 value: dbstring
1641 };
1642
1643 callback();
1644 };
1645
1646 /**
1647 * An adapter for adapters. Converts a non reference mode adapter into a reference mode adapter
1648 * which can perform destructuring and partioning. Each collection will be stored in its own key/save and
1649 * only dirty collections will be saved. If you turn on paging with default page size of 25megs and save
1650 * a 75 meg collection it should use up roughly 3 save slots (key/value pairs sent to inner adapter).
1651 * A dirty collection that spans three pages will save all three pages again
1652 * Paging mode was added mainly because Chrome has issues saving 'too large' of a string within a
1653 * single indexeddb row. If a single document update causes the collection to be flagged as dirty, all
1654 * of that collection's pages will be written on next save.
1655 *
1656 * @param {object} adapter - reference to a 'non-reference' mode loki adapter instance.
1657 * @param {object=} options - configuration options for partitioning and paging
1658 * @param {bool} options.paging - (default: false) set to true to enable paging collection data.
1659 * @param {int} options.pageSize - (default : 25MB) you can use this to limit size of strings passed to inner adapter.
1660 * @param {string} options.delimiter - allows you to override the default delimeter
1661 * @constructor LokiPartitioningAdapter
1662 */
1663 function LokiPartitioningAdapter(adapter, options) {
1664 this.mode = "reference";
1665 this.adapter = null;
1666 this.options = options || {};
1667 this.dbref = null;
1668 this.dbname = "";
1669 this.pageIterator = {};
1670
1671 // verify user passed an appropriate adapter
1672 if (adapter) {
1673 if (adapter.mode === "reference") {
1674 throw new Error("LokiPartitioningAdapter cannot be instantiated with a reference mode adapter");
1675 }
1676 else {
1677 this.adapter = adapter;
1678 }
1679 }
1680 else {
1681 throw new Error("LokiPartitioningAdapter requires a (non-reference mode) adapter on construction");
1682 }
1683
1684 // set collection paging defaults
1685 if (!this.options.hasOwnProperty("paging")) {
1686 this.options.paging = false;
1687 }
1688
1689 // default to page size of 25 megs (can be up to your largest serialized object size larger than this)
1690 if (!this.options.hasOwnProperty("pageSize")) {
1691 this.options.pageSize = 25*1024*1024;
1692 }
1693
1694 if (!this.options.hasOwnProperty("delimiter")) {
1695 this.options.delimiter = '$<\n';
1696 }
1697 }
1698
1699 /**
1700 * Loads a database which was partitioned into several key/value saves.
1701 * (Loki persistence adapter interface function)
1702 *
1703 * @param {string} dbname - name of the database (filename/keyname)
1704 * @param {function} callback - adapter callback to return load result to caller
1705 * @memberof LokiPartitioningAdapter
1706 */
1707 LokiPartitioningAdapter.prototype.loadDatabase = function (dbname, callback) {
1708 var self=this;
1709 this.dbname = dbname;
1710 this.dbref = new Loki(dbname);
1711
1712 // load the db container (without data)
1713 this.adapter.loadDatabase(dbname, function(result) {
1714 if (typeof result !== "string") {
1715 callback(new Error("LokiPartitioningAdapter received an unexpected response from inner adapter loadDatabase()"));
1716 }
1717
1718 // I will want to use loki destructuring helper methods so i will inflate into typed instance
1719 var db = JSON.parse(result);
1720 self.dbref.loadJSONObject(db);
1721 db = null;
1722
1723 var clen = self.dbref.collections.length;
1724
1725 if (self.dbref.collections.length === 0) {
1726 callback(self.dbref);
1727 return;
1728 }
1729
1730 self.pageIterator = {
1731 collection: 0,
1732 pageIndex: 0
1733 };
1734
1735 self.loadNextPartition(0, function() {
1736 callback(self.dbref);
1737 });
1738 });
1739 };
1740
1741 /**
1742 * Used to sequentially load each collection partition, one at a time.
1743 *
1744 * @param {int} partition - ordinal collection position to load next
1745 * @param {function} callback - adapter callback to return load result to caller
1746 */
1747 LokiPartitioningAdapter.prototype.loadNextPartition = function(partition, callback) {
1748 var keyname = this.dbname + "." + partition;
1749 var self=this;
1750
1751 if (this.options.paging === true) {
1752 this.pageIterator.pageIndex = 0;
1753 this.loadNextPage(callback);
1754 return;
1755 }
1756
1757 this.adapter.loadDatabase(keyname, function(result) {
1758 var data = self.dbref.deserializeCollection(result, { delimited: true, collectionIndex: partition });
1759 self.dbref.collections[partition].data = data;
1760
1761 if (++partition < self.dbref.collections.length) {
1762 self.loadNextPartition(partition, callback);
1763 }
1764 else {
1765 callback();
1766 }
1767 });
1768 };
1769
1770 /**
1771 * Used to sequentially load the next page of collection partition, one at a time.
1772 *
1773 * @param {function} callback - adapter callback to return load result to caller
1774 */
1775 LokiPartitioningAdapter.prototype.loadNextPage = function(callback) {
1776 // calculate name for next saved page in sequence
1777 var keyname = this.dbname + "." + this.pageIterator.collection + "." + this.pageIterator.pageIndex;
1778 var self=this;
1779
1780 // load whatever page is next in sequence
1781 this.adapter.loadDatabase(keyname, function(result) {
1782 var data = result.split(self.options.delimiter);
1783 result = ""; // free up memory now that we have split it into array
1784 var dlen = data.length;
1785 var idx;
1786
1787 // detect if last page by presence of final empty string element and remove it if so
1788 var isLastPage = (data[dlen-1] === "");
1789 if (isLastPage) {
1790 data.pop();
1791 dlen = data.length;
1792 // empty collections are just a delimiter meaning two blank items
1793 if (data[dlen-1] === "" && dlen === 1) {
1794 data.pop();
1795 dlen = data.length;
1796 }
1797 }
1798
1799 // convert stringified array elements to object instances and push to collection data
1800 for(idx=0; idx < dlen; idx++) {
1801 self.dbref.collections[self.pageIterator.collection].data.push(JSON.parse(data[idx]));
1802 data[idx] = null;
1803 }
1804 data = [];
1805
1806 // if last page, we are done with this partition
1807 if (isLastPage) {
1808
1809 // if there are more partitions, kick off next partition load
1810 if (++self.pageIterator.collection < self.dbref.collections.length) {
1811 self.loadNextPartition(self.pageIterator.collection, callback);
1812 }
1813 else {
1814 callback();
1815 }
1816 }
1817 else {
1818 self.pageIterator.pageIndex++;
1819 self.loadNextPage(callback);
1820 }
1821 });
1822 };
1823
1824 /**
1825 * Saves a database by partioning into separate key/value saves.
1826 * (Loki 'reference mode' persistence adapter interface function)
1827 *
1828 * @param {string} dbname - name of the database (filename/keyname)
1829 * @param {object} dbref - reference to database which we will partition and save.
1830 * @param {function} callback - adapter callback to return load result to caller
1831 *
1832 * @memberof LokiPartitioningAdapter
1833 */
1834 LokiPartitioningAdapter.prototype.exportDatabase = function(dbname, dbref, callback) {
1835 var self=this;
1836 var idx, clen = dbref.collections.length;
1837
1838 this.dbref = dbref;
1839 this.dbname = dbname;
1840
1841 // queue up dirty partitions to be saved
1842 this.dirtyPartitions = [-1];
1843 for(idx=0; idx<clen; idx++) {
1844 if (dbref.collections[idx].dirty) {
1845 this.dirtyPartitions.push(idx);
1846 }
1847 }
1848
1849 this.saveNextPartition(function(err) {
1850 callback(err);
1851 });
1852 };
1853
1854 /**
1855 * Helper method used internally to save each dirty collection, one at a time.
1856 *
1857 * @param {function} callback - adapter callback to return load result to caller
1858 */
1859 LokiPartitioningAdapter.prototype.saveNextPartition = function(callback) {
1860 var self=this;
1861 var partition = this.dirtyPartitions.shift();
1862 var keyname = this.dbname + ((partition===-1)?"":("." + partition));
1863
1864 // if we are doing paging and this is collection partition
1865 if (this.options.paging && partition !== -1) {
1866 this.pageIterator = {
1867 collection: partition,
1868 docIndex: 0,
1869 pageIndex: 0
1870 };
1871
1872 // since saveNextPage recursively calls itself until done, our callback means this whole paged partition is finished
1873 this.saveNextPage(function(err) {
1874 if (self.dirtyPartitions.length === 0) {
1875 callback(err);
1876 }
1877 else {
1878 self.saveNextPartition(callback);
1879 }
1880 });
1881 return;
1882 }
1883
1884 // otherwise this is 'non-paged' partioning...
1885 var result = this.dbref.serializeDestructured({
1886 partitioned : true,
1887 delimited: true,
1888 partition: partition
1889 });
1890
1891 this.adapter.saveDatabase(keyname, result, function(err) {
1892 if (err) {
1893 callback(err);
1894 return;
1895 }
1896
1897 if (self.dirtyPartitions.length === 0) {
1898 callback(null);
1899 }
1900 else {
1901 self.saveNextPartition(callback);
1902 }
1903 });
1904 };
1905
1906 /**
1907 * Helper method used internally to generate and save the next page of the current (dirty) partition.
1908 *
1909 * @param {function} callback - adapter callback to return load result to caller
1910 */
1911 LokiPartitioningAdapter.prototype.saveNextPage = function(callback) {
1912 var self=this;
1913 var coll = this.dbref.collections[this.pageIterator.collection];
1914 var keyname = this.dbname + "." + this.pageIterator.collection + "." + this.pageIterator.pageIndex;
1915 var pageLen=0,
1916 cdlen = coll.data.length,
1917 delimlen = this.options.delimiter.length;
1918 var serializedObject = "",
1919 pageBuilder = "";
1920 var doneWithPartition=false,
1921 doneWithPage=false;
1922
1923 var pageSaveCallback = function(err) {
1924 pageBuilder = "";
1925
1926 if (err) {
1927 callback(err);
1928 }
1929
1930 // update meta properties then continue process by invoking callback
1931 if (doneWithPartition) {
1932 callback(null);
1933 }
1934 else {
1935 self.pageIterator.pageIndex++;
1936 self.saveNextPage(callback);
1937 }
1938 };
1939
1940 if (coll.data.length === 0) {
1941 doneWithPartition = true;
1942 }
1943
1944 while (true) {
1945 if (!doneWithPartition) {
1946 // serialize object
1947 serializedObject = JSON.stringify(coll.data[this.pageIterator.docIndex]);
1948 pageBuilder += serializedObject;
1949 pageLen += serializedObject.length;
1950
1951 // if no more documents in collection to add, we are done with partition
1952 if (++this.pageIterator.docIndex >= cdlen) doneWithPartition = true;
1953 }
1954 // if our current page is bigger than defined pageSize, we are done with page
1955 if (pageLen >= this.options.pageSize) doneWithPage = true;
1956
1957 // if not done with current page, need delimiter before next item
1958 // if done with partition we also want a delmiter to indicate 'end of pages' final empty row
1959 if (!doneWithPage || doneWithPartition) {
1960 pageBuilder += this.options.delimiter;
1961 pageLen += delimlen;
1962 }
1963
1964 // if we are done with page save it and pass off to next recursive call or callback
1965 if (doneWithPartition || doneWithPage) {
1966 this.adapter.saveDatabase(keyname, pageBuilder, pageSaveCallback);
1967 return;
1968 }
1969 }
1970 };
1971
1972 /**
1973 * A loki persistence adapter which persists using node fs module
1974 * @constructor LokiFsAdapter
1975 */
1976 function LokiFsAdapter() {
1977 this.fs = require('fs');
1978 }
1979
1980 /**
1981 * loadDatabase() - Load data from file, will throw an error if the file does not exist
1982 * @param {string} dbname - the filename of the database to load
1983 * @param {function} callback - the callback to handle the result
1984 * @memberof LokiFsAdapter
1985 */
1986 LokiFsAdapter.prototype.loadDatabase = function loadDatabase(dbname, callback) {
1987 var self = this;
1988
1989 this.fs.stat(dbname, function (err, stats) {
1990 if (!err && stats.isFile()) {
1991 self.fs.readFile(dbname, {
1992 encoding: 'utf8'
1993 }, function readFileCallback(err, data) {
1994 if (err) {
1995 callback(new Error(err));
1996 } else {
1997 callback(data);
1998 }
1999 });
2000 }
2001 else {
2002 callback(null);
2003 }
2004 });
2005 };
2006
2007 /**
2008 * saveDatabase() - save data to file, will throw an error if the file can't be saved
2009 * might want to expand this to avoid dataloss on partial save
2010 * @param {string} dbname - the filename of the database to load
2011 * @param {function} callback - the callback to handle the result
2012 * @memberof LokiFsAdapter
2013 */
2014 LokiFsAdapter.prototype.saveDatabase = function saveDatabase(dbname, dbstring, callback) {
2015 var self = this;
2016 var tmpdbname = dbname + '~';
2017 this.fs.writeFile(tmpdbname, dbstring, function writeFileCallback(err) {
2018 if (err) {
2019 callback(new Error(err));
2020 } else {
2021 self.fs.rename(tmpdbname,dbname,callback);
2022 }
2023 });
2024 };
2025
2026 /**
2027 * deleteDatabase() - delete the database file, will throw an error if the
2028 * file can't be deleted
2029 * @param {string} dbname - the filename of the database to delete
2030 * @param {function} callback - the callback to handle the result
2031 * @memberof LokiFsAdapter
2032 */
2033 LokiFsAdapter.prototype.deleteDatabase = function deleteDatabase(dbname, callback) {
2034 this.fs.unlink(dbname, function deleteDatabaseCallback(err) {
2035 if (err) {
2036 callback(new Error(err));
2037 } else {
2038 callback();
2039 }
2040 });
2041 };
2042
2043
2044 /**
2045 * A loki persistence adapter which persists to web browser's local storage object
2046 * @constructor LokiLocalStorageAdapter
2047 */
2048 function LokiLocalStorageAdapter() {}
2049
2050 /**
2051 * loadDatabase() - Load data from localstorage
2052 * @param {string} dbname - the name of the database to load
2053 * @param {function} callback - the callback to handle the result
2054 * @memberof LokiLocalStorageAdapter
2055 */
2056 LokiLocalStorageAdapter.prototype.loadDatabase = function loadDatabase(dbname, callback) {
2057 if (localStorageAvailable()) {
2058 callback(localStorage.getItem(dbname));
2059 } else {
2060 callback(new Error('localStorage is not available'));
2061 }
2062 };
2063
2064 /**
2065 * saveDatabase() - save data to localstorage, will throw an error if the file can't be saved
2066 * might want to expand this to avoid dataloss on partial save
2067 * @param {string} dbname - the filename of the database to load
2068 * @param {function} callback - the callback to handle the result
2069 * @memberof LokiLocalStorageAdapter
2070 */
2071 LokiLocalStorageAdapter.prototype.saveDatabase = function saveDatabase(dbname, dbstring, callback) {
2072 if (localStorageAvailable()) {
2073 localStorage.setItem(dbname, dbstring);
2074 callback(null);
2075 } else {
2076 callback(new Error('localStorage is not available'));
2077 }
2078 };
2079
2080 /**
2081 * deleteDatabase() - delete the database from localstorage, will throw an error if it
2082 * can't be deleted
2083 * @param {string} dbname - the filename of the database to delete
2084 * @param {function} callback - the callback to handle the result
2085 * @memberof LokiLocalStorageAdapter
2086 */
2087 LokiLocalStorageAdapter.prototype.deleteDatabase = function deleteDatabase(dbname, callback) {
2088 if (localStorageAvailable()) {
2089 localStorage.removeItem(dbname);
2090 callback(null);
2091 } else {
2092 callback(new Error('localStorage is not available'));
2093 }
2094 };
2095
2096 /**
2097 * Handles loading from file system, local storage, or adapter (indexeddb)
2098 * This method utilizes loki configuration options (if provided) to determine which
2099 * persistence method to use, or environment detection (if configuration was not provided).
2100 *
2101 * @param {object} options - not currently used (remove or allow overrides?)
2102 * @param {function=} callback - (Optional) user supplied async callback / error handler
2103 * @memberof Loki
2104 */
2105 Loki.prototype.loadDatabase = function (options, callback) {
2106 var cFun = callback || function (err, data) {
2107 if (err) {
2108 throw err;
2109 }
2110 },
2111 self = this;
2112
2113 // the persistenceAdapter should be present if all is ok, but check to be sure.
2114 if (this.persistenceAdapter !== null) {
2115
2116 this.persistenceAdapter.loadDatabase(this.filename, function loadDatabaseCallback(dbString) {
2117 if (typeof (dbString) === 'string') {
2118 var parseSuccess = false;
2119 try {
2120 self.loadJSON(dbString, options || {});
2121 parseSuccess = true;
2122 } catch (err) {
2123 cFun(err);
2124 }
2125 if (parseSuccess) {
2126 cFun(null);
2127 self.emit('loaded', 'database ' + self.filename + ' loaded');
2128 }
2129 } else {
2130 // if adapter has returned an js object (other than null or error) attempt to load from JSON object
2131 if (typeof (dbString) === "object" && dbString !== null && !(dbString instanceof Error)) {
2132 self.loadJSONObject(dbString, options || {});
2133 cFun(null); // return null on success
2134 self.emit('loaded', 'database ' + self.filename + ' loaded');
2135 } else {
2136 // error from adapter (either null or instance of error), pass on to 'user' callback
2137 cFun(dbString);
2138 }
2139 }
2140 });
2141
2142 } else {
2143 cFun(new Error('persistenceAdapter not configured'));
2144 }
2145 };
2146
2147 /**
2148 * Handles saving to file system, local storage, or adapter (indexeddb)
2149 * This method utilizes loki configuration options (if provided) to determine which
2150 * persistence method to use, or environment detection (if configuration was not provided).
2151 *
2152 * @param {function=} callback - (Optional) user supplied async callback / error handler
2153 * @memberof Loki
2154 */
2155 Loki.prototype.saveDatabase = function (callback) {
2156 var cFun = callback || function (err) {
2157 if (err) {
2158 throw err;
2159 }
2160 return;
2161 },
2162 self = this;
2163
2164 // the persistenceAdapter should be present if all is ok, but check to be sure.
2165 if (this.persistenceAdapter !== null) {
2166 // check if the adapter is requesting (and supports) a 'reference' mode export
2167 if (this.persistenceAdapter.mode === "reference" && typeof this.persistenceAdapter.exportDatabase === "function") {
2168 // filename may seem redundant but loadDatabase will need to expect this same filename
2169 this.persistenceAdapter.exportDatabase(this.filename, this.copy({removeNonSerializable:true}), function exportDatabaseCallback(err) {
2170 self.autosaveClearFlags();
2171 cFun(err);
2172 });
2173 }
2174 // otherwise just pass the serialized database to adapter
2175 else {
2176 this.persistenceAdapter.saveDatabase(this.filename, self.serialize(), function saveDatabasecallback(err) {
2177 self.autosaveClearFlags();
2178 cFun(err);
2179 });
2180 }
2181 } else {
2182 cFun(new Error('persistenceAdapter not configured'));
2183 }
2184 };
2185
2186 // alias
2187 Loki.prototype.save = Loki.prototype.saveDatabase;
2188
2189 /**
2190 * Handles deleting a database from file system, local
2191 * storage, or adapter (indexeddb)
2192 * This method utilizes loki configuration options (if provided) to determine which
2193 * persistence method to use, or environment detection (if configuration was not provided).
2194 *
2195 * @param {object} options - not currently used (remove or allow overrides?)
2196 * @param {function=} callback - (Optional) user supplied async callback / error handler
2197 * @memberof Loki
2198 */
2199 Loki.prototype.deleteDatabase = function (options, callback) {
2200 var cFun = callback || function (err, data) {
2201 if (err) {
2202 throw err;
2203 }
2204 };
2205
2206 // the persistenceAdapter should be present if all is ok, but check to be sure.
2207 if (this.persistenceAdapter !== null) {
2208 this.persistenceAdapter.deleteDatabase(this.filename, function deleteDatabaseCallback(err) {
2209 cFun(err);
2210 });
2211 } else {
2212 cFun(new Error('persistenceAdapter not configured'));
2213 }
2214 };
2215
2216 /**
2217 * autosaveDirty - check whether any collections are 'dirty' meaning we need to save (entire) database
2218 *
2219 * @returns {boolean} - true if database has changed since last autosave, false if not.
2220 */
2221 Loki.prototype.autosaveDirty = function () {
2222 for (var idx = 0; idx < this.collections.length; idx++) {
2223 if (this.collections[idx].dirty) {
2224 return true;
2225 }
2226 }
2227
2228 return false;
2229 };
2230
2231 /**
2232 * autosaveClearFlags - resets dirty flags on all collections.
2233 * Called from saveDatabase() after db is saved.
2234 *
2235 */
2236 Loki.prototype.autosaveClearFlags = function () {
2237 for (var idx = 0; idx < this.collections.length; idx++) {
2238 this.collections[idx].dirty = false;
2239 }
2240 };
2241
2242 /**
2243 * autosaveEnable - begin a javascript interval to periodically save the database.
2244 *
2245 * @param {object} options - not currently used (remove or allow overrides?)
2246 * @param {function=} callback - (Optional) user supplied async callback
2247 */
2248 Loki.prototype.autosaveEnable = function (options, callback) {
2249 this.autosave = true;
2250
2251 var delay = 5000,
2252 self = this;
2253
2254 if (typeof (this.autosaveInterval) !== 'undefined' && this.autosaveInterval !== null) {
2255 delay = this.autosaveInterval;
2256 }
2257
2258 this.autosaveHandle = setInterval(function autosaveHandleInterval() {
2259 // use of dirty flag will need to be hierarchical since mods are done at collection level with no visibility of 'db'
2260 // so next step will be to implement collection level dirty flags set on insert/update/remove
2261 // along with loki level isdirty() function which iterates all collections to see if any are dirty
2262
2263 if (self.autosaveDirty()) {
2264 self.saveDatabase(callback);
2265 }
2266 }, delay);
2267 };
2268
2269 /**
2270 * autosaveDisable - stop the autosave interval timer.
2271 *
2272 */
2273 Loki.prototype.autosaveDisable = function () {
2274 if (typeof (this.autosaveHandle) !== 'undefined' && this.autosaveHandle !== null) {
2275 clearInterval(this.autosaveHandle);
2276 this.autosaveHandle = null;
2277 }
2278 };
2279
2280
2281 /**
2282 * Resultset class allowing chainable queries. Intended to be instanced internally.
2283 * Collection.find(), Collection.where(), and Collection.chain() instantiate this.
2284 *
2285 * @example
2286 * mycollection.chain()
2287 * .find({ 'doors' : 4 })
2288 * .where(function(obj) { return obj.name === 'Toyota' })
2289 * .data();
2290 *
2291 * @constructor Resultset
2292 * @param {Collection} collection - The collection which this Resultset will query against.
2293 * @param {Object=} options - Object containing one or more options.
2294 * @param {string} options.queryObj - Optional mongo-style query object to initialize resultset with.
2295 * @param {function} options.queryFunc - Optional javascript filter function to initialize resultset with.
2296 * @param {bool} options.firstOnly - Optional boolean used by collection.findOne().
2297 */
2298 function Resultset(collection, options) {
2299 options = options || {};
2300
2301 options.queryObj = options.queryObj || null;
2302 options.queryFunc = options.queryFunc || null;
2303 options.firstOnly = options.firstOnly || false;
2304
2305 // retain reference to collection we are querying against
2306 this.collection = collection;
2307
2308 // if chain() instantiates with null queryObj and queryFunc, so we will keep flag for later
2309 this.searchIsChained = (!options.queryObj && !options.queryFunc);
2310 this.filteredrows = [];
2311 this.filterInitialized = false;
2312
2313 // if user supplied initial queryObj or queryFunc, apply it
2314 if (typeof (options.queryObj) !== "undefined" && options.queryObj !== null) {
2315 return this.find(options.queryObj, options.firstOnly);
2316 }
2317 if (typeof (options.queryFunc) !== "undefined" && options.queryFunc !== null) {
2318 return this.where(options.queryFunc);
2319 }
2320
2321 // otherwise return unfiltered Resultset for future filtering
2322 return this;
2323 }
2324
2325 /**
2326 * reset() - Reset the resultset to its initial state.
2327 *
2328 * @returns {Resultset} Reference to this resultset, for future chain operations.
2329 */
2330 Resultset.prototype.reset = function () {
2331 if (this.filteredrows.length > 0) {
2332 this.filteredrows = [];
2333 }
2334 this.filterInitialized = false;
2335 return this;
2336 };
2337
2338 /**
2339 * toJSON() - Override of toJSON to avoid circular references
2340 *
2341 */
2342 Resultset.prototype.toJSON = function () {
2343 var copy = this.copy();
2344 copy.collection = null;
2345 return copy;
2346 };
2347
2348 /**
2349 * Allows you to limit the number of documents passed to next chain operation.
2350 * A resultset copy() is made to avoid altering original resultset.
2351 *
2352 * @param {int} qty - The number of documents to return.
2353 * @returns {Resultset} Returns a copy of the resultset, limited by qty, for subsequent chain ops.
2354 * @memberof Resultset
2355 */
2356 Resultset.prototype.limit = function (qty) {
2357 // if this is chained resultset with no filters applied, we need to populate filteredrows first
2358 if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length === 0) {
2359 this.filteredrows = this.collection.prepareFullDocIndex();
2360 }
2361
2362 var rscopy = new Resultset(this.collection);
2363 rscopy.filteredrows = this.filteredrows.slice(0, qty);
2364 rscopy.filterInitialized = true;
2365 return rscopy;
2366 };
2367
2368 /**
2369 * Used for skipping 'pos' number of documents in the resultset.
2370 *
2371 * @param {int} pos - Number of documents to skip; all preceding documents are filtered out.
2372 * @returns {Resultset} Returns a copy of the resultset, containing docs starting at 'pos' for subsequent chain ops.
2373 * @memberof Resultset
2374 */
2375 Resultset.prototype.offset = function (pos) {
2376 // if this is chained resultset with no filters applied, we need to populate filteredrows first
2377 if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length === 0) {
2378 this.filteredrows = this.collection.prepareFullDocIndex();
2379 }
2380
2381 var rscopy = new Resultset(this.collection);
2382 rscopy.filteredrows = this.filteredrows.slice(pos);
2383 rscopy.filterInitialized = true;
2384 return rscopy;
2385 };
2386
2387 /**
2388 * copy() - To support reuse of resultset in branched query situations.
2389 *
2390 * @returns {Resultset} Returns a copy of the resultset (set) but the underlying document references will be the same.
2391 * @memberof Resultset
2392 */
2393 Resultset.prototype.copy = function () {
2394 var result = new Resultset(this.collection);
2395
2396 if (this.filteredrows.length > 0) {
2397 result.filteredrows = this.filteredrows.slice();
2398 }
2399 result.filterInitialized = this.filterInitialized;
2400
2401 return result;
2402 };
2403
2404 /**
2405 * Alias of copy()
2406 * @memberof Resultset
2407 */
2408 Resultset.prototype.branch = Resultset.prototype.copy;
2409
2410 /**
2411 * transform() - executes a named collection transform or raw array of transform steps against the resultset.
2412 *
2413 * @param transform {(string|array)} - name of collection transform or raw transform array
2414 * @param parameters {object=} - (Optional) object property hash of parameters, if the transform requires them.
2415 * @returns {Resultset} either (this) resultset or a clone of of this resultset (depending on steps)
2416 * @memberof Resultset
2417 */
2418 Resultset.prototype.transform = function (transform, parameters) {
2419 var idx,
2420 step,
2421 rs = this;
2422
2423 // if transform is name, then do lookup first
2424 if (typeof transform === 'string') {
2425 if (this.collection.transforms.hasOwnProperty(transform)) {
2426 transform = this.collection.transforms[transform];
2427 }
2428 }
2429
2430 // either they passed in raw transform array or we looked it up, so process
2431 if (typeof transform !== 'object' || !Array.isArray(transform)) {
2432 throw new Error("Invalid transform");
2433 }
2434
2435 if (typeof parameters !== 'undefined') {
2436 transform = Utils.resolveTransformParams(transform, parameters);
2437 }
2438
2439 for (idx = 0; idx < transform.length; idx++) {
2440 step = transform[idx];
2441
2442 switch (step.type) {
2443 case "find":
2444 rs.find(step.value);
2445 break;
2446 case "where":
2447 rs.where(step.value);
2448 break;
2449 case "simplesort":
2450 rs.simplesort(step.property, step.desc);
2451 break;
2452 case "compoundsort":
2453 rs.compoundsort(step.value);
2454 break;
2455 case "sort":
2456 rs.sort(step.value);
2457 break;
2458 case "limit":
2459 rs = rs.limit(step.value);
2460 break; // limit makes copy so update reference
2461 case "offset":
2462 rs = rs.offset(step.value);
2463 break; // offset makes copy so update reference
2464 case "map":
2465 rs = rs.map(step.value);
2466 break;
2467 case "eqJoin":
2468 rs = rs.eqJoin(step.joinData, step.leftJoinKey, step.rightJoinKey, step.mapFun);
2469 break;
2470 // following cases break chain by returning array data so make any of these last in transform steps
2471 case "mapReduce":
2472 rs = rs.mapReduce(step.mapFunction, step.reduceFunction);
2473 break;
2474 // following cases update documents in current filtered resultset (use carefully)
2475 case "update":
2476 rs.update(step.value);
2477 break;
2478 case "remove":
2479 rs.remove();
2480 break;
2481 default:
2482 break;
2483 }
2484 }
2485
2486 return rs;
2487 };
2488
2489 /**
2490 * User supplied compare function is provided two documents to compare. (chainable)
2491 * @example
2492 * rslt.sort(function(obj1, obj2) {
2493 * if (obj1.name === obj2.name) return 0;
2494 * if (obj1.name > obj2.name) return 1;
2495 * if (obj1.name < obj2.name) return -1;
2496 * });
2497 *
2498 * @param {function} comparefun - A javascript compare function used for sorting.
2499 * @returns {Resultset} Reference to this resultset, sorted, for future chain operations.
2500 * @memberof Resultset
2501 */
2502 Resultset.prototype.sort = function (comparefun) {
2503 // if this is chained resultset with no filters applied, just we need to populate filteredrows first
2504 if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length === 0) {
2505 this.filteredrows = this.collection.prepareFullDocIndex();
2506 }
2507
2508 var wrappedComparer =
2509 (function (userComparer, data) {
2510 return function (a, b) {
2511 return userComparer(data[a], data[b]);
2512 };
2513 })(comparefun, this.collection.data);
2514
2515 this.filteredrows.sort(wrappedComparer);
2516
2517 return this;
2518 };
2519
2520 /**
2521 * Simpler, loose evaluation for user to sort based on a property name. (chainable).
2522 * Sorting based on the same lt/gt helper functions used for binary indices.
2523 *
2524 * @param {string} propname - name of property to sort by.
2525 * @param {bool=} isdesc - (Optional) If true, the property will be sorted in descending order
2526 * @returns {Resultset} Reference to this resultset, sorted, for future chain operations.
2527 * @memberof Resultset
2528 */
2529 Resultset.prototype.simplesort = function (propname, isdesc) {
2530 // if this is chained resultset with no filters applied, just we need to populate filteredrows first
2531 if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length === 0) {
2532 this.filteredrows = this.collection.prepareFullDocIndex();
2533 }
2534
2535 if (typeof (isdesc) === 'undefined') {
2536 isdesc = false;
2537 }
2538
2539 var wrappedComparer =
2540 (function (prop, desc, data) {
2541 return function (a, b) {
2542 return sortHelper(data[a][prop], data[b][prop], desc);
2543 };
2544 })(propname, isdesc, this.collection.data);
2545
2546 this.filteredrows.sort(wrappedComparer);
2547
2548 return this;
2549 };
2550
2551 /**
2552 * Allows sorting a resultset based on multiple columns.
2553 * @example
2554 * // to sort by age and then name (both ascending)
2555 * rs.compoundsort(['age', 'name']);
2556 * // to sort by age (ascending) and then by name (descending)
2557 * rs.compoundsort(['age', ['name', true]);
2558 *
2559 * @param {array} properties - array of property names or subarray of [propertyname, isdesc] used evaluate sort order
2560 * @returns {Resultset} Reference to this resultset, sorted, for future chain operations.
2561 * @memberof Resultset
2562 */
2563 Resultset.prototype.compoundsort = function (properties) {
2564 if (properties.length === 0) {
2565 throw new Error("Invalid call to compoundsort, need at least one property");
2566 }
2567
2568 var prop;
2569 if (properties.length === 1) {
2570 prop = properties[0];
2571 if (Array.isArray(prop)) {
2572 return this.simplesort(prop[0], prop[1]);
2573 }
2574 return this.simplesort(prop, false);
2575 }
2576
2577 // unify the structure of 'properties' to avoid checking it repeatedly while sorting
2578 for (var i = 0, len = properties.length; i < len; i += 1) {
2579 prop = properties[i];
2580 if (!Array.isArray(prop)) {
2581 properties[i] = [prop, false];
2582 }
2583 }
2584
2585 // if this is chained resultset with no filters applied, just we need to populate filteredrows first
2586 if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length === 0) {
2587 this.filteredrows = this.collection.prepareFullDocIndex();
2588 }
2589
2590 var wrappedComparer =
2591 (function (props, data) {
2592 return function (a, b) {
2593 return compoundeval(props, data[a], data[b]);
2594 };
2595 })(properties, this.collection.data);
2596
2597 this.filteredrows.sort(wrappedComparer);
2598
2599 return this;
2600 };
2601
2602 /**
2603 * findOr() - oversee the operation of OR'ed query expressions.
2604 * OR'ed expression evaluation runs each expression individually against the full collection,
2605 * and finally does a set OR on each expression's results.
2606 * Each evaluation can utilize a binary index to prevent multiple linear array scans.
2607 *
2608 * @param {array} expressionArray - array of expressions
2609 * @returns {Resultset} this resultset for further chain ops.
2610 */
2611 Resultset.prototype.findOr = function (expressionArray) {
2612 var fr = null,
2613 fri = 0,
2614 frlen = 0,
2615 docset = [],
2616 idxset = [],
2617 idx = 0,
2618 origCount = this.count();
2619
2620 // If filter is already initialized, then we query against only those items already in filter.
2621 // This means no index utilization for fields, so hopefully its filtered to a smallish filteredrows.
2622 for (var ei = 0, elen = expressionArray.length; ei < elen; ei++) {
2623 // we need to branch existing query to run each filter separately and combine results
2624 fr = this.branch().find(expressionArray[ei]).filteredrows;
2625 frlen = fr.length;
2626 // if the find operation did not reduce the initial set, then the initial set is the actual result
2627 if (frlen === origCount) {
2628 return this;
2629 }
2630
2631 // add any document 'hits'
2632 for (fri = 0; fri < frlen; fri++) {
2633 idx = fr[fri];
2634 if (idxset[idx] === undefined) {
2635 idxset[idx] = true;
2636 docset.push(idx);
2637 }
2638 }
2639 }
2640
2641 this.filteredrows = docset;
2642 this.filterInitialized = true;
2643
2644 return this;
2645 };
2646 Resultset.prototype.$or = Resultset.prototype.findOr;
2647
2648 /**
2649 * findAnd() - oversee the operation of AND'ed query expressions.
2650 * AND'ed expression evaluation runs each expression progressively against the full collection,
2651 * internally utilizing existing chained resultset functionality.
2652 * Only the first filter can utilize a binary index.
2653 *
2654 * @param {array} expressionArray - array of expressions
2655 * @returns {Resultset} this resultset for further chain ops.
2656 */
2657 Resultset.prototype.findAnd = function (expressionArray) {
2658 // we have already implementing method chaining in this (our Resultset class)
2659 // so lets just progressively apply user supplied and filters
2660 for (var i = 0, len = expressionArray.length; i < len; i++) {
2661 if (this.count() === 0) {
2662 return this;
2663 }
2664 this.find(expressionArray[i]);
2665 }
2666 return this;
2667 };
2668 Resultset.prototype.$and = Resultset.prototype.findAnd;
2669
2670 /**
2671 * Used for querying via a mongo-style query object.
2672 *
2673 * @param {object} query - A mongo-style query object used for filtering current results.
2674 * @param {boolean=} firstOnly - (Optional) Used by collection.findOne()
2675 * @returns {Resultset} this resultset for further chain ops.
2676 * @memberof Resultset
2677 */
2678 Resultset.prototype.find = function (query, firstOnly) {
2679 if (this.collection.data.length === 0) {
2680 if (this.searchIsChained) {
2681 this.filteredrows = [];
2682 this.filterInitialized = true;
2683 return this;
2684 }
2685 return [];
2686 }
2687
2688 var queryObject = query || 'getAll',
2689 p,
2690 property,
2691 queryObjectOp,
2692 operator,
2693 value,
2694 key,
2695 searchByIndex = false,
2696 result = [],
2697 index = null;
2698
2699 // if this was note invoked via findOne()
2700 firstOnly = firstOnly || false;
2701
2702 if (typeof queryObject === 'object') {
2703 for (p in queryObject) {
2704 if (hasOwnProperty.call(queryObject, p)) {
2705 property = p;
2706 queryObjectOp = queryObject[p];
2707 break;
2708 }
2709 }
2710 }
2711
2712 // apply no filters if they want all
2713 if (!property || queryObject === 'getAll') {
2714 // coll.find(), coll.findOne(), coll.chain().find().data() all path here
2715
2716 if (firstOnly) {
2717 return (this.collection.data.length > 0)?this.collection.data[0]: null;
2718 }
2719
2720 return (this.searchIsChained) ? (this) : (this.collection.data.slice());
2721 }
2722
2723 // injecting $and and $or expression tree evaluation here.
2724 if (property === '$and' || property === '$or') {
2725 if (this.searchIsChained) {
2726 this[property](queryObjectOp);
2727
2728 // for chained find with firstonly,
2729 if (firstOnly && this.filteredrows.length > 1) {
2730 this.filteredrows = this.filteredrows.slice(0, 1);
2731 }
2732
2733 return this;
2734 } else {
2735 // our $and operation internally chains filters
2736 result = this.collection.chain()[property](queryObjectOp).data();
2737
2738 // if this was coll.findOne() return first object or empty array if null
2739 // since this is invoked from a constructor we can't return null, so we will
2740 // make null in coll.findOne();
2741 if (firstOnly) {
2742 return (result.length === 0) ? ([]) : (result[0]);
2743 }
2744
2745 // not first only return all results
2746 return result;
2747 }
2748 }
2749
2750 // see if query object is in shorthand mode (assuming eq operator)
2751 if (queryObjectOp === null || (typeof queryObjectOp !== 'object' || queryObjectOp instanceof Date)) {
2752 operator = '$eq';
2753 value = queryObjectOp;
2754 } else if (typeof queryObjectOp === 'object') {
2755 for (key in queryObjectOp) {
2756 if (hasOwnProperty.call(queryObjectOp, key)) {
2757 operator = key;
2758 value = queryObjectOp[key];
2759 break;
2760 }
2761 }
2762 } else {
2763 throw new Error('Do not know what you want to do.');
2764 }
2765
2766 // for regex ops, precompile
2767 if (operator === '$regex') {
2768 if (Array.isArray(value)) {
2769 value = new RegExp(value[0], value[1]);
2770 } else if (!(value instanceof RegExp)) {
2771 value = new RegExp(value);
2772 }
2773 }
2774
2775 // if user is deep querying the object such as find('name.first': 'odin')
2776 var usingDotNotation = (property.indexOf('.') !== -1);
2777
2778 // if an index exists for the property being queried against, use it
2779 // for now only enabling for non-chained query (who's set of docs matches index)
2780 // or chained queries where it is the first filter applied and prop is indexed
2781 var doIndexCheck = !usingDotNotation &&
2782 (!this.searchIsChained || !this.filterInitialized);
2783
2784 if (doIndexCheck && this.collection.binaryIndices[property] &&
2785 indexedOpsList.indexOf(operator) !== -1) {
2786 // this is where our lazy index rebuilding will take place
2787 // basically we will leave all indexes dirty until we need them
2788 // so here we will rebuild only the index tied to this property
2789 // ensureIndex() will only rebuild if flagged as dirty since we are not passing force=true param
2790 if (this.collection.adaptiveBinaryIndices !== true) {
2791 this.collection.ensureIndex(property);
2792 }
2793
2794 searchByIndex = true;
2795 index = this.collection.binaryIndices[property];
2796 }
2797
2798 // the comparison function
2799 var fun = LokiOps[operator];
2800
2801 // "shortcut" for collection data
2802 var t = this.collection.data;
2803 // filter data length
2804 var i = 0,
2805 len = 0;
2806
2807 // Query executed differently depending on :
2808 // - whether it is chained or not
2809 // - whether the property being queried has an index defined
2810 // - if chained, we handle first pass differently for initial filteredrows[] population
2811 //
2812 // For performance reasons, each case has its own if block to minimize in-loop calculations
2813
2814 // If not a chained query, bypass filteredrows and work directly against data
2815 if (!this.searchIsChained) {
2816 if (!searchByIndex) {
2817 i = t.length;
2818
2819 if (firstOnly) {
2820 if (usingDotNotation) {
2821 property = property.split('.');
2822 while (i--) {
2823 if (dotSubScan(t[i], property, fun, value)) {
2824 return (t[i]);
2825 }
2826 }
2827 } else {
2828 while (i--) {
2829 if (fun(t[i][property], value)) {
2830 return (t[i]);
2831 }
2832 }
2833 }
2834
2835 return [];
2836 }
2837
2838 // if using dot notation then treat property as keypath such as 'name.first'.
2839 // currently supporting dot notation for non-indexed conditions only
2840 if (usingDotNotation) {
2841 property = property.split('.');
2842 while (i--) {
2843 if (dotSubScan(t[i], property, fun, value)) {
2844 result.push(t[i]);
2845 }
2846 }
2847 } else {
2848 while (i--) {
2849 if (fun(t[i][property], value)) {
2850 result.push(t[i]);
2851 }
2852 }
2853 }
2854 } else {
2855 // searching by binary index via calculateRange() utility method
2856 var seg = this.collection.calculateRange(operator, property, value);
2857
2858 // not chained so this 'find' was designated in Resultset constructor
2859 // so return object itself
2860 if (firstOnly) {
2861 if (seg[1] !== -1) {
2862 return t[index.values[seg[0]]];
2863 }
2864 return [];
2865 }
2866
2867 if (operator !== '$in') {
2868 for (i = seg[0]; i <= seg[1]; i++) {
2869 result.push(t[index.values[i]]);
2870 }
2871 } else {
2872 for (i = 0, len = seg.length; i < len; i++) {
2873 result.push(t[index.values[seg[i]]]);
2874 }
2875 }
2876 }
2877
2878 // not a chained query so return result as data[]
2879 return result;
2880 }
2881
2882
2883 // Otherwise this is a chained query
2884
2885 var filter, rowIdx = 0;
2886
2887 // If the filteredrows[] is already initialized, use it
2888 if (this.filterInitialized) {
2889 filter = this.filteredrows;
2890 i = filter.length;
2891
2892 // currently supporting dot notation for non-indexed conditions only
2893 if (usingDotNotation) {
2894 property = property.split('.');
2895 while (i--) {
2896 rowIdx = filter[i];
2897 if (dotSubScan(t[rowIdx], property, fun, value)) {
2898 result.push(rowIdx);
2899 }
2900 }
2901 } else {
2902 while (i--) {
2903 rowIdx = filter[i];
2904 if (fun(t[rowIdx][property], value)) {
2905 result.push(rowIdx);
2906 }
2907 }
2908 }
2909 }
2910 // first chained query so work against data[] but put results in filteredrows
2911 else {
2912 // if not searching by index
2913 if (!searchByIndex) {
2914 i = t.length;
2915
2916 if (usingDotNotation) {
2917 property = property.split('.');
2918 while (i--) {
2919 if (dotSubScan(t[i], property, fun, value)) {
2920 result.push(i);
2921 }
2922 }
2923 } else {
2924 while (i--) {
2925 if (fun(t[i][property], value)) {
2926 result.push(i);
2927 }
2928 }
2929 }
2930 } else {
2931 // search by index
2932 var segm = this.collection.calculateRange(operator, property, value);
2933
2934 if (operator !== '$in') {
2935 for (i = segm[0]; i <= segm[1]; i++) {
2936 result.push(index.values[i]);
2937 }
2938 } else {
2939 for (i = 0, len = segm.length; i < len; i++) {
2940 result.push(index.values[segm[i]]);
2941 }
2942 }
2943 }
2944
2945 this.filterInitialized = true; // next time work against filteredrows[]
2946 }
2947
2948 this.filteredrows = result;
2949 return this;
2950 };
2951
2952
2953 /**
2954 * where() - Used for filtering via a javascript filter function.
2955 *
2956 * @param {function} fun - A javascript function used for filtering current results by.
2957 * @returns {Resultset} this resultset for further chain ops.
2958 * @memberof Resultset
2959 */
2960 Resultset.prototype.where = function (fun) {
2961 var viewFunction,
2962 result = [];
2963
2964 if ('function' === typeof fun) {
2965 viewFunction = fun;
2966 } else {
2967 throw new TypeError('Argument is not a stored view or a function');
2968 }
2969 try {
2970 // if not a chained query then run directly against data[] and return object []
2971 if (!this.searchIsChained) {
2972 var i = this.collection.data.length;
2973
2974 while (i--) {
2975 if (viewFunction(this.collection.data[i]) === true) {
2976 result.push(this.collection.data[i]);
2977 }
2978 }
2979
2980 // not a chained query so returning result as data[]
2981 return result;
2982 }
2983 // else chained query, so run against filteredrows
2984 else {
2985 // If the filteredrows[] is already initialized, use it
2986 if (this.filterInitialized) {
2987 var j = this.filteredrows.length;
2988
2989 while (j--) {
2990 if (viewFunction(this.collection.data[this.filteredrows[j]]) === true) {
2991 result.push(this.filteredrows[j]);
2992 }
2993 }
2994
2995 this.filteredrows = result;
2996
2997 return this;
2998 }
2999 // otherwise this is initial chained op, work against data, push into filteredrows[]
3000 else {
3001 var k = this.collection.data.length;
3002
3003 while (k--) {
3004 if (viewFunction(this.collection.data[k]) === true) {
3005 result.push(k);
3006 }
3007 }
3008
3009 this.filteredrows = result;
3010 this.filterInitialized = true;
3011
3012 return this;
3013 }
3014 }
3015 } catch (err) {
3016 throw err;
3017 }
3018 };
3019
3020 /**
3021 * count() - returns the number of documents in the resultset.
3022 *
3023 * @returns {number} The number of documents in the resultset.
3024 * @memberof Resultset
3025 */
3026 Resultset.prototype.count = function () {
3027 if (this.searchIsChained && this.filterInitialized) {
3028 return this.filteredrows.length;
3029 }
3030 return this.collection.count();
3031 };
3032
3033 /**
3034 * Terminates the chain and returns array of filtered documents
3035 *
3036 * @param {object=} options - allows specifying 'forceClones' and 'forceCloneMethod' options.
3037 * @param {boolean} options.forceClones - Allows forcing the return of cloned objects even when
3038 * the collection is not configured for clone object.
3039 * @param {string} options.forceCloneMethod - Allows overriding the default or collection specified cloning method.
3040 * Possible values include 'parse-stringify', 'jquery-extend-deep', and 'shallow'
3041 *
3042 * @returns {array} Array of documents in the resultset
3043 * @memberof Resultset
3044 */
3045 Resultset.prototype.data = function (options) {
3046 var result = [],
3047 data = this.collection.data,
3048 len,
3049 i,
3050 method;
3051
3052 options = options || {};
3053
3054 // if this is chained resultset with no filters applied, just return collection.data
3055 if (this.searchIsChained && !this.filterInitialized) {
3056 if (this.filteredrows.length === 0) {
3057 // determine whether we need to clone objects or not
3058 if (this.collection.cloneObjects || options.forceClones) {
3059 len = data.length;
3060 method = options.forceCloneMethod || this.collection.cloneMethod;
3061
3062 for (i = 0; i < len; i++) {
3063 result.push(clone(data[i], method));
3064 }
3065 return result;
3066 }
3067 // otherwise we are not cloning so return sliced array with same object references
3068 else {
3069 return data.slice();
3070 }
3071 } else {
3072 // filteredrows must have been set manually, so use it
3073 this.filterInitialized = true;
3074 }
3075 }
3076
3077 var fr = this.filteredrows;
3078 len = fr.length;
3079
3080 if (this.collection.cloneObjects || options.forceClones) {
3081 method = options.forceCloneMethod || this.collection.cloneMethod;
3082 for (i = 0; i < len; i++) {
3083 result.push(clone(data[fr[i]], method));
3084 }
3085 } else {
3086 for (i = 0; i < len; i++) {
3087 result.push(data[fr[i]]);
3088 }
3089 }
3090 return result;
3091 };
3092
3093 /**
3094 * Used to run an update operation on all documents currently in the resultset.
3095 *
3096 * @param {function} updateFunction - User supplied updateFunction(obj) will be executed for each document object.
3097 * @returns {Resultset} this resultset for further chain ops.
3098 * @memberof Resultset
3099 */
3100 Resultset.prototype.update = function (updateFunction) {
3101
3102 if (typeof (updateFunction) !== "function") {
3103 throw new TypeError('Argument is not a function');
3104 }
3105
3106 // if this is chained resultset with no filters applied, we need to populate filteredrows first
3107 if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length === 0) {
3108 this.filteredrows = this.collection.prepareFullDocIndex();
3109 }
3110
3111 var len = this.filteredrows.length,
3112 rcd = this.collection.data;
3113
3114 for (var idx = 0; idx < len; idx++) {
3115 // pass in each document object currently in resultset to user supplied updateFunction
3116 updateFunction(rcd[this.filteredrows[idx]]);
3117
3118 // notify collection we have changed this object so it can update meta and allow DynamicViews to re-evaluate
3119 this.collection.update(rcd[this.filteredrows[idx]]);
3120 }
3121
3122 return this;
3123 };
3124
3125 /**
3126 * Removes all document objects which are currently in resultset from collection (as well as resultset)
3127 *
3128 * @returns {Resultset} this (empty) resultset for further chain ops.
3129 * @memberof Resultset
3130 */
3131 Resultset.prototype.remove = function () {
3132
3133 // if this is chained resultset with no filters applied, we need to populate filteredrows first
3134 if (this.searchIsChained && !this.filterInitialized && this.filteredrows.length === 0) {
3135 this.filteredrows = this.collection.prepareFullDocIndex();
3136 }
3137
3138 this.collection.remove(this.data());
3139
3140 this.filteredrows = [];
3141
3142 return this;
3143 };
3144
3145 /**
3146 * data transformation via user supplied functions
3147 *
3148 * @param {function} mapFunction - this function accepts a single document for you to transform and return
3149 * @param {function} reduceFunction - this function accepts many (array of map outputs) and returns single value
3150 * @returns {value} The output of your reduceFunction
3151 * @memberof Resultset
3152 */
3153 Resultset.prototype.mapReduce = function (mapFunction, reduceFunction) {
3154 try {
3155 return reduceFunction(this.data().map(mapFunction));
3156 } catch (err) {
3157 throw err;
3158 }
3159 };
3160
3161 /**
3162 * eqJoin() - Left joining two sets of data. Join keys can be defined or calculated properties
3163 * eqJoin expects the right join key values to be unique. Otherwise left data will be joined on the last joinData object with that key
3164 * @param {Array} joinData - Data array to join to.
3165 * @param {(string|function)} leftJoinKey - Property name in this result set to join on or a function to produce a value to join on
3166 * @param {(string|function)} rightJoinKey - Property name in the joinData to join on or a function to produce a value to join on
3167 * @param {function=} mapFun - (Optional) A function that receives each matching pair and maps them into output objects - function(left,right){return joinedObject}
3168 * @returns {Resultset} A resultset with data in the format [{left: leftObj, right: rightObj}]
3169 * @memberof Resultset
3170 */
3171 Resultset.prototype.eqJoin = function (joinData, leftJoinKey, rightJoinKey, mapFun) {
3172
3173 var leftData = [],
3174 leftDataLength,
3175 rightData = [],
3176 rightDataLength,
3177 key,
3178 result = [],
3179 leftKeyisFunction = typeof leftJoinKey === 'function',
3180 rightKeyisFunction = typeof rightJoinKey === 'function',
3181 joinMap = {};
3182
3183 //get the left data
3184 leftData = this.data();
3185 leftDataLength = leftData.length;
3186
3187 //get the right data
3188 if (joinData instanceof Resultset) {
3189 rightData = joinData.data();
3190 } else if (Array.isArray(joinData)) {
3191 rightData = joinData;
3192 } else {
3193 throw new TypeError('joinData needs to be an array or result set');
3194 }
3195 rightDataLength = rightData.length;
3196
3197 //construct a lookup table
3198
3199 for (var i = 0; i < rightDataLength; i++) {
3200 key = rightKeyisFunction ? rightJoinKey(rightData[i]) : rightData[i][rightJoinKey];
3201 joinMap[key] = rightData[i];
3202 }
3203
3204 if (!mapFun) {
3205 mapFun = function (left, right) {
3206 return {
3207 left: left,
3208 right: right
3209 };
3210 };
3211 }
3212
3213 //Run map function over each object in the resultset
3214 for (var j = 0; j < leftDataLength; j++) {
3215 key = leftKeyisFunction ? leftJoinKey(leftData[j]) : leftData[j][leftJoinKey];
3216 result.push(mapFun(leftData[j], joinMap[key] || {}));
3217 }
3218
3219 //return return a new resultset with no filters
3220 this.collection = new Collection('joinData');
3221 this.collection.insert(result);
3222 this.filteredrows = [];
3223 this.filterInitialized = false;
3224
3225 return this;
3226 };
3227
3228 Resultset.prototype.map = function (mapFun) {
3229 var data = this.data().map(mapFun);
3230 //return return a new resultset with no filters
3231 this.collection = new Collection('mappedData');
3232 this.collection.insert(data);
3233 this.filteredrows = [];
3234 this.filterInitialized = false;
3235
3236 return this;
3237 };
3238
3239 /**
3240 * DynamicView class is a versatile 'live' view class which can have filters and sorts applied.
3241 * Collection.addDynamicView(name) instantiates this DynamicView object and notifies it
3242 * whenever documents are add/updated/removed so it can remain up-to-date. (chainable)
3243 *
3244 * @example
3245 * var mydv = mycollection.addDynamicView('test'); // default is non-persistent
3246 * mydv.applyFind({ 'doors' : 4 });
3247 * mydv.applyWhere(function(obj) { return obj.name === 'Toyota'; });
3248 * var results = mydv.data();
3249 *
3250 * @constructor DynamicView
3251 * @implements LokiEventEmitter
3252 * @param {Collection} collection - A reference to the collection to work against
3253 * @param {string} name - The name of this dynamic view
3254 * @param {object=} options - (Optional) Pass in object with 'persistent' and/or 'sortPriority' options.
3255 * @param {boolean} options.persistent - indicates if view is to main internal results array in 'resultdata'
3256 * @param {string} options.sortPriority - 'passive' (sorts performed on call to data) or 'active' (after updates)
3257 * @param {number} options.minRebuildInterval - minimum rebuild interval (need clarification to docs here)
3258 * @see {@link Collection#addDynamicView} to construct instances of DynamicView
3259 */
3260 function DynamicView(collection, name, options) {
3261 this.collection = collection;
3262 this.name = name;
3263 this.rebuildPending = false;
3264 this.options = options || {};
3265
3266 if (!this.options.hasOwnProperty('persistent')) {
3267 this.options.persistent = false;
3268 }
3269
3270 // 'persistentSortPriority':
3271 // 'passive' will defer the sort phase until they call data(). (most efficient overall)
3272 // 'active' will sort async whenever next idle. (prioritizes read speeds)
3273 if (!this.options.hasOwnProperty('sortPriority')) {
3274 this.options.sortPriority = 'passive';
3275 }
3276
3277 if (!this.options.hasOwnProperty('minRebuildInterval')) {
3278 this.options.minRebuildInterval = 1;
3279 }
3280
3281 this.resultset = new Resultset(collection);
3282 this.resultdata = [];
3283 this.resultsdirty = false;
3284
3285 this.cachedresultset = null;
3286
3287 // keep ordered filter pipeline
3288 this.filterPipeline = [];
3289
3290 // sorting member variables
3291 // we only support one active search, applied using applySort() or applySimpleSort()
3292 this.sortFunction = null;
3293 this.sortCriteria = null;
3294 this.sortDirty = false;
3295
3296 // for now just have 1 event for when we finally rebuilt lazy view
3297 // once we refactor transactions, i will tie in certain transactional events
3298
3299 this.events = {
3300 'rebuild': []
3301 };
3302 }
3303
3304 DynamicView.prototype = new LokiEventEmitter();
3305
3306
3307 /**
3308 * rematerialize() - intended for use immediately after deserialization (loading)
3309 * This will clear out and reapply filterPipeline ops, recreating the view.
3310 * Since where filters do not persist correctly, this method allows
3311 * restoring the view to state where user can re-apply those where filters.
3312 *
3313 * @param {Object=} options - (Optional) allows specification of 'removeWhereFilters' option
3314 * @returns {DynamicView} This dynamic view for further chained ops.
3315 * @memberof DynamicView
3316 * @fires DynamicView.rebuild
3317 */
3318 DynamicView.prototype.rematerialize = function (options) {
3319 var fpl,
3320 fpi,
3321 idx;
3322
3323 options = options || {};
3324
3325 this.resultdata = [];
3326 this.resultsdirty = true;
3327 this.resultset = new Resultset(this.collection);
3328
3329 if (this.sortFunction || this.sortCriteria) {
3330 this.sortDirty = true;
3331 }
3332
3333 if (options.hasOwnProperty('removeWhereFilters')) {
3334 // for each view see if it had any where filters applied... since they don't
3335 // serialize those functions lets remove those invalid filters
3336 fpl = this.filterPipeline.length;
3337 fpi = fpl;
3338 while (fpi--) {
3339 if (this.filterPipeline[fpi].type === 'where') {
3340 if (fpi !== this.filterPipeline.length - 1) {
3341 this.filterPipeline[fpi] = this.filterPipeline[this.filterPipeline.length - 1];
3342 }
3343
3344 this.filterPipeline.length--;
3345 }
3346 }
3347 }
3348
3349 // back up old filter pipeline, clear filter pipeline, and reapply pipeline ops
3350 var ofp = this.filterPipeline;
3351 this.filterPipeline = [];
3352
3353 // now re-apply 'find' filterPipeline ops
3354 fpl = ofp.length;
3355 for (idx = 0; idx < fpl; idx++) {
3356 this.applyFind(ofp[idx].val);
3357 }
3358
3359 // during creation of unit tests, i will remove this forced refresh and leave lazy
3360 this.data();
3361
3362 // emit rebuild event in case user wants to be notified
3363 this.emit('rebuild', this);
3364
3365 return this;
3366 };
3367
3368 /**
3369 * branchResultset() - Makes a copy of the internal resultset for branched queries.
3370 * Unlike this dynamic view, the branched resultset will not be 'live' updated,
3371 * so your branched query should be immediately resolved and not held for future evaluation.
3372 *
3373 * @param {(string|array=)} transform - Optional name of collection transform, or an array of transform steps
3374 * @param {object=} parameters - optional parameters (if optional transform requires them)
3375 * @returns {Resultset} A copy of the internal resultset for branched queries.
3376 * @memberof DynamicView
3377 */
3378 DynamicView.prototype.branchResultset = function (transform, parameters) {
3379 var rs = this.resultset.branch();
3380
3381 if (typeof transform === 'undefined') {
3382 return rs;
3383 }
3384
3385 return rs.transform(transform, parameters);
3386 };
3387
3388 /**
3389 * toJSON() - Override of toJSON to avoid circular references
3390 *
3391 */
3392 DynamicView.prototype.toJSON = function () {
3393 var copy = new DynamicView(this.collection, this.name, this.options);
3394
3395 copy.resultset = this.resultset;
3396 copy.resultdata = []; // let's not save data (copy) to minimize size
3397 copy.resultsdirty = true;
3398 copy.filterPipeline = this.filterPipeline;
3399 copy.sortFunction = this.sortFunction;
3400 copy.sortCriteria = this.sortCriteria;
3401 copy.sortDirty = this.sortDirty;
3402
3403 // avoid circular reference, reapply in db.loadJSON()
3404 copy.collection = null;
3405
3406 return copy;
3407 };
3408
3409 /**
3410 * removeFilters() - Used to clear pipeline and reset dynamic view to initial state.
3411 * Existing options should be retained.
3412 * @memberof DynamicView
3413 */
3414 DynamicView.prototype.removeFilters = function () {
3415 this.rebuildPending = false;
3416 this.resultset.reset();
3417 this.resultdata = [];
3418 this.resultsdirty = false;
3419
3420 this.cachedresultset = null;
3421
3422 // keep ordered filter pipeline
3423 this.filterPipeline = [];
3424
3425 // sorting member variables
3426 // we only support one active search, applied using applySort() or applySimpleSort()
3427 this.sortFunction = null;
3428 this.sortCriteria = null;
3429 this.sortDirty = false;
3430 };
3431
3432 /**
3433 * applySort() - Used to apply a sort to the dynamic view
3434 * @example
3435 * dv.applySort(function(obj1, obj2) {
3436 * if (obj1.name === obj2.name) return 0;
3437 * if (obj1.name > obj2.name) return 1;
3438 * if (obj1.name < obj2.name) return -1;
3439 * });
3440 *
3441 * @param {function} comparefun - a javascript compare function used for sorting
3442 * @returns {DynamicView} this DynamicView object, for further chain ops.
3443 * @memberof DynamicView
3444 */
3445 DynamicView.prototype.applySort = function (comparefun) {
3446 this.sortFunction = comparefun;
3447 this.sortCriteria = null;
3448
3449 this.queueSortPhase();
3450
3451 return this;
3452 };
3453
3454 /**
3455 * applySimpleSort() - Used to specify a property used for view translation.
3456 * @example
3457 * dv.applySimpleSort("name");
3458 *
3459 * @param {string} propname - Name of property by which to sort.
3460 * @param {boolean=} isdesc - (Optional) If true, the sort will be in descending order.
3461 * @returns {DynamicView} this DynamicView object, for further chain ops.
3462 * @memberof DynamicView
3463 */
3464 DynamicView.prototype.applySimpleSort = function (propname, isdesc) {
3465 this.sortCriteria = [
3466 [propname, isdesc || false]
3467 ];
3468 this.sortFunction = null;
3469
3470 this.queueSortPhase();
3471
3472 return this;
3473 };
3474
3475 /**
3476 * applySortCriteria() - Allows sorting a resultset based on multiple columns.
3477 * @example
3478 * // to sort by age and then name (both ascending)
3479 * dv.applySortCriteria(['age', 'name']);
3480 * // to sort by age (ascending) and then by name (descending)
3481 * dv.applySortCriteria(['age', ['name', true]);
3482 * // to sort by age (descending) and then by name (descending)
3483 * dv.applySortCriteria(['age', true], ['name', true]);
3484 *
3485 * @param {array} properties - array of property names or subarray of [propertyname, isdesc] used evaluate sort order
3486 * @returns {DynamicView} Reference to this DynamicView, sorted, for future chain operations.
3487 * @memberof DynamicView
3488 */
3489 DynamicView.prototype.applySortCriteria = function (criteria) {
3490 this.sortCriteria = criteria;
3491 this.sortFunction = null;
3492
3493 this.queueSortPhase();
3494
3495 return this;
3496 };
3497
3498 /**
3499 * startTransaction() - marks the beginning of a transaction.
3500 *
3501 * @returns {DynamicView} this DynamicView object, for further chain ops.
3502 */
3503 DynamicView.prototype.startTransaction = function () {
3504 this.cachedresultset = this.resultset.copy();
3505
3506 return this;
3507 };
3508
3509 /**
3510 * commit() - commits a transaction.
3511 *
3512 * @returns {DynamicView} this DynamicView object, for further chain ops.
3513 */
3514 DynamicView.prototype.commit = function () {
3515 this.cachedresultset = null;
3516
3517 return this;
3518 };
3519
3520 /**
3521 * rollback() - rolls back a transaction.
3522 *
3523 * @returns {DynamicView} this DynamicView object, for further chain ops.
3524 */
3525 DynamicView.prototype.rollback = function () {
3526 this.resultset = this.cachedresultset;
3527
3528 if (this.options.persistent) {
3529 // for now just rebuild the persistent dynamic view data in this worst case scenario
3530 // (a persistent view utilizing transactions which get rolled back), we already know the filter so not too bad.
3531 this.resultdata = this.resultset.data();
3532
3533 this.emit('rebuild', this);
3534 }
3535
3536 return this;
3537 };
3538
3539
3540 /**
3541 * Implementation detail.
3542 * _indexOfFilterWithId() - Find the index of a filter in the pipeline, by that filter's ID.
3543 *
3544 * @param {(string|number)} uid - The unique ID of the filter.
3545 * @returns {number}: index of the referenced filter in the pipeline; -1 if not found.
3546 */
3547 DynamicView.prototype._indexOfFilterWithId = function (uid) {
3548 if (typeof uid === 'string' || typeof uid === 'number') {
3549 for (var idx = 0, len = this.filterPipeline.length; idx < len; idx += 1) {
3550 if (uid === this.filterPipeline[idx].uid) {
3551 return idx;
3552 }
3553 }
3554 }
3555 return -1;
3556 };
3557
3558 /**
3559 * Implementation detail.
3560 * _addFilter() - Add the filter object to the end of view's filter pipeline and apply the filter to the resultset.
3561 *
3562 * @param {object} filter - The filter object. Refer to applyFilter() for extra details.
3563 */
3564 DynamicView.prototype._addFilter = function (filter) {
3565 this.filterPipeline.push(filter);
3566 this.resultset[filter.type](filter.val);
3567 };
3568
3569 /**
3570 * reapplyFilters() - Reapply all the filters in the current pipeline.
3571 *
3572 * @returns {DynamicView} this DynamicView object, for further chain ops.
3573 */
3574 DynamicView.prototype.reapplyFilters = function () {
3575 this.resultset.reset();
3576
3577 this.cachedresultset = null;
3578 if (this.options.persistent) {
3579 this.resultdata = [];
3580 this.resultsdirty = true;
3581 }
3582
3583 var filters = this.filterPipeline;
3584 this.filterPipeline = [];
3585
3586 for (var idx = 0, len = filters.length; idx < len; idx += 1) {
3587 this._addFilter(filters[idx]);
3588 }
3589
3590 if (this.sortFunction || this.sortCriteria) {
3591 this.queueSortPhase();
3592 } else {
3593 this.queueRebuildEvent();
3594 }
3595
3596 return this;
3597 };
3598
3599 /**
3600 * applyFilter() - Adds or updates a filter in the DynamicView filter pipeline
3601 *
3602 * @param {object} filter - A filter object to add to the pipeline.
3603 * The object is in the format { 'type': filter_type, 'val', filter_param, 'uid', optional_filter_id }
3604 * @returns {DynamicView} this DynamicView object, for further chain ops.
3605 * @memberof DynamicView
3606 */
3607 DynamicView.prototype.applyFilter = function (filter) {
3608 var idx = this._indexOfFilterWithId(filter.uid);
3609 if (idx >= 0) {
3610 this.filterPipeline[idx] = filter;
3611 return this.reapplyFilters();
3612 }
3613
3614 this.cachedresultset = null;
3615 if (this.options.persistent) {
3616 this.resultdata = [];
3617 this.resultsdirty = true;
3618 }
3619
3620 this._addFilter(filter);
3621
3622 if (this.sortFunction || this.sortCriteria) {
3623 this.queueSortPhase();
3624 } else {
3625 this.queueRebuildEvent();
3626 }
3627
3628 return this;
3629 };
3630
3631 /**
3632 * applyFind() - Adds or updates a mongo-style query option in the DynamicView filter pipeline
3633 *
3634 * @param {object} query - A mongo-style query object to apply to pipeline
3635 * @param {(string|number)=} uid - Optional: The unique ID of this filter, to reference it in the future.
3636 * @returns {DynamicView} this DynamicView object, for further chain ops.
3637 * @memberof DynamicView
3638 */
3639 DynamicView.prototype.applyFind = function (query, uid) {
3640 this.applyFilter({
3641 type: 'find',
3642 val: query,
3643 uid: uid
3644 });
3645 return this;
3646 };
3647
3648 /**
3649 * applyWhere() - Adds or updates a javascript filter function in the DynamicView filter pipeline
3650 *
3651 * @param {function} fun - A javascript filter function to apply to pipeline
3652 * @param {(string|number)=} uid - Optional: The unique ID of this filter, to reference it in the future.
3653 * @returns {DynamicView} this DynamicView object, for further chain ops.
3654 * @memberof DynamicView
3655 */
3656 DynamicView.prototype.applyWhere = function (fun, uid) {
3657 this.applyFilter({
3658 type: 'where',
3659 val: fun,
3660 uid: uid
3661 });
3662 return this;
3663 };
3664
3665 /**
3666 * removeFilter() - Remove the specified filter from the DynamicView filter pipeline
3667 *
3668 * @param {(string|number)} uid - The unique ID of the filter to be removed.
3669 * @returns {DynamicView} this DynamicView object, for further chain ops.
3670 * @memberof DynamicView
3671 */
3672 DynamicView.prototype.removeFilter = function (uid) {
3673 var idx = this._indexOfFilterWithId(uid);
3674 if (idx < 0) {
3675 throw new Error("Dynamic view does not contain a filter with ID: " + uid);
3676 }
3677
3678 this.filterPipeline.splice(idx, 1);
3679 this.reapplyFilters();
3680 return this;
3681 };
3682
3683 /**
3684 * count() - returns the number of documents representing the current DynamicView contents.
3685 *
3686 * @returns {number} The number of documents representing the current DynamicView contents.
3687 * @memberof DynamicView
3688 */
3689 DynamicView.prototype.count = function () {
3690 if (this.options.persistent) {
3691 return this.resultdata.length;
3692 }
3693 return this.resultset.count();
3694 };
3695
3696 /**
3697 * data() - resolves and pending filtering and sorting, then returns document array as result.
3698 *
3699 * @returns {array} An array of documents representing the current DynamicView contents.
3700 * @memberof DynamicView
3701 */
3702 DynamicView.prototype.data = function () {
3703 // using final sort phase as 'catch all' for a few use cases which require full rebuild
3704 if (this.sortDirty || this.resultsdirty) {
3705 this.performSortPhase({
3706 suppressRebuildEvent: true
3707 });
3708 }
3709 return (this.options.persistent) ? (this.resultdata) : (this.resultset.data());
3710 };
3711
3712 /**
3713 * queueRebuildEvent() - When the view is not sorted we may still wish to be notified of rebuild events.
3714 * This event will throttle and queue a single rebuild event when batches of updates affect the view.
3715 */
3716 DynamicView.prototype.queueRebuildEvent = function () {
3717 if (this.rebuildPending) {
3718 return;
3719 }
3720 this.rebuildPending = true;
3721
3722 var self = this;
3723 setTimeout(function () {
3724 if (self.rebuildPending) {
3725 self.rebuildPending = false;
3726 self.emit('rebuild', self);
3727 }
3728 }, this.options.minRebuildInterval);
3729 };
3730
3731 /**
3732 * queueSortPhase : If the view is sorted we will throttle sorting to either :
3733 * (1) passive - when the user calls data(), or
3734 * (2) active - once they stop updating and yield js thread control
3735 */
3736 DynamicView.prototype.queueSortPhase = function () {
3737 // already queued? exit without queuing again
3738 if (this.sortDirty) {
3739 return;
3740 }
3741 this.sortDirty = true;
3742
3743 var self = this;
3744 if (this.options.sortPriority === "active") {
3745 // active sorting... once they are done and yield js thread, run async performSortPhase()
3746 setTimeout(function () {
3747 self.performSortPhase();
3748 }, this.options.minRebuildInterval);
3749 } else {
3750 // must be passive sorting... since not calling performSortPhase (until data call), lets use queueRebuildEvent to
3751 // potentially notify user that data has changed.
3752 this.queueRebuildEvent();
3753 }
3754 };
3755
3756 /**
3757 * performSortPhase() - invoked synchronously or asynchronously to perform final sort phase (if needed)
3758 *
3759 */
3760 DynamicView.prototype.performSortPhase = function (options) {
3761 // async call to this may have been pre-empted by synchronous call to data before async could fire
3762 if (!this.sortDirty && !this.resultsdirty) {
3763 return;
3764 }
3765
3766 options = options || {};
3767
3768 if (this.sortDirty) {
3769 if (this.sortFunction) {
3770 this.resultset.sort(this.sortFunction);
3771 } else if (this.sortCriteria) {
3772 this.resultset.compoundsort(this.sortCriteria);
3773 }
3774
3775 this.sortDirty = false;
3776 }
3777
3778 if (this.options.persistent) {
3779 // persistent view, rebuild local resultdata array
3780 this.resultdata = this.resultset.data();
3781 this.resultsdirty = false;
3782 }
3783
3784 if (!options.suppressRebuildEvent) {
3785 this.emit('rebuild', this);
3786 }
3787 };
3788
3789 /**
3790 * evaluateDocument() - internal method for (re)evaluating document inclusion.
3791 * Called by : collection.insert() and collection.update().
3792 *
3793 * @param {int} objIndex - index of document to (re)run through filter pipeline.
3794 * @param {bool} isNew - true if the document was just added to the collection.
3795 */
3796 DynamicView.prototype.evaluateDocument = function (objIndex, isNew) {
3797 // if no filter applied yet, the result 'set' should remain 'everything'
3798 if (!this.resultset.filterInitialized) {
3799 if (this.options.persistent) {
3800 this.resultdata = this.resultset.data();
3801 }
3802 // need to re-sort to sort new document
3803 if (this.sortFunction || this.sortCriteria) {
3804 this.queueSortPhase();
3805 } else {
3806 this.queueRebuildEvent();
3807 }
3808 return;
3809 }
3810
3811 var ofr = this.resultset.filteredrows;
3812 var oldPos = (isNew) ? (-1) : (ofr.indexOf(+objIndex));
3813 var oldlen = ofr.length;
3814
3815 // creating a 1-element resultset to run filter chain ops on to see if that doc passes filters;
3816 // mostly efficient algorithm, slight stack overhead price (this function is called on inserts and updates)
3817 var evalResultset = new Resultset(this.collection);
3818 evalResultset.filteredrows = [objIndex];
3819 evalResultset.filterInitialized = true;
3820 var filter;
3821 for (var idx = 0, len = this.filterPipeline.length; idx < len; idx++) {
3822 filter = this.filterPipeline[idx];
3823 evalResultset[filter.type](filter.val);
3824 }
3825
3826 // not a true position, but -1 if not pass our filter(s), 0 if passed filter(s)
3827 var newPos = (evalResultset.filteredrows.length === 0) ? -1 : 0;
3828
3829 // wasn't in old, shouldn't be now... do nothing
3830 if (oldPos === -1 && newPos === -1) return;
3831
3832 // wasn't in resultset, should be now... add
3833 if (oldPos === -1 && newPos !== -1) {
3834 ofr.push(objIndex);
3835
3836 if (this.options.persistent) {
3837 this.resultdata.push(this.collection.data[objIndex]);
3838 }
3839
3840 // need to re-sort to sort new document
3841 if (this.sortFunction || this.sortCriteria) {
3842 this.queueSortPhase();
3843 } else {
3844 this.queueRebuildEvent();
3845 }
3846
3847 return;
3848 }
3849
3850 // was in resultset, shouldn't be now... delete
3851 if (oldPos !== -1 && newPos === -1) {
3852 if (oldPos < oldlen - 1) {
3853 ofr.splice(oldPos, 1);
3854
3855 if (this.options.persistent) {
3856 this.resultdata.splice(oldPos, 1);
3857 }
3858 } else {
3859 ofr.length = oldlen - 1;
3860
3861 if (this.options.persistent) {
3862 this.resultdata.length = oldlen - 1;
3863 }
3864 }
3865
3866 // in case changes to data altered a sort column
3867 if (this.sortFunction || this.sortCriteria) {
3868 this.queueSortPhase();
3869 } else {
3870 this.queueRebuildEvent();
3871 }
3872
3873 return;
3874 }
3875
3876 // was in resultset, should still be now... (update persistent only?)
3877 if (oldPos !== -1 && newPos !== -1) {
3878 if (this.options.persistent) {
3879 // in case document changed, replace persistent view data with the latest collection.data document
3880 this.resultdata[oldPos] = this.collection.data[objIndex];
3881 }
3882
3883 // in case changes to data altered a sort column
3884 if (this.sortFunction || this.sortCriteria) {
3885 this.queueSortPhase();
3886 } else {
3887 this.queueRebuildEvent();
3888 }
3889
3890 return;
3891 }
3892 };
3893
3894 /**
3895 * removeDocument() - internal function called on collection.delete()
3896 */
3897 DynamicView.prototype.removeDocument = function (objIndex) {
3898 // if no filter applied yet, the result 'set' should remain 'everything'
3899 if (!this.resultset.filterInitialized) {
3900 if (this.options.persistent) {
3901 this.resultdata = this.resultset.data();
3902 }
3903 // in case changes to data altered a sort column
3904 if (this.sortFunction || this.sortCriteria) {
3905 this.queueSortPhase();
3906 } else {
3907 this.queueRebuildEvent();
3908 }
3909 return;
3910 }
3911
3912 var ofr = this.resultset.filteredrows;
3913 var oldPos = ofr.indexOf(+objIndex);
3914 var oldlen = ofr.length;
3915 var idx;
3916
3917 if (oldPos !== -1) {
3918 // if not last row in resultdata, swap last to hole and truncate last row
3919 if (oldPos < oldlen - 1) {
3920 ofr[oldPos] = ofr[oldlen - 1];
3921 ofr.length = oldlen - 1;
3922
3923 if (this.options.persistent) {
3924 this.resultdata[oldPos] = this.resultdata[oldlen - 1];
3925 this.resultdata.length = oldlen - 1;
3926 }
3927 }
3928 // last row, so just truncate last row
3929 else {
3930 ofr.length = oldlen - 1;
3931
3932 if (this.options.persistent) {
3933 this.resultdata.length = oldlen - 1;
3934 }
3935 }
3936
3937 // in case changes to data altered a sort column
3938 if (this.sortFunction || this.sortCriteria) {
3939 this.queueSortPhase();
3940 } else {
3941 this.queueRebuildEvent();
3942 }
3943 }
3944
3945 // since we are using filteredrows to store data array positions
3946 // if they remove a document (whether in our view or not),
3947 // we need to adjust array positions -1 for all document array references after that position
3948 oldlen = ofr.length;
3949 for (idx = 0; idx < oldlen; idx++) {
3950 if (ofr[idx] > objIndex) {
3951 ofr[idx]--;
3952 }
3953 }
3954 };
3955
3956 /**
3957 * mapReduce() - data transformation via user supplied functions
3958 *
3959 * @param {function} mapFunction - this function accepts a single document for you to transform and return
3960 * @param {function} reduceFunction - this function accepts many (array of map outputs) and returns single value
3961 * @returns The output of your reduceFunction
3962 * @memberof DynamicView
3963 */
3964 DynamicView.prototype.mapReduce = function (mapFunction, reduceFunction) {
3965 try {
3966 return reduceFunction(this.data().map(mapFunction));
3967 } catch (err) {
3968 throw err;
3969 }
3970 };
3971
3972
3973 /**
3974 * Collection class that handles documents of same type
3975 * @constructor Collection
3976 * @implements LokiEventEmitter
3977 * @param {string} name - collection name
3978 * @param {(array|object)=} options - (optional) array of property names to be indicized OR a configuration object
3979 * @param {array} options.unique - array of property names to define unique constraints for
3980 * @param {array} options.exact - array of property names to define exact constraints for
3981 * @param {array} options.indices - array property names to define binary indexes for
3982 * @param {boolean} options.adaptiveBinaryIndices - collection indices will be actively rebuilt rather than lazily (default: true)
3983 * @param {boolean} options.asyncListeners - default is false
3984 * @param {boolean} options.disableChangesApi - default is true
3985 * @param {boolean} options.autoupdate - use Object.observe to update objects automatically (default: false)
3986 * @param {boolean} options.clone - specify whether inserts and queries clone to/from user
3987 * @param {string} options.cloneMethod - 'parse-stringify' (default), 'jquery-extend-deep', 'shallow'
3988 * @param {int} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default.
3989 * @see {@link Loki#addCollection} for normal creation of collections
3990 */
3991 function Collection(name, options) {
3992 // the name of the collection
3993
3994 this.name = name;
3995 // the data held by the collection
3996 this.data = [];
3997 this.idIndex = []; // index of id
3998 this.binaryIndices = {}; // user defined indexes
3999 this.constraints = {
4000 unique: {},
4001 exact: {}
4002 };
4003
4004 // unique contraints contain duplicate object references, so they are not persisted.
4005 // we will keep track of properties which have unique contraint applied here, and regenerate on load
4006 this.uniqueNames = [];
4007
4008 // transforms will be used to store frequently used query chains as a series of steps
4009 // which itself can be stored along with the database.
4010 this.transforms = {};
4011
4012 // the object type of the collection
4013 this.objType = name;
4014
4015 // in autosave scenarios we will use collection level dirty flags to determine whether save is needed.
4016 // currently, if any collection is dirty we will autosave the whole database if autosave is configured.
4017 // defaulting to true since this is called from addCollection and adding a collection should trigger save
4018 this.dirty = true;
4019
4020 // private holders for cached data
4021 this.cachedIndex = null;
4022 this.cachedBinaryIndex = null;
4023 this.cachedData = null;
4024 var self = this;
4025
4026 /* OPTIONS */
4027 options = options || {};
4028
4029 // exact match and unique constraints
4030 if (options.hasOwnProperty('unique')) {
4031 if (!Array.isArray(options.unique)) {
4032 options.unique = [options.unique];
4033 }
4034 options.unique.forEach(function (prop) {
4035 self.uniqueNames.push(prop); // used to regenerate on subsequent database loads
4036 self.constraints.unique[prop] = new UniqueIndex(prop);
4037 });
4038 }
4039
4040 if (options.hasOwnProperty('exact')) {
4041 options.exact.forEach(function (prop) {
4042 self.constraints.exact[prop] = new ExactIndex(prop);
4043 });
4044 }
4045
4046 // if set to true we will optimally keep indices 'fresh' during insert/update/remove ops (never dirty/never needs rebuild)
4047 // if you frequently intersperse insert/update/remove ops between find ops this will likely be significantly faster option.
4048 this.adaptiveBinaryIndices = options.hasOwnProperty('adaptiveBinaryIndices') ? options.adaptiveBinaryIndices : true;
4049
4050 // is collection transactional
4051 this.transactional = options.hasOwnProperty('transactional') ? options.transactional : false;
4052
4053 // options to clone objects when inserting them
4054 this.cloneObjects = options.hasOwnProperty('clone') ? options.clone : false;
4055
4056 // default clone method (if enabled) is parse-stringify
4057 this.cloneMethod = options.hasOwnProperty('cloneMethod') ? options.cloneMethod : "parse-stringify";
4058
4059 // option to make event listeners async, default is sync
4060 this.asyncListeners = options.hasOwnProperty('asyncListeners') ? options.asyncListeners : false;
4061
4062 // disable track changes
4063 this.disableChangesApi = options.hasOwnProperty('disableChangesApi') ? options.disableChangesApi : true;
4064
4065 // option to observe objects and update them automatically, ignored if Object.observe is not supported
4066 this.autoupdate = options.hasOwnProperty('autoupdate') ? options.autoupdate : false;
4067
4068 //option to activate a cleaner daemon - clears "aged" documents at set intervals.
4069 this.ttl = {
4070 age: null,
4071 ttlInterval: null,
4072 daemon: null
4073 };
4074 this.setTTL(options.ttl || -1, options.ttlInterval);
4075
4076 // currentMaxId - change manually at your own peril!
4077 this.maxId = 0;
4078
4079 this.DynamicViews = [];
4080
4081 // events
4082 this.events = {
4083 'insert': [],
4084 'update': [],
4085 'pre-insert': [],
4086 'pre-update': [],
4087 'close': [],
4088 'flushbuffer': [],
4089 'error': [],
4090 'delete': [],
4091 'warning': []
4092 };
4093
4094 // changes are tracked by collection and aggregated by the db
4095 this.changes = [];
4096
4097 // initialize the id index
4098 this.ensureId();
4099 var indices = [];
4100 // initialize optional user-supplied indices array ['age', 'lname', 'zip']
4101 if (options && options.indices) {
4102 if (Object.prototype.toString.call(options.indices) === '[object Array]') {
4103 indices = options.indices;
4104 } else if (typeof options.indices === 'string') {
4105 indices = [options.indices];
4106 } else {
4107 throw new TypeError('Indices needs to be a string or an array of strings');
4108 }
4109 }
4110
4111 for (var idx = 0; idx < indices.length; idx++) {
4112 this.ensureIndex(indices[idx]);
4113 }
4114
4115 function observerCallback(changes) {
4116
4117 var changedObjects = typeof Set === 'function' ? new Set() : [];
4118
4119 if (!changedObjects.add)
4120 changedObjects.add = function (object) {
4121 if (this.indexOf(object) === -1)
4122 this.push(object);
4123 return this;
4124 };
4125
4126 changes.forEach(function (change) {
4127 changedObjects.add(change.object);
4128 });
4129
4130 changedObjects.forEach(function (object) {
4131 if (!hasOwnProperty.call(object, '$loki'))
4132 return self.removeAutoUpdateObserver(object);
4133 try {
4134 self.update(object);
4135 } catch (err) {}
4136 });
4137 }
4138
4139 this.observerCallback = observerCallback;
4140
4141 /*
4142 * This method creates a clone of the current status of an object and associates operation and collection name,
4143 * so the parent db can aggregate and generate a changes object for the entire db
4144 */
4145 function createChange(name, op, obj) {
4146 self.changes.push({
4147 name: name,
4148 operation: op,
4149 obj: JSON.parse(JSON.stringify(obj))
4150 });
4151 }
4152
4153 // clear all the changes
4154 function flushChanges() {
4155 self.changes = [];
4156 }
4157
4158 this.getChanges = function () {
4159 return self.changes;
4160 };
4161
4162 this.flushChanges = flushChanges;
4163
4164 /**
4165 * If the changes API is disabled make sure only metadata is added without re-evaluating everytime if the changesApi is enabled
4166 */
4167 function insertMeta(obj) {
4168 if (!obj) {
4169 return;
4170 }
4171 if (!obj.meta) {
4172 obj.meta = {};
4173 }
4174
4175 obj.meta.created = (new Date()).getTime();
4176 obj.meta.revision = 0;
4177 }
4178
4179 function updateMeta(obj) {
4180 if (!obj) {
4181 return;
4182 }
4183 obj.meta.updated = (new Date()).getTime();
4184 obj.meta.revision += 1;
4185 }
4186
4187 function createInsertChange(obj) {
4188 createChange(self.name, 'I', obj);
4189 }
4190
4191 function createUpdateChange(obj) {
4192 createChange(self.name, 'U', obj);
4193 }
4194
4195 function insertMetaWithChange(obj) {
4196 insertMeta(obj);
4197 createInsertChange(obj);
4198 }
4199
4200 function updateMetaWithChange(obj) {
4201 updateMeta(obj);
4202 createUpdateChange(obj);
4203 }
4204
4205
4206 /* assign correct handler based on ChangesAPI flag */
4207 var insertHandler, updateHandler;
4208
4209 function setHandlers() {
4210 insertHandler = self.disableChangesApi ? insertMeta : insertMetaWithChange;
4211 updateHandler = self.disableChangesApi ? updateMeta : updateMetaWithChange;
4212 }
4213
4214 setHandlers();
4215
4216 this.setChangesApi = function (enabled) {
4217 self.disableChangesApi = !enabled;
4218 setHandlers();
4219 };
4220 /**
4221 * built-in events
4222 */
4223 this.on('insert', function insertCallback(obj) {
4224 insertHandler(obj);
4225 });
4226
4227 this.on('update', function updateCallback(obj) {
4228 updateHandler(obj);
4229 });
4230
4231 this.on('delete', function deleteCallback(obj) {
4232 if (!self.disableChangesApi) {
4233 createChange(self.name, 'R', obj);
4234 }
4235 });
4236
4237 this.on('warning', function (warning) {
4238 self.console.warn(warning);
4239 });
4240 // for de-serialization purposes
4241 flushChanges();
4242 }
4243
4244 Collection.prototype = new LokiEventEmitter();
4245
4246 Collection.prototype.console = {
4247 log: function () {},
4248 warn: function () {},
4249 error: function () {},
4250 };
4251
4252 Collection.prototype.addAutoUpdateObserver = function (object) {
4253 if (!this.autoupdate || typeof Object.observe !== 'function')
4254 return;
4255
4256 Object.observe(object, this.observerCallback, ['add', 'update', 'delete', 'reconfigure', 'setPrototype']);
4257 };
4258
4259 Collection.prototype.removeAutoUpdateObserver = function (object) {
4260 if (!this.autoupdate || typeof Object.observe !== 'function')
4261 return;
4262
4263 Object.unobserve(object, this.observerCallback);
4264 };
4265
4266 /**
4267 * Adds a named collection transform to the collection
4268 * @param {string} name - name to associate with transform
4269 * @param {array} transform - an array of transformation 'step' objects to save into the collection
4270 * @memberof Collection
4271 */
4272 Collection.prototype.addTransform = function (name, transform) {
4273 if (this.transforms.hasOwnProperty(name)) {
4274 throw new Error("a transform by that name already exists");
4275 }
4276
4277 this.transforms[name] = transform;
4278 };
4279
4280 /**
4281 * Updates a named collection transform to the collection
4282 * @param {string} name - name to associate with transform
4283 * @param {object} transform - a transformation object to save into collection
4284 * @memberof Collection
4285 */
4286 Collection.prototype.setTransform = function (name, transform) {
4287 this.transforms[name] = transform;
4288 };
4289
4290 /**
4291 * Removes a named collection transform from the collection
4292 * @param {string} name - name of collection transform to remove
4293 * @memberof Collection
4294 */
4295 Collection.prototype.removeTransform = function (name) {
4296 delete this.transforms[name];
4297 };
4298
4299 Collection.prototype.byExample = function (template) {
4300 var k, obj, query;
4301 query = [];
4302 for (k in template) {
4303 if (!template.hasOwnProperty(k)) continue;
4304 query.push((
4305 obj = {},
4306 obj[k] = template[k],
4307 obj
4308 ));
4309 }
4310 return {
4311 '$and': query
4312 };
4313 };
4314
4315 Collection.prototype.findObject = function (template) {
4316 return this.findOne(this.byExample(template));
4317 };
4318
4319 Collection.prototype.findObjects = function (template) {
4320 return this.find(this.byExample(template));
4321 };
4322
4323 /*----------------------------+
4324 | TTL daemon |
4325 +----------------------------*/
4326 Collection.prototype.ttlDaemonFuncGen = function () {
4327 var collection = this;
4328 var age = this.ttl.age;
4329 return function ttlDaemon() {
4330 var now = Date.now();
4331 var toRemove = collection.chain().where(function daemonFilter(member) {
4332 var timestamp = member.meta.updated || member.meta.created;
4333 var diff = now - timestamp;
4334 return age < diff;
4335 });
4336 toRemove.remove();
4337 };
4338 };
4339
4340 Collection.prototype.setTTL = function (age, interval) {
4341 if (age < 0) {
4342 clearInterval(this.ttl.daemon);
4343 } else {
4344 this.ttl.age = age;
4345 this.ttl.ttlInterval = interval;
4346 this.ttl.daemon = setInterval(this.ttlDaemonFuncGen(), interval);
4347 }
4348 };
4349
4350 /*----------------------------+
4351 | INDEXING |
4352 +----------------------------*/
4353
4354 /**
4355 * create a row filter that covers all documents in the collection
4356 */
4357 Collection.prototype.prepareFullDocIndex = function () {
4358 var len = this.data.length;
4359 var indexes = new Array(len);
4360 for (var i = 0; i < len; i += 1) {
4361 indexes[i] = i;
4362 }
4363 return indexes;
4364 };
4365
4366 /**
4367 * Will allow reconfiguring certain collection options.
4368 * @param {boolean} options.adaptiveBinaryIndices - collection indices will be actively rebuilt rather than lazily
4369 * @memberof Collection
4370 */
4371 Collection.prototype.configureOptions = function (options) {
4372 options = options || {};
4373
4374 if (options.hasOwnProperty('adaptiveBinaryIndices')) {
4375 this.adaptiveBinaryIndices = options.adaptiveBinaryIndices;
4376
4377 // if switching to adaptive binary indices, make sure none are 'dirty'
4378 if (this.adaptiveBinaryIndices) {
4379 this.ensureAllIndexes();
4380 }
4381 }
4382 };
4383
4384 /**
4385 * Ensure binary index on a certain field
4386 * @param {string} property - name of property to create binary index on
4387 * @param {boolean=} force - (Optional) flag indicating whether to construct index immediately
4388 * @memberof Collection
4389 */
4390 Collection.prototype.ensureIndex = function (property, force) {
4391 // optional parameter to force rebuild whether flagged as dirty or not
4392 if (typeof (force) === 'undefined') {
4393 force = false;
4394 }
4395
4396 if (property === null || property === undefined) {
4397 throw new Error('Attempting to set index without an associated property');
4398 }
4399
4400 if (this.binaryIndices[property] && !force) {
4401 if (!this.binaryIndices[property].dirty) return;
4402 }
4403
4404 var index = {
4405 'name': property,
4406 'dirty': true,
4407 'values': this.prepareFullDocIndex()
4408 };
4409 this.binaryIndices[property] = index;
4410
4411 var wrappedComparer =
4412 (function (p, data) {
4413 return function (a, b) {
4414 var objAp = data[a][p],
4415 objBp = data[b][p];
4416 if (objAp !== objBp) {
4417 if (ltHelper(objAp, objBp, false)) return -1;
4418 if (gtHelper(objAp, objBp, false)) return 1;
4419 }
4420 return 0;
4421 };
4422 })(property, this.data);
4423
4424 index.values.sort(wrappedComparer);
4425 index.dirty = false;
4426
4427 this.dirty = true; // for autosave scenarios
4428 };
4429
4430 Collection.prototype.getSequencedIndexValues = function (property) {
4431 var idx, idxvals = this.binaryIndices[property].values;
4432 var result = "";
4433
4434 for (idx = 0; idx < idxvals.length; idx++) {
4435 result += " [" + idx + "] " + this.data[idxvals[idx]][property];
4436 }
4437
4438 return result;
4439 };
4440
4441 Collection.prototype.ensureUniqueIndex = function (field) {
4442 var index = this.constraints.unique[field];
4443 if (!index) {
4444 // keep track of new unique index for regenerate after database (re)load.
4445 if (this.uniqueNames.indexOf(field) == -1) {
4446 this.uniqueNames.push(field);
4447 }
4448 }
4449
4450 // if index already existed, (re)loading it will likely cause collisions, rebuild always
4451 this.constraints.unique[field] = index = new UniqueIndex(field);
4452 this.data.forEach(function (obj) {
4453 index.set(obj);
4454 });
4455 return index;
4456 };
4457
4458 /**
4459 * Ensure all binary indices
4460 */
4461 Collection.prototype.ensureAllIndexes = function (force) {
4462 var key, bIndices = this.binaryIndices;
4463 for (key in bIndices) {
4464 if (hasOwnProperty.call(bIndices, key)) {
4465 this.ensureIndex(key, force);
4466 }
4467 }
4468 };
4469
4470 Collection.prototype.flagBinaryIndexesDirty = function () {
4471 var key, bIndices = this.binaryIndices;
4472 for (key in bIndices) {
4473 if (hasOwnProperty.call(bIndices, key)) {
4474 bIndices[key].dirty = true;
4475 }
4476 }
4477 };
4478
4479 Collection.prototype.flagBinaryIndexDirty = function (index) {
4480 if (this.binaryIndices[index])
4481 this.binaryIndices[index].dirty = true;
4482 };
4483
4484 /**
4485 * Quickly determine number of documents in collection (or query)
4486 * @param {object=} query - (optional) query object to count results of
4487 * @returns {number} number of documents in the collection
4488 * @memberof Collection
4489 */
4490 Collection.prototype.count = function (query) {
4491 if (!query) {
4492 return this.data.length;
4493 }
4494
4495 return this.chain().find(query).filteredrows.length;
4496 };
4497
4498 /**
4499 * Rebuild idIndex
4500 */
4501 Collection.prototype.ensureId = function () {
4502 var len = this.data.length,
4503 i = 0;
4504
4505 this.idIndex = [];
4506 for (i; i < len; i += 1) {
4507 this.idIndex.push(this.data[i].$loki);
4508 }
4509 };
4510
4511 /**
4512 * Rebuild idIndex async with callback - useful for background syncing with a remote server
4513 */
4514 Collection.prototype.ensureIdAsync = function (callback) {
4515 this.async(function () {
4516 this.ensureId();
4517 }, callback);
4518 };
4519
4520 /**
4521 * Add a dynamic view to the collection
4522 * @param {string} name - name of dynamic view to add
4523 * @param {object=} options - (optional) options to configure dynamic view with
4524 * @param {boolean} options.persistent - indicates if view is to main internal results array in 'resultdata'
4525 * @param {string} options.sortPriority - 'passive' (sorts performed on call to data) or 'active' (after updates)
4526 * @param {number} options.minRebuildInterval - minimum rebuild interval (need clarification to docs here)
4527 * @returns {DynamicView} reference to the dynamic view added
4528 * @memberof Collection
4529 **/
4530
4531 Collection.prototype.addDynamicView = function (name, options) {
4532 var dv = new DynamicView(this, name, options);
4533 this.DynamicViews.push(dv);
4534
4535 return dv;
4536 };
4537
4538 /**
4539 * Remove a dynamic view from the collection
4540 * @param {string} name - name of dynamic view to remove
4541 * @memberof Collection
4542 **/
4543 Collection.prototype.removeDynamicView = function (name) {
4544 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
4545 if (this.DynamicViews[idx].name === name) {
4546 this.DynamicViews.splice(idx, 1);
4547 }
4548 }
4549 };
4550
4551 /**
4552 * Look up dynamic view reference from within the collection
4553 * @param {string} name - name of dynamic view to retrieve reference of
4554 * @returns {DynamicView} A reference to the dynamic view with that name
4555 * @memberof Collection
4556 **/
4557 Collection.prototype.getDynamicView = function (name) {
4558 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
4559 if (this.DynamicViews[idx].name === name) {
4560 return this.DynamicViews[idx];
4561 }
4562 }
4563
4564 return null;
4565 };
4566
4567 /**
4568 * Applies a 'mongo-like' find query object and passes all results to an update function.
4569 * For filter function querying you should migrate to [updateWhere()]{@link Collection#updateWhere}.
4570 *
4571 * @param {object|function} filterObject - 'mongo-like' query object (or deprecated filterFunction mode)
4572 * @param {function} updateFunction - update function to run against filtered documents
4573 * @memberof Collection
4574 */
4575 Collection.prototype.findAndUpdate = function (filterObject, updateFunction) {
4576 if (typeof (filterObject) === "function") {
4577 this.updateWhere(filterObject, updateFunction);
4578 }
4579 else {
4580 this.chain().find(filterObject).update(updateFunction);
4581 }
4582 };
4583
4584 /**
4585 * Applies a 'mongo-like' find query object removes all documents which match that filter.
4586 *
4587 * @param {object} filterObject - 'mongo-like' query object
4588 * @memberof Collection
4589 */
4590 Collection.prototype.findAndRemove = function(filterObject) {
4591 this.chain().find(filterObject).remove();
4592 };
4593
4594 /**
4595 * Adds object(s) to collection, ensure object(s) have meta properties, clone it if necessary, etc.
4596 * @param {(object|array)} doc - the document (or array of documents) to be inserted
4597 * @returns {(object|array)} document or documents inserted
4598 * @memberof Collection
4599 */
4600 Collection.prototype.insert = function (doc) {
4601 if (!Array.isArray(doc)) {
4602 return this.insertOne(doc);
4603 }
4604
4605 // holder to the clone of the object inserted if collections is set to clone objects
4606 var obj;
4607 var results = [];
4608
4609 this.emit('pre-insert', doc);
4610 for (var i = 0, len = doc.length; i < len; i++) {
4611 obj = this.insertOne(doc[i], true);
4612 if (!obj) {
4613 return undefined;
4614 }
4615 results.push(obj);
4616 }
4617 this.emit('insert', doc);
4618 return results.length === 1 ? results[0] : results;
4619 };
4620
4621 /**
4622 * Adds a single object, ensures it has meta properties, clone it if necessary, etc.
4623 * @param {object} doc - the document to be inserted
4624 * @param {boolean} bulkInsert - quiet pre-insert and insert event emits
4625 * @returns {object} document or 'undefined' if there was a problem inserting it
4626 * @memberof Collection
4627 */
4628 Collection.prototype.insertOne = function (doc, bulkInsert) {
4629 var err = null;
4630 var returnObj;
4631
4632 if (typeof doc !== 'object') {
4633 err = new TypeError('Document needs to be an object');
4634 } else if (doc === null) {
4635 err = new TypeError('Object cannot be null');
4636 }
4637
4638 if (err !== null) {
4639 this.emit('error', err);
4640 throw err;
4641 }
4642
4643 // if configured to clone, do so now... otherwise just use same obj reference
4644 var obj = this.cloneObjects ? clone(doc, this.cloneMethod) : doc;
4645
4646 if (typeof obj.meta === 'undefined') {
4647 obj.meta = {
4648 revision: 0,
4649 created: 0
4650 };
4651 }
4652
4653 // if cloning, give user back clone of 'cloned' object with $loki and meta
4654 returnObj = this.cloneObjects ? clone(obj, this.cloneMethod) : obj;
4655
4656 // allow pre-insert to modify actual collection reference even if cloning
4657 if (!bulkInsert) {
4658 this.emit('pre-insert', obj);
4659 }
4660 if (!this.add(obj)) {
4661 return undefined;
4662 }
4663
4664 this.addAutoUpdateObserver(returnObj);
4665 if (!bulkInsert) {
4666 this.emit('insert', returnObj);
4667 }
4668 return returnObj;
4669 };
4670
4671 /**
4672 * Empties the collection.
4673 * @memberof Collection
4674 */
4675 Collection.prototype.clear = function () {
4676 this.data = [];
4677 this.idIndex = [];
4678 this.binaryIndices = {};
4679 this.cachedIndex = null;
4680 this.cachedBinaryIndex = null;
4681 this.cachedData = null;
4682 this.maxId = 0;
4683 this.DynamicViews = [];
4684 this.dirty = true;
4685 };
4686
4687 /**
4688 * Updates an object and notifies collection that the document has changed.
4689 * @param {object} doc - document to update within the collection
4690 * @memberof Collection
4691 */
4692 Collection.prototype.update = function (doc) {
4693 if (Array.isArray(doc)) {
4694 var k = 0,
4695 len = doc.length;
4696 for (k; k < len; k += 1) {
4697 this.update(doc[k]);
4698 }
4699 return;
4700 }
4701
4702 // verify object is a properly formed document
4703 if (!hasOwnProperty.call(doc, '$loki')) {
4704 throw new Error('Trying to update unsynced document. Please save the document first by using insert() or addMany()');
4705 }
4706 try {
4707 this.startTransaction();
4708 var arr = this.get(doc.$loki, true),
4709 oldInternal, // ref to existing obj
4710 newInternal, // ref to new internal obj
4711 position,
4712 self = this;
4713
4714 if (!arr) {
4715 throw new Error('Trying to update a document not in collection.');
4716 }
4717
4718 oldInternal = arr[0]; // -internal- obj ref
4719 position = arr[1]; // position in data array
4720
4721 // if configured to clone, do so now... otherwise just use same obj reference
4722 newInternal = this.cloneObjects ? clone(doc, this.cloneMethod) : doc;
4723
4724 this.emit('pre-update', doc);
4725
4726 Object.keys(this.constraints.unique).forEach(function (key) {
4727 self.constraints.unique[key].update(oldInternal, newInternal);
4728 });
4729
4730 // operate the update
4731 this.data[position] = newInternal;
4732
4733 if (newInternal !== doc) {
4734 this.addAutoUpdateObserver(doc);
4735 }
4736
4737 // now that we can efficiently determine the data[] position of newly added document,
4738 // submit it for all registered DynamicViews to evaluate for inclusion/exclusion
4739 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
4740 this.DynamicViews[idx].evaluateDocument(position, false);
4741 }
4742
4743 var key;
4744 if (this.adaptiveBinaryIndices) {
4745 // for each binary index defined in collection, immediately update rather than flag for lazy rebuild
4746 var bIndices = this.binaryIndices;
4747 for (key in bIndices) {
4748 this.adaptiveBinaryIndexUpdate(position, key);
4749 }
4750 }
4751 else {
4752 this.flagBinaryIndexesDirty();
4753 }
4754
4755 this.idIndex[position] = newInternal.$loki;
4756 //this.flagBinaryIndexesDirty();
4757
4758 this.commit();
4759 this.dirty = true; // for autosave scenarios
4760
4761 this.emit('update', doc, this.cloneObjects ? clone(oldInternal, this.cloneMethod) : null);
4762 return doc;
4763 } catch (err) {
4764 this.rollback();
4765 this.console.error(err.message);
4766 this.emit('error', err);
4767 throw (err); // re-throw error so user does not think it succeeded
4768 }
4769 };
4770
4771 /**
4772 * Add object to collection
4773 */
4774 Collection.prototype.add = function (obj) {
4775 // if parameter isn't object exit with throw
4776 if ('object' !== typeof obj) {
4777 throw new TypeError('Object being added needs to be an object');
4778 }
4779 // if object you are adding already has id column it is either already in the collection
4780 // or the object is carrying its own 'id' property. If it also has a meta property,
4781 // then this is already in collection so throw error, otherwise rename to originalId and continue adding.
4782 if (typeof (obj.$loki) !== 'undefined') {
4783 throw new Error('Document is already in collection, please use update()');
4784 }
4785
4786 /*
4787 * try adding object to collection
4788 */
4789 try {
4790 this.startTransaction();
4791 this.maxId++;
4792
4793 if (isNaN(this.maxId)) {
4794 this.maxId = (this.data[this.data.length - 1].$loki + 1);
4795 }
4796
4797 obj.$loki = this.maxId;
4798 obj.meta.version = 0;
4799
4800 var key, constrUnique = this.constraints.unique;
4801 for (key in constrUnique) {
4802 if (hasOwnProperty.call(constrUnique, key)) {
4803 constrUnique[key].set(obj);
4804 }
4805 }
4806
4807 // add new obj id to idIndex
4808 this.idIndex.push(obj.$loki);
4809
4810 // add the object
4811 this.data.push(obj);
4812
4813 var addedPos = this.data.length - 1;
4814
4815 // now that we can efficiently determine the data[] position of newly added document,
4816 // submit it for all registered DynamicViews to evaluate for inclusion/exclusion
4817 var dvlen = this.DynamicViews.length;
4818 for (var i = 0; i < dvlen; i++) {
4819 this.DynamicViews[i].evaluateDocument(addedPos, true);
4820 }
4821
4822 if (this.adaptiveBinaryIndices) {
4823 // for each binary index defined in collection, immediately update rather than flag for lazy rebuild
4824 var bIndices = this.binaryIndices;
4825 for (key in bIndices) {
4826 this.adaptiveBinaryIndexInsert(addedPos, key);
4827 }
4828 }
4829 else {
4830 this.flagBinaryIndexesDirty();
4831 }
4832
4833 this.commit();
4834 this.dirty = true; // for autosave scenarios
4835
4836 return (this.cloneObjects) ? (clone(obj, this.cloneMethod)) : (obj);
4837 } catch (err) {
4838 this.rollback();
4839 this.console.error(err.message);
4840 this.emit('error', err);
4841 throw (err); // re-throw error so user does not think it succeeded
4842 }
4843 };
4844
4845 /**
4846 * Applies a filter function and passes all results to an update function.
4847 *
4848 * @param {function} filterFunction - filter function whose results will execute update
4849 * @param {function} updateFunction - update function to run against filtered documents
4850 * @memberof Collection
4851 */
4852 Collection.prototype.updateWhere = function(filterFunction, updateFunction) {
4853 var results = this.where(filterFunction),
4854 i = 0,
4855 obj;
4856 try {
4857 for (i; i < results.length; i++) {
4858 obj = updateFunction(results[i]);
4859 this.update(obj);
4860 }
4861
4862 } catch (err) {
4863 this.rollback();
4864 this.console.error(err.message);
4865 }
4866 };
4867
4868 /**
4869 * Remove all documents matching supplied filter function.
4870 * For 'mongo-like' querying you should migrate to [findAndRemove()]{@link Collection#findAndRemove}.
4871 * @param {function|object} query - query object to filter on
4872 * @memberof Collection
4873 */
4874 Collection.prototype.removeWhere = function (query) {
4875 var list;
4876 if (typeof query === 'function') {
4877 list = this.data.filter(query);
4878 this.remove(list);
4879 } else {
4880 this.chain().find(query).remove();
4881 }
4882 };
4883
4884 Collection.prototype.removeDataOnly = function () {
4885 this.remove(this.data.slice());
4886 };
4887
4888 /**
4889 * Remove a document from the collection
4890 * @param {object} doc - document to remove from collection
4891 * @memberof Collection
4892 */
4893 Collection.prototype.remove = function (doc) {
4894 if (typeof doc === 'number') {
4895 doc = this.get(doc);
4896 }
4897
4898 if ('object' !== typeof doc) {
4899 throw new Error('Parameter is not an object');
4900 }
4901 if (Array.isArray(doc)) {
4902 var k = 0,
4903 len = doc.length;
4904 for (k; k < len; k += 1) {
4905 this.remove(doc[k]);
4906 }
4907 return;
4908 }
4909
4910 if (!hasOwnProperty.call(doc, '$loki')) {
4911 throw new Error('Object is not a document stored in the collection');
4912 }
4913
4914 try {
4915 this.startTransaction();
4916 var arr = this.get(doc.$loki, true),
4917 // obj = arr[0],
4918 position = arr[1];
4919 var self = this;
4920 Object.keys(this.constraints.unique).forEach(function (key) {
4921 if (doc[key] !== null && typeof doc[key] !== 'undefined') {
4922 self.constraints.unique[key].remove(doc[key]);
4923 }
4924 });
4925 // now that we can efficiently determine the data[] position of newly added document,
4926 // submit it for all registered DynamicViews to remove
4927 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
4928 this.DynamicViews[idx].removeDocument(position);
4929 }
4930
4931 if (this.adaptiveBinaryIndices) {
4932 // for each binary index defined in collection, immediately update rather than flag for lazy rebuild
4933 var key, bIndices = this.binaryIndices;
4934 for (key in bIndices) {
4935 this.adaptiveBinaryIndexRemove(position, key);
4936 }
4937 }
4938 else {
4939 this.flagBinaryIndexesDirty();
4940 }
4941
4942 this.data.splice(position, 1);
4943 this.removeAutoUpdateObserver(doc);
4944
4945 // remove id from idIndex
4946 this.idIndex.splice(position, 1);
4947
4948 this.commit();
4949 this.dirty = true; // for autosave scenarios
4950 this.emit('delete', arr[0]);
4951 delete doc.$loki;
4952 delete doc.meta;
4953 return doc;
4954
4955 } catch (err) {
4956 this.rollback();
4957 this.console.error(err.message);
4958 this.emit('error', err);
4959 return null;
4960 }
4961 };
4962
4963 /*---------------------+
4964 | Finding methods |
4965 +----------------------*/
4966
4967 /**
4968 * Get by Id - faster than other methods because of the searching algorithm
4969 * @param {int} id - $loki id of document you want to retrieve
4970 * @param {boolean} returnPosition - if 'true' we will return [object, position]
4971 * @returns {(object|array|null)} Object reference if document was found, null if not,
4972 * or an array if 'returnPosition' was passed.
4973 * @memberof Collection
4974 */
4975 Collection.prototype.get = function (id, returnPosition) {
4976 var retpos = returnPosition || false,
4977 data = this.idIndex,
4978 max = data.length - 1,
4979 min = 0,
4980 mid = (min + max) >> 1;
4981
4982 id = typeof id === 'number' ? id : parseInt(id, 10);
4983
4984 if (isNaN(id)) {
4985 throw new TypeError('Passed id is not an integer');
4986 }
4987
4988 while (data[min] < data[max]) {
4989 mid = (min + max) >> 1;
4990
4991 if (data[mid] < id) {
4992 min = mid + 1;
4993 } else {
4994 max = mid;
4995 }
4996 }
4997
4998 if (max === min && data[min] === id) {
4999 if (retpos) {
5000 return [this.data[min], min];
5001 }
5002 return this.data[min];
5003 }
5004 return null;
5005
5006 };
5007
5008 /**
5009 * Perform binary range lookup for the data[dataPosition][binaryIndexName] property value
5010 * Since multiple documents may contain the same value (which the index is sorted on),
5011 * we hone in on range and then linear scan range to find exact index array position.
5012 * @param {int} dataPosition : coll.data array index/position
5013 * @param {string} binaryIndexName : index to search for dataPosition in
5014 */
5015 Collection.prototype.getBinaryIndexPosition = function(dataPosition, binaryIndexName) {
5016 var val = this.data[dataPosition][binaryIndexName];
5017 var index = this.binaryIndices[binaryIndexName].values;
5018
5019 // i think calculateRange can probably be moved to collection
5020 // as it doesn't seem to need resultset. need to verify
5021 //var rs = new Resultset(this, null, null);
5022 var range = this.calculateRange("$eq", binaryIndexName, val);
5023
5024 if (range[0] === 0 && range[1] === -1) {
5025 // uhoh didn't find range
5026 return null;
5027 }
5028
5029 var min = range[0];
5030 var max = range[1];
5031
5032 // narrow down the sub-segment of index values
5033 // where the indexed property value exactly matches our
5034 // value and then linear scan to find exact -index- position
5035 for(var idx = min; idx <= max; idx++) {
5036 if (index[idx] === dataPosition) return idx;
5037 }
5038
5039 // uhoh
5040 return null;
5041 };
5042
5043 /**
5044 * Adaptively insert a selected item to the index.
5045 * @param {int} dataPosition : coll.data array index/position
5046 * @param {string} binaryIndexName : index to search for dataPosition in
5047 */
5048 Collection.prototype.adaptiveBinaryIndexInsert = function(dataPosition, binaryIndexName) {
5049 var index = this.binaryIndices[binaryIndexName].values;
5050 var val = this.data[dataPosition][binaryIndexName];
5051 //var rs = new Resultset(this, null, null);
5052 var idxPos = this.calculateRangeStart(binaryIndexName, val);
5053
5054 // insert new data index into our binary index at the proper sorted location for relevant property calculated by idxPos.
5055 // doing this after adjusting dataPositions so no clash with previous item at that position.
5056 this.binaryIndices[binaryIndexName].values.splice(idxPos, 0, dataPosition);
5057 };
5058
5059 /**
5060 * Adaptively update a selected item within an index.
5061 * @param {int} dataPosition : coll.data array index/position
5062 * @param {string} binaryIndexName : index to search for dataPosition in
5063 */
5064 Collection.prototype.adaptiveBinaryIndexUpdate = function(dataPosition, binaryIndexName) {
5065 // linear scan needed to find old position within index unless we optimize for clone scenarios later
5066 // within (my) node 5.6.0, the following for() loop with strict compare is -much- faster than indexOf()
5067 var idxPos,
5068 index = this.binaryIndices[binaryIndexName].values,
5069 len=index.length;
5070
5071 for(idxPos=0; idxPos < len; idxPos++) {
5072 if (index[idxPos] === dataPosition) break;
5073 }
5074
5075 //var idxPos = this.binaryIndices[binaryIndexName].values.indexOf(dataPosition);
5076 this.binaryIndices[binaryIndexName].values.splice(idxPos, 1);
5077
5078 //this.adaptiveBinaryIndexRemove(dataPosition, binaryIndexName, true);
5079 this.adaptiveBinaryIndexInsert(dataPosition, binaryIndexName);
5080 };
5081
5082 /**
5083 * Adaptively remove a selected item from the index.
5084 * @param {int} dataPosition : coll.data array index/position
5085 * @param {string} binaryIndexName : index to search for dataPosition in
5086 */
5087 Collection.prototype.adaptiveBinaryIndexRemove = function(dataPosition, binaryIndexName, removedFromIndexOnly) {
5088 var idxPos = this.getBinaryIndexPosition(dataPosition, binaryIndexName);
5089 var index = this.binaryIndices[binaryIndexName].values;
5090 var len,
5091 idx;
5092
5093 if (idxPos === null) {
5094 // throw new Error('unable to determine binary index position');
5095 return null;
5096 }
5097
5098 // remove document from index
5099 this.binaryIndices[binaryIndexName].values.splice(idxPos, 1);
5100
5101 // if we passed this optional flag parameter, we are calling from adaptiveBinaryIndexUpdate,
5102 // in which case data positions stay the same.
5103 if (removedFromIndexOnly === true) {
5104 return;
5105 }
5106
5107 // since index stores data array positions, if we remove a document
5108 // we need to adjust array positions -1 for all document positions greater than removed position
5109 len = index.length;
5110 for (idx = 0; idx < len; idx++) {
5111 if (index[idx] > dataPosition) {
5112 index[idx]--;
5113 }
5114 }
5115 };
5116
5117 /**
5118 * Internal method used for index maintenance. Given a prop (index name), and a value
5119 * (which may or may not yet exist) this will find the proper location where it can be added.
5120 */
5121 Collection.prototype.calculateRangeStart = function (prop, val) {
5122 var rcd = this.data;
5123 var index = this.binaryIndices[prop].values;
5124 var min = 0;
5125 var max = index.length - 1;
5126 var mid = 0;
5127
5128 if (index.length === 0) {
5129 return 0;
5130 }
5131
5132 var minVal = rcd[index[min]][prop];
5133 var maxVal = rcd[index[max]][prop];
5134
5135 // hone in on start position of value
5136 while (min < max) {
5137 mid = (min + max) >> 1;
5138
5139 if (ltHelper(rcd[index[mid]][prop], val, false)) {
5140 min = mid + 1;
5141 } else {
5142 max = mid;
5143 }
5144 }
5145
5146 var lbound = min;
5147
5148 if (ltHelper(rcd[index[lbound]][prop], val, false)) {
5149 return lbound+1;
5150 }
5151 else {
5152 return lbound;
5153 }
5154 };
5155
5156 /**
5157 * Internal method used for indexed $between. Given a prop (index name), and a value
5158 * (which may or may not yet exist) this will find the final position of that upper range value.
5159 */
5160 Collection.prototype.calculateRangeEnd = function (prop, val) {
5161 var rcd = this.data;
5162 var index = this.binaryIndices[prop].values;
5163 var min = 0;
5164 var max = index.length - 1;
5165 var mid = 0;
5166
5167 if (index.length === 0) {
5168 return 0;
5169 }
5170
5171 var minVal = rcd[index[min]][prop];
5172 var maxVal = rcd[index[max]][prop];
5173
5174 // hone in on start position of value
5175 while (min < max) {
5176 mid = (min + max) >> 1;
5177
5178 if (ltHelper(val, rcd[index[mid]][prop], false)) {
5179 max = mid;
5180 } else {
5181 min = mid + 1;
5182 }
5183 }
5184
5185 var ubound = max;
5186
5187 if (gtHelper(rcd[index[ubound]][prop], val, false)) {
5188 return ubound-1;
5189 }
5190 else {
5191 return ubound;
5192 }
5193 };
5194
5195 /**
5196 * calculateRange() - Binary Search utility method to find range/segment of values matching criteria.
5197 * this is used for collection.find() and first find filter of resultset/dynview
5198 * slightly different than get() binary search in that get() hones in on 1 value,
5199 * but we have to hone in on many (range)
5200 * @param {string} op - operation, such as $eq
5201 * @param {string} prop - name of property to calculate range for
5202 * @param {object} val - value to use for range calculation.
5203 * @returns {array} [start, end] index array positions
5204 */
5205 Collection.prototype.calculateRange = function (op, prop, val) {
5206 var rcd = this.data;
5207 var index = this.binaryIndices[prop].values;
5208 var min = 0;
5209 var max = index.length - 1;
5210 var mid = 0;
5211
5212 // when no documents are in collection, return empty range condition
5213 if (rcd.length === 0) {
5214 return [0, -1];
5215 }
5216
5217 var minVal = rcd[index[min]][prop];
5218 var maxVal = rcd[index[max]][prop];
5219
5220 // if value falls outside of our range return [0, -1] to designate no results
5221 switch (op) {
5222 case '$eq':
5223 case '$aeq':
5224 if (ltHelper(val, minVal, false) || gtHelper(val, maxVal, false)) {
5225 return [0, -1];
5226 }
5227 break;
5228 case '$dteq':
5229 if (ltHelper(val, minVal, false) || gtHelper(val, maxVal, false)) {
5230 return [0, -1];
5231 }
5232 break;
5233 case '$gt':
5234 if (gtHelper(val, maxVal, true)) {
5235 return [0, -1];
5236 }
5237 break;
5238 case '$gte':
5239 if (gtHelper(val, maxVal, false)) {
5240 return [0, -1];
5241 }
5242 break;
5243 case '$lt':
5244 if (ltHelper(val, minVal, true)) {
5245 return [0, -1];
5246 }
5247 if (ltHelper(maxVal, val, false)) {
5248 return [0, rcd.length - 1];
5249 }
5250 break;
5251 case '$lte':
5252 if (ltHelper(val, minVal, false)) {
5253 return [0, -1];
5254 }
5255 if (ltHelper(maxVal, val, true)) {
5256 return [0, rcd.length - 1];
5257 }
5258 break;
5259 case '$between':
5260 return ([this.calculateRangeStart(prop, val[0]), this.calculateRangeEnd(prop, val[1])]);
5261 case '$in':
5262 var idxset = [],
5263 segResult = [];
5264 // query each value '$eq' operator and merge the seqment results.
5265 for (var j = 0, len = val.length; j < len; j++) {
5266 var seg = this.calculateRange('$eq', prop, val[j]);
5267
5268 for (var i = seg[0]; i <= seg[1]; i++) {
5269 if (idxset[i] === undefined) {
5270 idxset[i] = true;
5271 segResult.push(i);
5272 }
5273 }
5274 }
5275 return segResult;
5276 }
5277
5278 // hone in on start position of value
5279 while (min < max) {
5280 mid = (min + max) >> 1;
5281
5282 if (ltHelper(rcd[index[mid]][prop], val, false)) {
5283 min = mid + 1;
5284 } else {
5285 max = mid;
5286 }
5287 }
5288
5289 var lbound = min;
5290
5291 // do not reset min, as the upper bound cannot be prior to the found low bound
5292 max = index.length - 1;
5293
5294 // hone in on end position of value
5295 while (min < max) {
5296 mid = (min + max) >> 1;
5297
5298 if (ltHelper(val, rcd[index[mid]][prop], false)) {
5299 max = mid;
5300 } else {
5301 min = mid + 1;
5302 }
5303 }
5304
5305 var ubound = max;
5306
5307 var lval = rcd[index[lbound]][prop];
5308 var uval = rcd[index[ubound]][prop];
5309
5310 switch (op) {
5311 case '$eq':
5312 if (lval !== val) {
5313 return [0, -1];
5314 }
5315 if (uval !== val) {
5316 ubound--;
5317 }
5318
5319 return [lbound, ubound];
5320 case '$dteq':
5321 if (lval > val || lval < val) {
5322 return [0, -1];
5323 }
5324 if (uval > val || uval < val) {
5325 ubound--;
5326 }
5327
5328 return [lbound, ubound];
5329
5330
5331 case '$gt':
5332 if (ltHelper(uval, val, true)) {
5333 return [0, -1];
5334 }
5335
5336 return [ubound, rcd.length - 1];
5337
5338 case '$gte':
5339 if (ltHelper(lval, val, false)) {
5340 return [0, -1];
5341 }
5342
5343 return [lbound, rcd.length - 1];
5344
5345 case '$lt':
5346 if (lbound === 0 && ltHelper(lval, val, false)) {
5347 return [0, 0];
5348 }
5349 return [0, lbound - 1];
5350
5351 case '$lte':
5352 if (uval !== val) {
5353 ubound--;
5354 }
5355
5356 if (ubound === 0 && ltHelper(uval, val, false)) {
5357 return [0, 0];
5358 }
5359 return [0, ubound];
5360
5361 default:
5362 return [0, rcd.length - 1];
5363 }
5364 };
5365
5366 /**
5367 * Retrieve doc by Unique index
5368 * @param {string} field - name of uniquely indexed property to use when doing lookup
5369 * @param {value} value - unique value to search for
5370 * @returns {object} document matching the value passed
5371 * @memberof Collection
5372 */
5373 Collection.prototype.by = function (field, value) {
5374 var self;
5375 if (value === undefined) {
5376 self = this;
5377 return function (value) {
5378 return self.by(field, value);
5379 };
5380 }
5381
5382 var result = this.constraints.unique[field].get(value);
5383 if (!this.cloneObjects) {
5384 return result;
5385 } else {
5386 return clone(result, this.cloneMethod);
5387 }
5388 };
5389
5390 /**
5391 * Find one object by index property, by property equal to value
5392 * @param {object} query - query object used to perform search with
5393 * @returns {(object|null)} First matching document, or null if none
5394 * @memberof Collection
5395 */
5396 Collection.prototype.findOne = function (query) {
5397 query = query || {};
5398
5399 // Instantiate Resultset and exec find op passing firstOnly = true param
5400 var result = new Resultset(this, {
5401 queryObj: query,
5402 firstOnly: true
5403 });
5404
5405 if (Array.isArray(result) && result.length === 0) {
5406 return null;
5407 } else {
5408 if (!this.cloneObjects) {
5409 return result;
5410 } else {
5411 return clone(result, this.cloneMethod);
5412 }
5413 }
5414 };
5415
5416 /**
5417 * Chain method, used for beginning a series of chained find() and/or view() operations
5418 * on a collection.
5419 *
5420 * @param {array} transform - Ordered array of transform step objects similar to chain
5421 * @param {object} parameters - Object containing properties representing parameters to substitute
5422 * @returns {Resultset} (this) resultset, or data array if any map or join functions where called
5423 * @memberof Collection
5424 */
5425 Collection.prototype.chain = function (transform, parameters) {
5426 var rs = new Resultset(this);
5427
5428 if (typeof transform === 'undefined') {
5429 return rs;
5430 }
5431
5432 return rs.transform(transform, parameters);
5433 };
5434
5435 /**
5436 * Find method, api is similar to mongodb.
5437 * for more complex queries use [chain()]{@link Collection#chain} or [where()]{@link Collection#where}.
5438 * @example {@tutorial Query Examples}
5439 * @param {object} query - 'mongo-like' query object
5440 * @returns {array} Array of matching documents
5441 * @memberof Collection
5442 */
5443 Collection.prototype.find = function (query) {
5444 if (typeof (query) === 'undefined') {
5445 query = 'getAll';
5446 }
5447
5448 var results = new Resultset(this, {
5449 queryObj: query
5450 });
5451 if (!this.cloneObjects) {
5452 return results;
5453 } else {
5454 return cloneObjectArray(results, this.cloneMethod);
5455 }
5456 };
5457
5458 /**
5459 * Find object by unindexed field by property equal to value,
5460 * simply iterates and returns the first element matching the query
5461 */
5462 Collection.prototype.findOneUnindexed = function (prop, value) {
5463 var i = this.data.length,
5464 doc;
5465 while (i--) {
5466 if (this.data[i][prop] === value) {
5467 doc = this.data[i];
5468 return doc;
5469 }
5470 }
5471 return null;
5472 };
5473
5474 /**
5475 * Transaction methods
5476 */
5477
5478 /** start the transation */
5479 Collection.prototype.startTransaction = function () {
5480 if (this.transactional) {
5481 this.cachedData = clone(this.data, this.cloneMethod);
5482 this.cachedIndex = this.idIndex;
5483 this.cachedBinaryIndex = this.binaryIndices;
5484
5485 // propagate startTransaction to dynamic views
5486 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
5487 this.DynamicViews[idx].startTransaction();
5488 }
5489 }
5490 };
5491
5492 /** commit the transation */
5493 Collection.prototype.commit = function () {
5494 if (this.transactional) {
5495 this.cachedData = null;
5496 this.cachedIndex = null;
5497 this.cachedBinaryIndex = null;
5498
5499 // propagate commit to dynamic views
5500 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
5501 this.DynamicViews[idx].commit();
5502 }
5503 }
5504 };
5505
5506 /** roll back the transation */
5507 Collection.prototype.rollback = function () {
5508 if (this.transactional) {
5509 if (this.cachedData !== null && this.cachedIndex !== null) {
5510 this.data = this.cachedData;
5511 this.idIndex = this.cachedIndex;
5512 this.binaryIndices = this.cachedBinaryIndex;
5513 }
5514
5515 // propagate rollback to dynamic views
5516 for (var idx = 0; idx < this.DynamicViews.length; idx++) {
5517 this.DynamicViews[idx].rollback();
5518 }
5519 }
5520 };
5521
5522 // async executor. This is only to enable callbacks at the end of the execution.
5523 Collection.prototype.async = function (fun, callback) {
5524 setTimeout(function () {
5525 if (typeof fun === 'function') {
5526 fun();
5527 callback();
5528 } else {
5529 throw new TypeError('Argument passed for async execution is not a function');
5530 }
5531 }, 0);
5532 };
5533
5534 /**
5535 * Query the collection by supplying a javascript filter function.
5536 * @example
5537 * var results = coll.where(function(obj) {
5538 * return obj.legs === 8;
5539 * });
5540 *
5541 * @param {function} fun - filter function to run against all collection docs
5542 * @returns {array} all documents which pass your filter function
5543 * @memberof Collection
5544 */
5545 Collection.prototype.where = function (fun) {
5546 var results = new Resultset(this, {
5547 queryFunc: fun
5548 });
5549 if (!this.cloneObjects) {
5550 return results;
5551 } else {
5552 return cloneObjectArray(results, this.cloneMethod);
5553 }
5554 };
5555
5556 /**
5557 * Map Reduce operation
5558 *
5559 * @param {function} mapFunction - function to use as map function
5560 * @param {function} reduceFunction - function to use as reduce function
5561 * @returns {data} The result of your mapReduce operation
5562 * @memberof Collection
5563 */
5564 Collection.prototype.mapReduce = function (mapFunction, reduceFunction) {
5565 try {
5566 return reduceFunction(this.data.map(mapFunction));
5567 } catch (err) {
5568 throw err;
5569 }
5570 };
5571
5572 /**
5573 * Join two collections on specified properties
5574 *
5575 * @param {array} joinData - array of documents to 'join' to this collection
5576 * @param {string} leftJoinProp - property name in collection
5577 * @param {string} rightJoinProp - property name in joinData
5578 * @param {function=} mapFun - (Optional) map function to use
5579 * @returns {Resultset} Result of the mapping operation
5580 * @memberof Collection
5581 */
5582 Collection.prototype.eqJoin = function (joinData, leftJoinProp, rightJoinProp, mapFun) {
5583 // logic in Resultset class
5584 return new Resultset(this).eqJoin(joinData, leftJoinProp, rightJoinProp, mapFun);
5585 };
5586
5587 /* ------ STAGING API -------- */
5588 /**
5589 * stages: a map of uniquely identified 'stages', which hold copies of objects to be
5590 * manipulated without affecting the data in the original collection
5591 */
5592 Collection.prototype.stages = {};
5593
5594 /**
5595 * (Staging API) create a stage and/or retrieve it
5596 * @memberof Collection
5597 */
5598 Collection.prototype.getStage = function (name) {
5599 if (!this.stages[name]) {
5600 this.stages[name] = {};
5601 }
5602 return this.stages[name];
5603 };
5604 /**
5605 * a collection of objects recording the changes applied through a commmitStage
5606 */
5607 Collection.prototype.commitLog = [];
5608
5609 /**
5610 * (Staging API) create a copy of an object and insert it into a stage
5611 * @memberof Collection
5612 */
5613 Collection.prototype.stage = function (stageName, obj) {
5614 var copy = JSON.parse(JSON.stringify(obj));
5615 this.getStage(stageName)[obj.$loki] = copy;
5616 return copy;
5617 };
5618
5619 /**
5620 * (Staging API) re-attach all objects to the original collection, so indexes and views can be rebuilt
5621 * then create a message to be inserted in the commitlog
5622 * @param {string} stageName - name of stage
5623 * @param {string} message
5624 * @memberof Collection
5625 */
5626 Collection.prototype.commitStage = function (stageName, message) {
5627 var stage = this.getStage(stageName),
5628 prop,
5629 timestamp = new Date().getTime();
5630
5631 for (prop in stage) {
5632
5633 this.update(stage[prop]);
5634 this.commitLog.push({
5635 timestamp: timestamp,
5636 message: message,
5637 data: JSON.parse(JSON.stringify(stage[prop]))
5638 });
5639 }
5640 this.stages[stageName] = {};
5641 };
5642
5643 Collection.prototype.no_op = function () {
5644 return;
5645 };
5646
5647 /**
5648 * @memberof Collection
5649 */
5650 Collection.prototype.extract = function (field) {
5651 var i = 0,
5652 len = this.data.length,
5653 isDotNotation = isDeepProperty(field),
5654 result = [];
5655 for (i; i < len; i += 1) {
5656 result.push(deepProperty(this.data[i], field, isDotNotation));
5657 }
5658 return result;
5659 };
5660
5661 /**
5662 * @memberof Collection
5663 */
5664 Collection.prototype.max = function (field) {
5665 return Math.max.apply(null, this.extract(field));
5666 };
5667
5668 /**
5669 * @memberof Collection
5670 */
5671 Collection.prototype.min = function (field) {
5672 return Math.min.apply(null, this.extract(field));
5673 };
5674
5675 /**
5676 * @memberof Collection
5677 */
5678 Collection.prototype.maxRecord = function (field) {
5679 var i = 0,
5680 len = this.data.length,
5681 deep = isDeepProperty(field),
5682 result = {
5683 index: 0,
5684 value: undefined
5685 },
5686 max;
5687
5688 for (i; i < len; i += 1) {
5689 if (max !== undefined) {
5690 if (max < deepProperty(this.data[i], field, deep)) {
5691 max = deepProperty(this.data[i], field, deep);
5692 result.index = this.data[i].$loki;
5693 }
5694 } else {
5695 max = deepProperty(this.data[i], field, deep);
5696 result.index = this.data[i].$loki;
5697 }
5698 }
5699 result.value = max;
5700 return result;
5701 };
5702
5703 /**
5704 * @memberof Collection
5705 */
5706 Collection.prototype.minRecord = function (field) {
5707 var i = 0,
5708 len = this.data.length,
5709 deep = isDeepProperty(field),
5710 result = {
5711 index: 0,
5712 value: undefined
5713 },
5714 min;
5715
5716 for (i; i < len; i += 1) {
5717 if (min !== undefined) {
5718 if (min > deepProperty(this.data[i], field, deep)) {
5719 min = deepProperty(this.data[i], field, deep);
5720 result.index = this.data[i].$loki;
5721 }
5722 } else {
5723 min = deepProperty(this.data[i], field, deep);
5724 result.index = this.data[i].$loki;
5725 }
5726 }
5727 result.value = min;
5728 return result;
5729 };
5730
5731 /**
5732 * @memberof Collection
5733 */
5734 Collection.prototype.extractNumerical = function (field) {
5735 return this.extract(field).map(parseBase10).filter(Number).filter(function (n) {
5736 return !(isNaN(n));
5737 });
5738 };
5739
5740 /**
5741 * Calculates the average numerical value of a property
5742 *
5743 * @param {string} field - name of property in docs to average
5744 * @returns {number} average of property in all docs in the collection
5745 * @memberof Collection
5746 */
5747 Collection.prototype.avg = function (field) {
5748 return average(this.extractNumerical(field));
5749 };
5750
5751 /**
5752 * Calculate standard deviation of a field
5753 * @memberof Collection
5754 * @param {string} field
5755 */
5756 Collection.prototype.stdDev = function (field) {
5757 return standardDeviation(this.extractNumerical(field));
5758 };
5759
5760 /**
5761 * @memberof Collection
5762 * @param {string} field
5763 */
5764 Collection.prototype.mode = function (field) {
5765 var dict = {},
5766 data = this.extract(field);
5767 data.forEach(function (obj) {
5768 if (dict[obj]) {
5769 dict[obj] += 1;
5770 } else {
5771 dict[obj] = 1;
5772 }
5773 });
5774 var max,
5775 prop, mode;
5776 for (prop in dict) {
5777 if (max) {
5778 if (max < dict[prop]) {
5779 mode = prop;
5780 }
5781 } else {
5782 mode = prop;
5783 max = dict[prop];
5784 }
5785 }
5786 return mode;
5787 };
5788
5789 /**
5790 * @memberof Collection
5791 * @param {string} field - property name
5792 */
5793 Collection.prototype.median = function (field) {
5794 var values = this.extractNumerical(field);
5795 values.sort(sub);
5796
5797 var half = Math.floor(values.length / 2);
5798
5799 if (values.length % 2) {
5800 return values[half];
5801 } else {
5802 return (values[half - 1] + values[half]) / 2.0;
5803 }
5804 };
5805
5806 /**
5807 * General utils, including statistical functions
5808 */
5809 function isDeepProperty(field) {
5810 return field.indexOf('.') !== -1;
5811 }
5812
5813 function parseBase10(num) {
5814 return parseFloat(num, 10);
5815 }
5816
5817 function isNotUndefined(obj) {
5818 return obj !== undefined;
5819 }
5820
5821 function add(a, b) {
5822 return a + b;
5823 }
5824
5825 function sub(a, b) {
5826 return a - b;
5827 }
5828
5829 function median(values) {
5830 values.sort(sub);
5831 var half = Math.floor(values.length / 2);
5832 return (values.length % 2) ? values[half] : ((values[half - 1] + values[half]) / 2.0);
5833 }
5834
5835 function average(array) {
5836 return (array.reduce(add, 0)) / array.length;
5837 }
5838
5839 function standardDeviation(values) {
5840 var avg = average(values);
5841 var squareDiffs = values.map(function (value) {
5842 var diff = value - avg;
5843 var sqrDiff = diff * diff;
5844 return sqrDiff;
5845 });
5846
5847 var avgSquareDiff = average(squareDiffs);
5848
5849 var stdDev = Math.sqrt(avgSquareDiff);
5850 return stdDev;
5851 }
5852
5853 function deepProperty(obj, property, isDeep) {
5854 if (isDeep === false) {
5855 // pass without processing
5856 return obj[property];
5857 }
5858 var pieces = property.split('.'),
5859 root = obj;
5860 while (pieces.length > 0) {
5861 root = root[pieces.shift()];
5862 }
5863 return root;
5864 }
5865
5866 function binarySearch(array, item, fun) {
5867 var lo = 0,
5868 hi = array.length,
5869 compared,
5870 mid;
5871 while (lo < hi) {
5872 mid = (lo + hi) >> 1;
5873 compared = fun.apply(null, [item, array[mid]]);
5874 if (compared === 0) {
5875 return {
5876 found: true,
5877 index: mid
5878 };
5879 } else if (compared < 0) {
5880 hi = mid;
5881 } else {
5882 lo = mid + 1;
5883 }
5884 }
5885 return {
5886 found: false,
5887 index: hi
5888 };
5889 }
5890
5891 function BSonSort(fun) {
5892 return function (array, item) {
5893 return binarySearch(array, item, fun);
5894 };
5895 }
5896
5897 function KeyValueStore() {}
5898
5899 KeyValueStore.prototype = {
5900 keys: [],
5901 values: [],
5902 sort: function (a, b) {
5903 return (a < b) ? -1 : ((a > b) ? 1 : 0);
5904 },
5905 setSort: function (fun) {
5906 this.bs = new BSonSort(fun);
5907 },
5908 bs: function () {
5909 return new BSonSort(this.sort);
5910 },
5911 set: function (key, value) {
5912 var pos = this.bs(this.keys, key);
5913 if (pos.found) {
5914 this.values[pos.index] = value;
5915 } else {
5916 this.keys.splice(pos.index, 0, key);
5917 this.values.splice(pos.index, 0, value);
5918 }
5919 },
5920 get: function (key) {
5921 return this.values[binarySearch(this.keys, key, this.sort).index];
5922 }
5923 };
5924
5925 function UniqueIndex(uniqueField) {
5926 this.field = uniqueField;
5927 this.keyMap = {};
5928 this.lokiMap = {};
5929 }
5930 UniqueIndex.prototype.keyMap = {};
5931 UniqueIndex.prototype.lokiMap = {};
5932 UniqueIndex.prototype.set = function (obj) {
5933 var fieldValue = obj[this.field];
5934 if (fieldValue !== null && typeof (fieldValue) !== 'undefined') {
5935 if (this.keyMap[fieldValue]) {
5936 throw new Error('Duplicate key for property ' + this.field + ': ' + fieldValue);
5937 } else {
5938 this.keyMap[fieldValue] = obj;
5939 this.lokiMap[obj.$loki] = fieldValue;
5940 }
5941 }
5942 };
5943 UniqueIndex.prototype.get = function (key) {
5944 return this.keyMap[key];
5945 };
5946
5947 UniqueIndex.prototype.byId = function (id) {
5948 return this.keyMap[this.lokiMap[id]];
5949 };
5950 /**
5951 * Updates a document's unique index given an updated object.
5952 * @param {Object} obj Original document object
5953 * @param {Object} doc New document object (likely the same as obj)
5954 */
5955 UniqueIndex.prototype.update = function (obj, doc) {
5956 if (this.lokiMap[obj.$loki] !== doc[this.field]) {
5957 var old = this.lokiMap[obj.$loki];
5958 this.set(doc);
5959 // make the old key fail bool test, while avoiding the use of delete (mem-leak prone)
5960 this.keyMap[old] = undefined;
5961 } else {
5962 this.keyMap[obj[this.field]] = doc;
5963 }
5964 };
5965 UniqueIndex.prototype.remove = function (key) {
5966 var obj = this.keyMap[key];
5967 if (obj !== null && typeof obj !== 'undefined') {
5968 this.keyMap[key] = undefined;
5969 this.lokiMap[obj.$loki] = undefined;
5970 } else {
5971 throw new Error('Key is not in unique index: ' + this.field);
5972 }
5973 };
5974 UniqueIndex.prototype.clear = function () {
5975 this.keyMap = {};
5976 this.lokiMap = {};
5977 };
5978
5979 function ExactIndex(exactField) {
5980 this.index = {};
5981 this.field = exactField;
5982 }
5983
5984 // add the value you want returned to the key in the index
5985 ExactIndex.prototype = {
5986 set: function add(key, val) {
5987 if (this.index[key]) {
5988 this.index[key].push(val);
5989 } else {
5990 this.index[key] = [val];
5991 }
5992 },
5993
5994 // remove the value from the index, if the value was the last one, remove the key
5995 remove: function remove(key, val) {
5996 var idxSet = this.index[key];
5997 for (var i in idxSet) {
5998 if (idxSet[i] == val) {
5999 idxSet.splice(i, 1);
6000 }
6001 }
6002 if (idxSet.length < 1) {
6003 this.index[key] = undefined;
6004 }
6005 },
6006
6007 // get the values related to the key, could be more than one
6008 get: function get(key) {
6009 return this.index[key];
6010 },
6011
6012 // clear will zap the index
6013 clear: function clear(key) {
6014 this.index = {};
6015 }
6016 };
6017
6018 function SortedIndex(sortedField) {
6019 this.field = sortedField;
6020 }
6021
6022 SortedIndex.prototype = {
6023 keys: [],
6024 values: [],
6025 // set the default sort
6026 sort: function (a, b) {
6027 return (a < b) ? -1 : ((a > b) ? 1 : 0);
6028 },
6029 bs: function () {
6030 return new BSonSort(this.sort);
6031 },
6032 // and allow override of the default sort
6033 setSort: function (fun) {
6034 this.bs = new BSonSort(fun);
6035 },
6036 // add the value you want returned to the key in the index
6037 set: function (key, value) {
6038 var pos = binarySearch(this.keys, key, this.sort);
6039 if (pos.found) {
6040 this.values[pos.index].push(value);
6041 } else {
6042 this.keys.splice(pos.index, 0, key);
6043 this.values.splice(pos.index, 0, [value]);
6044 }
6045 },
6046 // get all values which have a key == the given key
6047 get: function (key) {
6048 var bsr = binarySearch(this.keys, key, this.sort);
6049 if (bsr.found) {
6050 return this.values[bsr.index];
6051 } else {
6052 return [];
6053 }
6054 },
6055 // get all values which have a key < the given key
6056 getLt: function (key) {
6057 var bsr = binarySearch(this.keys, key, this.sort);
6058 var pos = bsr.index;
6059 if (bsr.found) pos--;
6060 return this.getAll(key, 0, pos);
6061 },
6062 // get all values which have a key > the given key
6063 getGt: function (key) {
6064 var bsr = binarySearch(this.keys, key, this.sort);
6065 var pos = bsr.index;
6066 if (bsr.found) pos++;
6067 return this.getAll(key, pos, this.keys.length);
6068 },
6069
6070 // get all vals from start to end
6071 getAll: function (key, start, end) {
6072 var results = [];
6073 for (var i = start; i < end; i++) {
6074 results = results.concat(this.values[i]);
6075 }
6076 return results;
6077 },
6078 // just in case someone wants to do something smart with ranges
6079 getPos: function (key) {
6080 return binarySearch(this.keys, key, this.sort);
6081 },
6082 // remove the value from the index, if the value was the last one, remove the key
6083 remove: function (key, value) {
6084 var pos = binarySearch(this.keys, key, this.sort).index;
6085 var idxSet = this.values[pos];
6086 for (var i in idxSet) {
6087 if (idxSet[i] == value) idxSet.splice(i, 1);
6088 }
6089 if (idxSet.length < 1) {
6090 this.keys.splice(pos, 1);
6091 this.values.splice(pos, 1);
6092 }
6093 },
6094 // clear will zap the index
6095 clear: function () {
6096 this.keys = [];
6097 this.values = [];
6098 }
6099 };
6100
6101
6102 Loki.LokiOps = LokiOps;
6103 Loki.Collection = Collection;
6104 Loki.KeyValueStore = KeyValueStore;
6105 Loki.LokiMemoryAdapter = LokiMemoryAdapter;
6106 Loki.LokiPartitioningAdapter = LokiPartitioningAdapter;
6107 Loki.LokiLocalStorageAdapter = LokiLocalStorageAdapter;
6108 Loki.LokiFsAdapter = LokiFsAdapter;
6109 Loki.persistenceAdapters = {
6110 fs: LokiFsAdapter,
6111 localStorage: LokiLocalStorageAdapter
6112 };
6113 return Loki;
6114 }());
6115
6116}));