@herdwatch/lokijs 1.5.8-dev.7 → 1.5.12-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lokijs.js CHANGED
@@ -22,6 +22,36 @@
22
22
 
23
23
  var hasOwnProperty = Object.prototype.hasOwnProperty;
24
24
 
25
+ function deepFreeze(obj) {
26
+ var prop, i;
27
+ if (Array.isArray(obj)) {
28
+ for (i = 0; i < obj.length; i++) {
29
+ deepFreeze(obj[i]);
30
+ }
31
+ freeze(obj);
32
+ } else if (obj !== null && (typeof obj === 'object')) {
33
+ for (prop in obj) {
34
+ if (obj.hasOwnProperty(prop)) {
35
+ deepFreeze(obj[prop]);
36
+ }
37
+ }
38
+ freeze(obj);
39
+ }
40
+ }
41
+
42
+ function freeze(obj) {
43
+ if (!Object.isFrozen(obj)) {
44
+ Object.freeze(obj);
45
+ }
46
+ }
47
+
48
+ function unFreeze(obj) {
49
+ if (!Object.isFrozen(obj)) {
50
+ return obj;
51
+ }
52
+ return clone(obj, 'shallow');
53
+ }
54
+
25
55
  var Utils = {
26
56
  copyProperties: function (src, dest) {
27
57
  var prop;
@@ -100,12 +130,12 @@
100
130
  return object[path];
101
131
  }
102
132
 
103
- if (typeof(path) === "string") {
133
+ if (typeof (path) === "string") {
104
134
  path = path.split(".");
105
135
  }
106
136
 
107
137
  if (!Array.isArray(path)) {
108
- throw new Error("path must be a string or array. Found " + typeof(path));
138
+ throw new Error("path must be a string or array. Found " + typeof (path));
109
139
  }
110
140
 
111
141
  var index = 0,
@@ -150,7 +180,7 @@
150
180
  case false: t1 = 3; break;
151
181
  case true: t1 = 4; break;
152
182
  case "": t1 = 5; break;
153
- default: t1 = (prop1 === prop1)?9:0; break;
183
+ default: t1 = (prop1 === prop1) ? 9 : 0; break;
154
184
  }
155
185
 
156
186
  switch (prop2) {
@@ -159,12 +189,12 @@
159
189
  case false: t2 = 3; break;
160
190
  case true: t2 = 4; break;
161
191
  case "": t2 = 5; break;
162
- default: t2 = (prop2 === prop2)?9:0; break;
192
+ default: t2 = (prop2 === prop2) ? 9 : 0; break;
163
193
  }
164
194
 
165
195
  // one or both is edge case
166
196
  if (t1 !== 9 || t2 !== 9) {
167
- return (t1===t2);
197
+ return (t1 === t2);
168
198
  }
169
199
  }
170
200
 
@@ -202,7 +232,7 @@
202
232
  case true: t1 = 4; break;
203
233
  case "": t1 = 5; break;
204
234
  // if strict equal probably 0 so sort higher, otherwise probably NaN so sort lower than even null
205
- default: t1 = (prop1 === prop1)?9:0; break;
235
+ default: t1 = (prop1 === prop1) ? 9 : 0; break;
206
236
  }
207
237
 
208
238
  switch (prop2) {
@@ -211,12 +241,12 @@
211
241
  case false: t2 = 3; break;
212
242
  case true: t2 = 4; break;
213
243
  case "": t2 = 5; break;
214
- default: t2 = (prop2 === prop2)?9:0; break;
244
+ default: t2 = (prop2 === prop2) ? 9 : 0; break;
215
245
  }
216
246
 
217
247
  // one or both is edge case
218
248
  if (t1 !== 9 || t2 !== 9) {
219
- return (t1===t2)?equal:(t1<t2);
249
+ return (t1 === t2) ? equal : (t1 < t2);
220
250
  }
221
251
  }
222
252
 
@@ -269,7 +299,7 @@
269
299
  case true: t1 = 4; break;
270
300
  case "": t1 = 5; break;
271
301
  // NaN 0
272
- default: t1 = (prop1 === prop1)?9:0; break;
302
+ default: t1 = (prop1 === prop1) ? 9 : 0; break;
273
303
  }
274
304
 
275
305
  switch (prop2) {
@@ -278,12 +308,12 @@
278
308
  case false: t2 = 3; break;
279
309
  case true: t2 = 4; break;
280
310
  case "": t2 = 5; break;
281
- default: t2 = (prop2 === prop2)?9:0; break;
311
+ default: t2 = (prop2 === prop2) ? 9 : 0; break;
282
312
  }
283
313
 
284
314
  // one or both is edge case
285
315
  if (t1 !== 9 || t2 !== 9) {
286
- return (t1===t2)?equal:(t1>t2);
316
+ return (t1 === t2) ? equal : (t1 > t2);
287
317
  }
288
318
  }
289
319
 
@@ -376,30 +406,31 @@
376
406
  * @param {array} paths - array of properties to drill into
377
407
  * @param {function} fun - evaluation function to test with
378
408
  * @param {any} value - comparative value to also pass to (compare) fun
409
+ * @param {any} extra - extra arg to also pass to compare fun
379
410
  * @param {number} poffset - index of the item in 'paths' to start the sub-scan from
380
411
  */
381
- function dotSubScan(root, paths, fun, value, poffset) {
412
+ function dotSubScan(root, paths, fun, value, extra, poffset) {
382
413
  var pathOffset = poffset || 0;
383
414
  var path = paths[pathOffset];
384
415
 
385
416
  var valueFound = false;
386
417
  var element;
387
- if (typeof root === 'object' && path in root) {
418
+ if (root !== null && typeof root === 'object' && path in root) {
388
419
  element = root[path];
389
420
  }
390
421
  if (pathOffset + 1 >= paths.length) {
391
422
  // if we have already expanded out the dot notation,
392
423
  // then just evaluate the test function and value on the element
393
- valueFound = fun(element, value);
424
+ valueFound = fun(element, value, extra);
394
425
  } else if (Array.isArray(element)) {
395
426
  for (var index = 0, len = element.length; index < len; index += 1) {
396
- valueFound = dotSubScan(element[index], paths, fun, value, pathOffset + 1);
427
+ valueFound = dotSubScan(element[index], paths, fun, value, extra, pathOffset + 1);
397
428
  if (valueFound === true) {
398
429
  break;
399
430
  }
400
431
  }
401
432
  } else {
402
- valueFound = dotSubScan(element, paths, fun, value, pathOffset + 1);
433
+ valueFound = dotSubScan(element, paths, fun, value, extra, pathOffset + 1);
403
434
  }
404
435
 
405
436
  return valueFound;
@@ -418,10 +449,10 @@
418
449
  return null;
419
450
  }
420
451
 
421
- function doQueryOp(val, op) {
452
+ function doQueryOp(val, op, record) {
422
453
  for (var p in op) {
423
454
  if (hasOwnProperty.call(op, p)) {
424
- return LokiOps[p](val, op[p]);
455
+ return LokiOps[p](val, op[p], record);
425
456
  }
426
457
  }
427
458
  return false;
@@ -503,6 +534,10 @@
503
534
  return b.indexOf(a) !== -1;
504
535
  },
505
536
 
537
+ $inSet: function(a, b) {
538
+ return b.has(a);
539
+ },
540
+
506
541
  $nin: function (a, b) {
507
542
  return b.indexOf(a) === -1;
508
543
  },
@@ -553,24 +588,24 @@
553
588
 
554
589
  $elemMatch: function (a, b) {
555
590
  if (Array.isArray(a)) {
556
- return a.some(function(item){
557
- return Object.keys(b).every(function(property) {
591
+ return a.some(function (item) {
592
+ return Object.keys(b).every(function (property) {
558
593
  var filter = b[property];
559
594
  if (!(typeof filter === 'object' && filter)) {
560
595
  filter = { $eq: filter };
561
596
  }
562
597
 
563
598
  if (property.indexOf('.') !== -1) {
564
- return dotSubScan(item, property.split('.'), doQueryOp, b[property]);
599
+ return dotSubScan(item, property.split('.'), doQueryOp, b[property], item);
565
600
  }
566
- return doQueryOp(item[property], filter);
601
+ return doQueryOp(item[property], filter, item);
567
602
  });
568
603
  });
569
604
  }
570
605
  return false;
571
606
  },
572
607
 
573
- $type: function (a, b) {
608
+ $type: function (a, b, record) {
574
609
  var type = typeof a;
575
610
  if (type === 'object') {
576
611
  if (Array.isArray(a)) {
@@ -579,23 +614,23 @@
579
614
  type = 'date';
580
615
  }
581
616
  }
582
- return (typeof b !== 'object') ? (type === b) : doQueryOp(type, b);
617
+ return (typeof b !== 'object') ? (type === b) : doQueryOp(type, b, record);
583
618
  },
584
619
 
585
- $finite: function(a, b) {
620
+ $finite: function (a, b) {
586
621
  return (b === isFinite(a));
587
622
  },
588
623
 
589
- $size: function (a, b) {
624
+ $size: function (a, b, record) {
590
625
  if (Array.isArray(a)) {
591
- return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b);
626
+ return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b, record);
592
627
  }
593
628
  return false;
594
629
  },
595
630
 
596
- $len: function (a, b) {
631
+ $len: function (a, b, record) {
597
632
  if (typeof a === 'string') {
598
- return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b);
633
+ return (typeof b !== 'object') ? (a.length === b) : doQueryOp(a.length, b, record);
599
634
  }
600
635
  return false;
601
636
  },
@@ -608,22 +643,22 @@
608
643
  // a is the value in the collection
609
644
  // b is the nested query operation (for '$not')
610
645
  // or an array of nested query operations (for '$and' and '$or')
611
- $not: function (a, b) {
612
- return !doQueryOp(a, b);
646
+ $not: function (a, b, record) {
647
+ return !doQueryOp(a, b, record);
613
648
  },
614
649
 
615
- $and: function (a, b) {
650
+ $and: function (a, b, record) {
616
651
  for (var idx = 0, len = b.length; idx < len; idx += 1) {
617
- if (!doQueryOp(a, b[idx])) {
652
+ if (!doQueryOp(a, b[idx], record)) {
618
653
  return false;
619
654
  }
620
655
  }
621
656
  return true;
622
657
  },
623
658
 
624
- $or: function (a, b) {
659
+ $or: function (a, b, record) {
625
660
  for (var idx = 0, len = b.length; idx < len; idx += 1) {
626
- if (doQueryOp(a, b[idx])) {
661
+ if (doQueryOp(a, b[idx], record)) {
627
662
  return true;
628
663
  }
629
664
  }
@@ -639,6 +674,21 @@
639
674
  }
640
675
  };
641
676
 
677
+ // ops that can be used with { $$op: 'column-name' } syntax
678
+ var valueLevelOps = ['$eq', '$aeq', '$ne', '$dteq', '$gt', '$gte', '$lt', '$lte', '$jgt', '$jgte', '$jlt', '$jlte', '$type'];
679
+ valueLevelOps.forEach(function (op) {
680
+ var fun = LokiOps[op];
681
+ LokiOps['$' + op] = function (a, spec, record) {
682
+ if (typeof spec === 'string') {
683
+ return fun(a, record[spec]);
684
+ } else if (typeof spec === 'function') {
685
+ return fun(a, spec(record));
686
+ } else {
687
+ throw new Error('Invalid argument to $$ matcher');
688
+ }
689
+ };
690
+ });
691
+
642
692
  // if an op is registered in this object, our 'calculateRange' can use it with our binary indices.
643
693
  // if the op is registered to a function, we will run that function/op as a 2nd pass filter on results.
644
694
  // those 2nd pass filter functions should be similar to LokiOps functions, accepting 2 vals to compare.
@@ -663,39 +713,39 @@
663
713
  cloned;
664
714
 
665
715
  switch (cloneMethod) {
666
- case "parse-stringify":
667
- cloned = JSON.parse(JSON.stringify(data));
668
- break;
669
- case "jquery-extend-deep":
670
- cloned = jQuery.extend(true, {}, data);
671
- break;
672
- case "shallow":
673
- // more compatible method for older browsers
674
- cloned = Object.create(data.constructor.prototype);
675
- Object.keys(data).map(function (i) {
676
- cloned[i] = data[i];
677
- });
678
- break;
679
- case "shallow-assign":
680
- // should be supported by newer environments/browsers
681
- cloned = Object.create(data.constructor.prototype);
682
- Object.assign(cloned, data);
683
- break;
684
- case "shallow-recurse-objects":
685
- // shallow clone top level properties
686
- cloned = clone(data, "shallow");
687
- var keys = Object.keys(data);
688
- // for each of the top level properties which are object literals, recursively shallow copy
689
- keys.forEach(function(key) {
690
- if (typeof data[key] === "object" && data[key].constructor.name === "Object") {
691
- cloned[key] = clone(data[key], "shallow-recurse-objects");
692
- }else if(Array.isArray(data[key])){
693
- cloned[key] = cloneObjectArray(data[key], "shallow-recurse-objects");
694
- }
695
- });
696
- break;
697
- default:
698
- break;
716
+ case "parse-stringify":
717
+ cloned = JSON.parse(JSON.stringify(data));
718
+ break;
719
+ case "jquery-extend-deep":
720
+ cloned = jQuery.extend(true, {}, data);
721
+ break;
722
+ case "shallow":
723
+ // more compatible method for older browsers
724
+ cloned = Object.create(data.constructor.prototype);
725
+ Object.keys(data).map(function (i) {
726
+ cloned[i] = data[i];
727
+ });
728
+ break;
729
+ case "shallow-assign":
730
+ // should be supported by newer environments/browsers
731
+ cloned = Object.create(data.constructor.prototype);
732
+ Object.assign(cloned, data);
733
+ break;
734
+ case "shallow-recurse-objects":
735
+ // shallow clone top level properties
736
+ cloned = clone(data, "shallow");
737
+ var keys = Object.keys(data);
738
+ // for each of the top level properties which are object literals, recursively shallow copy
739
+ keys.forEach(function (key) {
740
+ if (typeof data[key] === "object" && data[key].constructor.name === "Object") {
741
+ cloned[key] = clone(data[key], "shallow-recurse-objects");
742
+ } else if (Array.isArray(data[key])) {
743
+ cloned[key] = cloneObjectArray(data[key], "shallow-recurse-objects");
744
+ }
745
+ });
746
+ break;
747
+ default:
748
+ break;
699
749
  }
700
750
 
701
751
  return cloned;
@@ -728,7 +778,7 @@
728
778
  *
729
779
  * @constructor LokiEventEmitter
730
780
  */
731
- function LokiEventEmitter() {}
781
+ function LokiEventEmitter() { }
732
782
 
733
783
  /**
734
784
  * @prop {hashmap} events - a hashmap, with each property being an array of callbacks
@@ -756,7 +806,7 @@
756
806
  var self = this;
757
807
 
758
808
  if (Array.isArray(eventName)) {
759
- eventName.forEach(function(currentEventName) {
809
+ eventName.forEach(function (currentEventName) {
760
810
  self.on(currentEventName, listener);
761
811
  });
762
812
  return listener;
@@ -824,7 +874,7 @@
824
874
  var self = this;
825
875
 
826
876
  if (Array.isArray(eventName)) {
827
- eventName.forEach(function(currentEventName) {
877
+ eventName.forEach(function (currentEventName) {
828
878
  self.removeListener(currentEventName, listener);
829
879
  });
830
880
 
@@ -903,8 +953,8 @@
903
953
 
904
954
  var getENV = function () {
905
955
  if (typeof global !== 'undefined' && (global.android || global.NSObject)) {
906
- // If no adapter assume nativescript which needs adapter to be passed manually
907
- return 'NATIVESCRIPT'; //nativescript
956
+ // If no adapter assume nativescript which needs adapter to be passed manually
957
+ return 'NATIVESCRIPT'; //nativescript
908
958
  }
909
959
 
910
960
  if (typeof window === 'undefined') {
@@ -980,11 +1030,11 @@
980
1030
  */
981
1031
  Loki.prototype.configureOptions = function (options, initialConfig) {
982
1032
  var defaultPersistence = {
983
- 'NODEJS': 'fs',
984
- 'BROWSER': 'localStorage',
985
- 'CORDOVA': 'localStorage',
986
- 'MEMORY': 'memory'
987
- },
1033
+ 'NODEJS': 'fs',
1034
+ 'BROWSER': 'localStorage',
1035
+ 'CORDOVA': 'localStorage',
1036
+ 'MEMORY': 'memory'
1037
+ },
988
1038
  persistenceMethods = {
989
1039
  'fs': LokiFsAdapter,
990
1040
  'localStorage': LokiLocalStorageAdapter,
@@ -1079,7 +1129,7 @@
1079
1129
  * @param {bool} options.removeNonSerializable - nulls properties not safe for serialization.
1080
1130
  * @memberof Loki
1081
1131
  */
1082
- Loki.prototype.copy = function(options) {
1132
+ Loki.prototype.copy = function (options) {
1083
1133
  // in case running in an environment without accurate environment detection, pass 'NA'
1084
1134
  var databaseCopy = new Loki(this.filename, { env: "NA" });
1085
1135
  var clen, idx;
@@ -1090,12 +1140,12 @@
1090
1140
  databaseCopy.loadJSONObject(this, { retainDirtyFlags: true });
1091
1141
 
1092
1142
  // since our JSON serializeReplacer is not invoked for reference database adapters, this will let us mimic
1093
- if(options.hasOwnProperty("removeNonSerializable") && options.removeNonSerializable === true) {
1143
+ if (options.hasOwnProperty("removeNonSerializable") && options.removeNonSerializable === true) {
1094
1144
  databaseCopy.autosaveHandle = null;
1095
1145
  databaseCopy.persistenceAdapter = null;
1096
1146
 
1097
1147
  clen = databaseCopy.collections.length;
1098
- for (idx=0; idx<clen; idx++) {
1148
+ for (idx = 0; idx < clen; idx++) {
1099
1149
  databaseCopy.collections[idx].constraints = null;
1100
1150
  databaseCopy.collections[idx].ttl = null;
1101
1151
  }
@@ -1254,18 +1304,18 @@
1254
1304
  */
1255
1305
  Loki.prototype.serializeReplacer = function (key, value) {
1256
1306
  switch (key) {
1257
- case 'autosaveHandle':
1258
- case 'persistenceAdapter':
1259
- case 'constraints':
1260
- case 'ttl':
1261
- return null;
1262
- case 'throttledSavePending':
1263
- case 'throttledCallbacks':
1264
- return undefined;
1265
- case 'lokiConsoleWrapper':
1266
- return null;
1267
- default:
1268
- return value;
1307
+ case 'autosaveHandle':
1308
+ case 'persistenceAdapter':
1309
+ case 'constraints':
1310
+ case 'ttl':
1311
+ return null;
1312
+ case 'throttledSavePending':
1313
+ case 'throttledCallbacks':
1314
+ return undefined;
1315
+ case 'lokiConsoleWrapper':
1316
+ return null;
1317
+ default:
1318
+ return value;
1269
1319
  }
1270
1320
  };
1271
1321
 
@@ -1282,7 +1332,7 @@
1282
1332
  options.serializationMethod = this.options.serializationMethod;
1283
1333
  }
1284
1334
 
1285
- switch(options.serializationMethod) {
1335
+ switch (options.serializationMethod) {
1286
1336
  case "normal": return JSON.stringify(this, this.serializeReplacer);
1287
1337
  case "pretty": return JSON.stringify(this, this.serializeReplacer, 2);
1288
1338
  case "destructured": return this.serializeDestructured(); // use default options
@@ -1308,7 +1358,7 @@
1308
1358
  * @returns {string|array} A custom, restructured aggregation of independent serializations.
1309
1359
  * @memberof Loki
1310
1360
  */
1311
- Loki.prototype.serializeDestructured = function(options) {
1361
+ Loki.prototype.serializeDestructured = function (options) {
1312
1362
  var idx, sidx, result, resultlen;
1313
1363
  var reconstruct = [];
1314
1364
  var dbcopy;
@@ -1340,7 +1390,7 @@
1340
1390
  dbcopy = new Loki(this.filename);
1341
1391
  dbcopy.loadJSONObject(this);
1342
1392
 
1343
- for(idx=0; idx < dbcopy.collections.length; idx++) {
1393
+ for (idx = 0; idx < dbcopy.collections.length; idx++) {
1344
1394
  dbcopy.collections[idx].data = [];
1345
1395
  }
1346
1396
 
@@ -1355,13 +1405,13 @@
1355
1405
  // at this point we must be deconstructing the entire database
1356
1406
  // start by pushing db serialization into first array element
1357
1407
  reconstruct.push(dbcopy.serialize({
1358
- serializationMethod: "normal"
1408
+ serializationMethod: "normal"
1359
1409
  }));
1360
1410
 
1361
1411
  dbcopy = null;
1362
1412
 
1363
1413
  // push collection data into subsequent elements
1364
- for(idx=0; idx < this.collections.length; idx++) {
1414
+ for (idx = 0; idx < this.collections.length; idx++) {
1365
1415
  result = this.serializeCollection({
1366
1416
  delimited: options.delimited,
1367
1417
  delimiter: options.delimiter,
@@ -1379,7 +1429,7 @@
1379
1429
  // Hopefully this will allow g.c. to reduce memory pressure, if needed.
1380
1430
  resultlen = result.length;
1381
1431
 
1382
- for (sidx=0; sidx < resultlen; sidx++) {
1432
+ for (sidx = 0; sidx < resultlen; sidx++) {
1383
1433
  reconstruct.push(result[sidx]);
1384
1434
  result[sidx] = null;
1385
1435
  }
@@ -1441,7 +1491,7 @@
1441
1491
  * @returns {string|array} A custom, restructured aggregation of independent serializations for a single collection.
1442
1492
  * @memberof Loki
1443
1493
  */
1444
- Loki.prototype.serializeCollection = function(options) {
1494
+ Loki.prototype.serializeCollection = function (options) {
1445
1495
  var doccount,
1446
1496
  docidx,
1447
1497
  resultlines = [];
@@ -1460,13 +1510,13 @@
1460
1510
 
1461
1511
  resultlines = [];
1462
1512
 
1463
- for(docidx=0; docidx<doccount; docidx++) {
1513
+ for (docidx = 0; docidx < doccount; docidx++) {
1464
1514
  resultlines.push(JSON.stringify(this.collections[options.collectionIndex].data[docidx]));
1465
1515
  }
1466
1516
 
1467
1517
  // D and DA
1468
1518
  if (options.delimited) {
1469
- // indicate no more documents in collection (via empty delimited string)
1519
+ // indicate no more documents in collection (via empty delimited string)
1470
1520
  resultlines.push("");
1471
1521
 
1472
1522
  return resultlines.join(options.delimiter);
@@ -1493,10 +1543,10 @@
1493
1543
  * @returns {object|array} An object representation of the deserialized database, not yet applied to 'this' db or document array
1494
1544
  * @memberof Loki
1495
1545
  */
1496
- Loki.prototype.deserializeDestructured = function(destructuredSource, options) {
1497
- var workarray=[];
1546
+ Loki.prototype.deserializeDestructured = function (destructuredSource, options) {
1547
+ var workarray = [];
1498
1548
  var len, cdb;
1499
- var idx, collIndex=0, collCount, lineIndex=1, done=false;
1549
+ var idx, collIndex = 0, collCount, lineIndex = 1, done = false;
1500
1550
  var currLine, currObject;
1501
1551
 
1502
1552
  options = options || {};
@@ -1528,15 +1578,15 @@
1528
1578
  }
1529
1579
 
1530
1580
  // single collection, return doc array
1531
- return this.deserializeCollection(destructuredSource[options.partition+1], options);
1581
+ return this.deserializeCollection(destructuredSource[options.partition + 1], options);
1532
1582
  }
1533
1583
 
1534
1584
  // Otherwise we are restoring an entire partitioned db
1535
1585
  cdb = JSON.parse(destructuredSource[0]);
1536
1586
  collCount = cdb.collections.length;
1537
- for(collIndex=0; collIndex<collCount; collIndex++) {
1587
+ for (collIndex = 0; collIndex < collCount; collIndex++) {
1538
1588
  // attach each collection docarray to container collection data, add 1 to collection array index since db is at 0
1539
- cdb.collections[collIndex].data = this.deserializeCollection(destructuredSource[collIndex+1], options);
1589
+ cdb.collections[collIndex].data = this.deserializeCollection(destructuredSource[collIndex + 1], options);
1540
1590
  }
1541
1591
 
1542
1592
  return cdb;
@@ -1599,8 +1649,8 @@
1599
1649
  * @returns {array} an array of documents to attach to collection.data.
1600
1650
  * @memberof Loki
1601
1651
  */
1602
- Loki.prototype.deserializeCollection = function(destructuredSource, options) {
1603
- var workarray=[];
1652
+ Loki.prototype.deserializeCollection = function (destructuredSource, options) {
1653
+ var workarray = [];
1604
1654
  var idx, len;
1605
1655
 
1606
1656
  options = options || {};
@@ -1626,7 +1676,7 @@
1626
1676
  }
1627
1677
 
1628
1678
  len = workarray.length;
1629
- for (idx=0; idx < len; idx++) {
1679
+ for (idx = 0; idx < len; idx++) {
1630
1680
  workarray[idx] = JSON.parse(workarray[idx]);
1631
1681
  }
1632
1682
 
@@ -1646,12 +1696,13 @@
1646
1696
  if (serializedDb.length === 0) {
1647
1697
  dbObject = {};
1648
1698
  } else {
1699
+
1649
1700
  // using option defined in instantiated db not what was in serialized db
1650
1701
  switch (this.options.serializationMethod) {
1651
1702
  case "normal":
1652
1703
  case "pretty": dbObject = JSON.parse(serializedDb); break;
1653
1704
  case "destructured": dbObject = this.deserializeDestructured(serializedDb); break;
1654
- default: dbObject = JSON.parse(serializedDb); break;
1705
+ default: dbObject = JSON.parse(serializedDb); break;
1655
1706
  }
1656
1707
  }
1657
1708
 
@@ -1689,11 +1740,11 @@
1689
1740
  var collOptions = options[coll.name];
1690
1741
  var inflater;
1691
1742
 
1692
- if(collOptions.proto) {
1743
+ if (collOptions.proto) {
1693
1744
  inflater = collOptions.inflate || Utils.copyProperties;
1694
1745
 
1695
- return function(data) {
1696
- var collObj = new(collOptions.proto)();
1746
+ return function (data) {
1747
+ var collObj = new (collOptions.proto)();
1697
1748
  inflater(data, collObj);
1698
1749
  return collObj;
1699
1750
  };
@@ -1708,10 +1759,11 @@
1708
1759
  copyColl = this.addCollection(coll.name, {
1709
1760
  disableChangesApi: coll.disableChangesApi,
1710
1761
  disableDeltaChangesApi: coll.disableDeltaChangesApi,
1711
- disableMeta: coll.disableMeta
1762
+ disableMeta: coll.disableMeta,
1763
+ disableFreeze: coll.hasOwnProperty('disableFreeze') ? coll.disableFreeze : true
1712
1764
  });
1713
1765
 
1714
- copyColl.adaptiveBinaryIndices = coll.hasOwnProperty('adaptiveBinaryIndices')?(coll.adaptiveBinaryIndices === true): false;
1766
+ copyColl.adaptiveBinaryIndices = coll.hasOwnProperty('adaptiveBinaryIndices') ? (coll.adaptiveBinaryIndices === true) : false;
1715
1767
  copyColl.transactional = coll.transactional;
1716
1768
  copyColl.asyncListeners = coll.asyncListeners;
1717
1769
  copyColl.cloneObjects = coll.cloneObjects;
@@ -1727,27 +1779,48 @@
1727
1779
  copyColl.dirty = false;
1728
1780
  }
1729
1781
 
1730
- // load each element individually
1731
- clen = coll.data.length;
1732
- j = 0;
1733
- if (options && options.hasOwnProperty(coll.name)) {
1734
- loader = makeLoader(coll);
1735
-
1736
- for (j; j < clen; j++) {
1737
- collObj = loader(coll.data[j]);
1738
- copyColl.data[j] = collObj;
1739
- copyColl.addAutoUpdateObserver(collObj);
1782
+ if (coll.getData) {
1783
+ if ((options && options.hasOwnProperty(coll.name)) || !copyColl.disableFreeze || copyColl.autoupdate) {
1784
+ throw new Error("this collection cannot be loaded lazily: " + coll.name);
1740
1785
  }
1786
+ copyColl.getData = coll.getData;
1787
+ Object.defineProperty(copyColl, 'data', {
1788
+ /* jshint loopfunc:true */
1789
+ get: function() {
1790
+ var data = this.getData();
1791
+ this.getData = null;
1792
+ Object.defineProperty(this, 'data', { value: data });
1793
+ return data;
1794
+ }
1795
+ /* jshint loopfunc:false */
1796
+ });
1741
1797
  } else {
1742
-
1743
- for (j; j < clen; j++) {
1744
- copyColl.data[j] = coll.data[j];
1745
- copyColl.addAutoUpdateObserver(copyColl.data[j]);
1798
+ // load each element individually
1799
+ clen = coll.data.length;
1800
+ j = 0;
1801
+ if (options && options.hasOwnProperty(coll.name)) {
1802
+ loader = makeLoader(coll);
1803
+
1804
+ for (j; j < clen; j++) {
1805
+ collObj = loader(coll.data[j]);
1806
+ copyColl.data[j] = collObj;
1807
+ copyColl.addAutoUpdateObserver(collObj);
1808
+ if (!copyColl.disableFreeze) {
1809
+ deepFreeze(copyColl.data[j]);
1810
+ }
1811
+ }
1812
+ } else {
1813
+ for (j; j < clen; j++) {
1814
+ copyColl.data[j] = coll.data[j];
1815
+ copyColl.addAutoUpdateObserver(copyColl.data[j]);
1816
+ if (!copyColl.disableFreeze) {
1817
+ deepFreeze(copyColl.data[j]);
1818
+ }
1819
+ }
1746
1820
  }
1747
1821
  }
1748
1822
 
1749
1823
  copyColl.maxId = (typeof coll.maxId === 'undefined') ? 0 : coll.maxId;
1750
- copyColl.idIndex = coll.idIndex;
1751
1824
  if (typeof (coll.binaryIndices) !== 'undefined') {
1752
1825
  copyColl.binaryIndices = coll.binaryIndices;
1753
1826
  }
@@ -1755,15 +1828,10 @@
1755
1828
  copyColl.transforms = coll.transforms;
1756
1829
  }
1757
1830
 
1758
- copyColl.ensureId();
1759
-
1760
1831
  // regenerate unique indexes
1761
1832
  copyColl.uniqueNames = [];
1762
1833
  if (coll.hasOwnProperty("uniqueNames")) {
1763
1834
  copyColl.uniqueNames = coll.uniqueNames;
1764
- for (j = 0; j < copyColl.uniqueNames.length; j++) {
1765
- copyColl.ensureUniqueIndex(copyColl.uniqueNames[j]);
1766
- }
1767
1835
  }
1768
1836
 
1769
1837
  // in case they are loading a database created before we added dynamic views, handle undefined
@@ -1777,11 +1845,18 @@
1777
1845
  dv.resultdata = colldv.resultdata;
1778
1846
  dv.resultsdirty = colldv.resultsdirty;
1779
1847
  dv.filterPipeline = colldv.filterPipeline;
1780
-
1848
+ dv.sortCriteriaSimple = colldv.sortCriteriaSimple;
1781
1849
  dv.sortCriteria = colldv.sortCriteria;
1782
1850
  dv.sortFunction = null;
1783
-
1784
1851
  dv.sortDirty = colldv.sortDirty;
1852
+ if (!copyColl.disableFreeze) {
1853
+ deepFreeze(dv.filterPipeline);
1854
+ if (dv.sortCriteriaSimple) {
1855
+ deepFreeze(dv.sortCriteriaSimple);
1856
+ } else if (dv.sortCriteria) {
1857
+ deepFreeze(dv.sortCriteria);
1858
+ }
1859
+ }
1785
1860
  dv.resultset.filteredrows = colldv.resultset.filteredrows;
1786
1861
  dv.resultset.filterInitialized = colldv.resultset.filterInitialized;
1787
1862
 
@@ -1792,9 +1867,9 @@
1792
1867
 
1793
1868
  // Upgrade Logic for binary index refactoring at version 1.5
1794
1869
  if (dbObject.databaseVersion < 1.5) {
1795
- // rebuild all indices
1796
- copyColl.ensureAllIndexes(true);
1797
- copyColl.dirty = true;
1870
+ // rebuild all indices
1871
+ copyColl.ensureAllIndexes(true);
1872
+ copyColl.dirty = true;
1798
1873
  }
1799
1874
  }
1800
1875
  };
@@ -1919,16 +1994,16 @@
1919
1994
  * @memberof LokiMemoryAdapter
1920
1995
  */
1921
1996
  LokiMemoryAdapter.prototype.loadDatabase = function (dbname, callback) {
1922
- var self=this;
1997
+ var self = this;
1923
1998
 
1924
1999
  if (this.options.asyncResponses) {
1925
- setTimeout(function() {
2000
+ setTimeout(function () {
1926
2001
  if (self.hashStore.hasOwnProperty(dbname)) {
1927
2002
  callback(self.hashStore[dbname].value);
1928
2003
  }
1929
2004
  else {
1930
2005
  // database doesn't exist, return falsy
1931
- callback (null);
2006
+ callback(null);
1932
2007
  }
1933
2008
  }, this.options.asyncTimeout);
1934
2009
  }
@@ -1938,7 +2013,7 @@
1938
2013
  callback(this.hashStore[dbname].value);
1939
2014
  }
1940
2015
  else {
1941
- callback (null);
2016
+ callback(null);
1942
2017
  }
1943
2018
  }
1944
2019
  };
@@ -1952,15 +2027,15 @@
1952
2027
  * @memberof LokiMemoryAdapter
1953
2028
  */
1954
2029
  LokiMemoryAdapter.prototype.saveDatabase = function (dbname, dbstring, callback) {
1955
- var self=this;
2030
+ var self = this;
1956
2031
  var saveCount;
1957
2032
 
1958
2033
  if (this.options.asyncResponses) {
1959
- setTimeout(function() {
1960
- saveCount = (self.hashStore.hasOwnProperty(dbname)?self.hashStore[dbname].savecount:0);
2034
+ setTimeout(function () {
2035
+ saveCount = (self.hashStore.hasOwnProperty(dbname) ? self.hashStore[dbname].savecount : 0);
1961
2036
 
1962
2037
  self.hashStore[dbname] = {
1963
- savecount: saveCount+1,
2038
+ savecount: saveCount + 1,
1964
2039
  lastsave: new Date(),
1965
2040
  value: dbstring
1966
2041
  };
@@ -1969,10 +2044,10 @@
1969
2044
  }, this.options.asyncTimeout);
1970
2045
  }
1971
2046
  else {
1972
- saveCount = (this.hashStore.hasOwnProperty(dbname)?this.hashStore[dbname].savecount:0);
2047
+ saveCount = (this.hashStore.hasOwnProperty(dbname) ? this.hashStore[dbname].savecount : 0);
1973
2048
 
1974
2049
  this.hashStore[dbname] = {
1975
- savecount: saveCount+1,
2050
+ savecount: saveCount + 1,
1976
2051
  lastsave: new Date(),
1977
2052
  value: dbstring
1978
2053
  };
@@ -1988,7 +2063,7 @@
1988
2063
  * @param {function} callback - function to call when done
1989
2064
  * @memberof LokiMemoryAdapter
1990
2065
  */
1991
- LokiMemoryAdapter.prototype.deleteDatabase = function(dbname, callback) {
2066
+ LokiMemoryAdapter.prototype.deleteDatabase = function (dbname, callback) {
1992
2067
  if (this.hashStore.hasOwnProperty(dbname)) {
1993
2068
  delete this.hashStore[dbname];
1994
2069
  }
@@ -2043,7 +2118,7 @@
2043
2118
 
2044
2119
  // default to page size of 25 megs (can be up to your largest serialized object size larger than this)
2045
2120
  if (!this.options.hasOwnProperty("pageSize")) {
2046
- this.options.pageSize = 25*1024*1024;
2121
+ this.options.pageSize = 25 * 1024 * 1024;
2047
2122
  }
2048
2123
 
2049
2124
  if (!this.options.hasOwnProperty("delimiter")) {
@@ -2060,12 +2135,12 @@
2060
2135
  * @memberof LokiPartitioningAdapter
2061
2136
  */
2062
2137
  LokiPartitioningAdapter.prototype.loadDatabase = function (dbname, callback) {
2063
- var self=this;
2138
+ var self = this;
2064
2139
  this.dbname = dbname;
2065
2140
  this.dbref = new Loki(dbname);
2066
2141
 
2067
2142
  // load the db container (without data)
2068
- this.adapter.loadDatabase(dbname, function(result) {
2143
+ this.adapter.loadDatabase(dbname, function (result) {
2069
2144
  // empty database condition is for inner adapter return null/undefined/falsy
2070
2145
  if (!result) {
2071
2146
  // partition 0 not found so new database, no need to try to load other partitions.
@@ -2095,7 +2170,7 @@
2095
2170
  pageIndex: 0
2096
2171
  };
2097
2172
 
2098
- self.loadNextPartition(0, function() {
2173
+ self.loadNextPartition(0, function () {
2099
2174
  callback(self.dbref);
2100
2175
  });
2101
2176
  });
@@ -2107,9 +2182,9 @@
2107
2182
  * @param {int} partition - ordinal collection position to load next
2108
2183
  * @param {function} callback - adapter callback to return load result to caller
2109
2184
  */
2110
- LokiPartitioningAdapter.prototype.loadNextPartition = function(partition, callback) {
2185
+ LokiPartitioningAdapter.prototype.loadNextPartition = function (partition, callback) {
2111
2186
  var keyname = this.dbname + "." + partition;
2112
- var self=this;
2187
+ var self = this;
2113
2188
 
2114
2189
  if (this.options.paging === true) {
2115
2190
  this.pageIterator.pageIndex = 0;
@@ -2117,7 +2192,7 @@
2117
2192
  return;
2118
2193
  }
2119
2194
 
2120
- this.adapter.loadDatabase(keyname, function(result) {
2195
+ this.adapter.loadDatabase(keyname, function (result) {
2121
2196
  var data = self.dbref.deserializeCollection(result, { delimited: true, collectionIndex: partition });
2122
2197
  self.dbref.collections[partition].data = data;
2123
2198
 
@@ -2135,32 +2210,32 @@
2135
2210
  *
2136
2211
  * @param {function} callback - adapter callback to return load result to caller
2137
2212
  */
2138
- LokiPartitioningAdapter.prototype.loadNextPage = function(callback) {
2213
+ LokiPartitioningAdapter.prototype.loadNextPage = function (callback) {
2139
2214
  // calculate name for next saved page in sequence
2140
2215
  var keyname = this.dbname + "." + this.pageIterator.collection + "." + this.pageIterator.pageIndex;
2141
- var self=this;
2216
+ var self = this;
2142
2217
 
2143
2218
  // load whatever page is next in sequence
2144
- this.adapter.loadDatabase(keyname, function(result) {
2219
+ this.adapter.loadDatabase(keyname, function (result) {
2145
2220
  var data = result.split(self.options.delimiter);
2146
2221
  result = ""; // free up memory now that we have split it into array
2147
2222
  var dlen = data.length;
2148
2223
  var idx;
2149
2224
 
2150
2225
  // detect if last page by presence of final empty string element and remove it if so
2151
- var isLastPage = (data[dlen-1] === "");
2226
+ var isLastPage = (data[dlen - 1] === "");
2152
2227
  if (isLastPage) {
2153
2228
  data.pop();
2154
2229
  dlen = data.length;
2155
2230
  // empty collections are just a delimiter meaning two blank items
2156
- if (data[dlen-1] === "" && dlen === 1) {
2231
+ if (data[dlen - 1] === "" && dlen === 1) {
2157
2232
  data.pop();
2158
2233
  dlen = data.length;
2159
2234
  }
2160
2235
  }
2161
2236
 
2162
2237
  // convert stringified array elements to object instances and push to collection data
2163
- for(idx=0; idx < dlen; idx++) {
2238
+ for (idx = 0; idx < dlen; idx++) {
2164
2239
  self.dbref.collections[self.pageIterator.collection].data.push(JSON.parse(data[idx]));
2165
2240
  data[idx] = null;
2166
2241
  }
@@ -2194,8 +2269,8 @@
2194
2269
  *
2195
2270
  * @memberof LokiPartitioningAdapter
2196
2271
  */
2197
- LokiPartitioningAdapter.prototype.exportDatabase = function(dbname, dbref, callback) {
2198
- var self=this;
2272
+ LokiPartitioningAdapter.prototype.exportDatabase = function (dbname, dbref, callback) {
2273
+ var self = this;
2199
2274
  var idx, clen = dbref.collections.length;
2200
2275
 
2201
2276
  this.dbref = dbref;
@@ -2203,13 +2278,13 @@
2203
2278
 
2204
2279
  // queue up dirty partitions to be saved
2205
2280
  this.dirtyPartitions = [-1];
2206
- for(idx=0; idx<clen; idx++) {
2281
+ for (idx = 0; idx < clen; idx++) {
2207
2282
  if (dbref.collections[idx].dirty) {
2208
2283
  this.dirtyPartitions.push(idx);
2209
2284
  }
2210
2285
  }
2211
2286
 
2212
- this.saveNextPartition(function(err) {
2287
+ this.saveNextPartition(function (err) {
2213
2288
  callback(err);
2214
2289
  });
2215
2290
  };
@@ -2219,10 +2294,10 @@
2219
2294
  *
2220
2295
  * @param {function} callback - adapter callback to return load result to caller
2221
2296
  */
2222
- LokiPartitioningAdapter.prototype.saveNextPartition = function(callback) {
2223
- var self=this;
2297
+ LokiPartitioningAdapter.prototype.saveNextPartition = function (callback) {
2298
+ var self = this;
2224
2299
  var partition = this.dirtyPartitions.shift();
2225
- var keyname = this.dbname + ((partition===-1)?"":("." + partition));
2300
+ var keyname = this.dbname + ((partition === -1) ? "" : ("." + partition));
2226
2301
 
2227
2302
  // if we are doing paging and this is collection partition
2228
2303
  if (this.options.paging && partition !== -1) {
@@ -2233,7 +2308,7 @@
2233
2308
  };
2234
2309
 
2235
2310
  // since saveNextPage recursively calls itself until done, our callback means this whole paged partition is finished
2236
- this.saveNextPage(function(err) {
2311
+ this.saveNextPage(function (err) {
2237
2312
  if (self.dirtyPartitions.length === 0) {
2238
2313
  callback(err);
2239
2314
  }
@@ -2246,12 +2321,12 @@
2246
2321
 
2247
2322
  // otherwise this is 'non-paged' partioning...
2248
2323
  var result = this.dbref.serializeDestructured({
2249
- partitioned : true,
2324
+ partitioned: true,
2250
2325
  delimited: true,
2251
2326
  partition: partition
2252
2327
  });
2253
2328
 
2254
- this.adapter.saveDatabase(keyname, result, function(err) {
2329
+ this.adapter.saveDatabase(keyname, result, function (err) {
2255
2330
  if (err) {
2256
2331
  callback(err);
2257
2332
  return;
@@ -2271,19 +2346,19 @@
2271
2346
  *
2272
2347
  * @param {function} callback - adapter callback to return load result to caller
2273
2348
  */
2274
- LokiPartitioningAdapter.prototype.saveNextPage = function(callback) {
2275
- var self=this;
2349
+ LokiPartitioningAdapter.prototype.saveNextPage = function (callback) {
2350
+ var self = this;
2276
2351
  var coll = this.dbref.collections[this.pageIterator.collection];
2277
2352
  var keyname = this.dbname + "." + this.pageIterator.collection + "." + this.pageIterator.pageIndex;
2278
- var pageLen=0,
2353
+ var pageLen = 0,
2279
2354
  cdlen = coll.data.length,
2280
2355
  delimlen = this.options.delimiter.length;
2281
2356
  var serializedObject = "",
2282
2357
  pageBuilder = "";
2283
- var doneWithPartition=false,
2284
- doneWithPage=false;
2358
+ var doneWithPartition = false,
2359
+ doneWithPage = false;
2285
2360
 
2286
- var pageSaveCallback = function(err) {
2361
+ var pageSaveCallback = function (err) {
2287
2362
  pageBuilder = "";
2288
2363
 
2289
2364
  if (err) {
@@ -2337,11 +2412,11 @@
2337
2412
  * @constructor LokiFsAdapter
2338
2413
  */
2339
2414
  function LokiFsAdapter() {
2340
- // try {
2341
- // this.fs = require('fs');
2342
- // }catch(e) {
2415
+ try {
2416
+ this.fs = require('fs');
2417
+ } catch (e) {
2343
2418
  this.fs = null;
2344
- // }
2419
+ }
2345
2420
  }
2346
2421
 
2347
2422
  /**
@@ -2385,7 +2460,7 @@
2385
2460
  if (err) {
2386
2461
  callback(new Error(err));
2387
2462
  } else {
2388
- self.fs.rename(tmpdbname,dbname,callback);
2463
+ self.fs.rename(tmpdbname, dbname, callback);
2389
2464
  }
2390
2465
  });
2391
2466
  };
@@ -2412,7 +2487,7 @@
2412
2487
  * A loki persistence adapter which persists to web browser's local storage object
2413
2488
  * @constructor LokiLocalStorageAdapter
2414
2489
  */
2415
- function LokiLocalStorageAdapter() {}
2490
+ function LokiLocalStorageAdapter() { }
2416
2491
 
2417
2492
  /**
2418
2493
  * loadDatabase() - Load data from localstorage
@@ -2470,7 +2545,7 @@
2470
2545
  * @param {int} options.recursiveWaitLimitDelay - (default: 2000) cutoff in ms to stop recursively re-draining
2471
2546
  * @memberof Loki
2472
2547
  */
2473
- Loki.prototype.throttledSaveDrain = function(callback, options) {
2548
+ Loki.prototype.throttledSaveDrain = function (callback, options) {
2474
2549
  var self = this;
2475
2550
  var now = (new Date()).getTime();
2476
2551
 
@@ -2497,7 +2572,7 @@
2497
2572
  // if we want to wait until we are in a state where there are no pending saves at all
2498
2573
  if (options.recursiveWait) {
2499
2574
  // queue the following meta callback for when it completes
2500
- this.throttledCallbacks.push(function() {
2575
+ this.throttledCallbacks.push(function () {
2501
2576
  // if there is now another save pending...
2502
2577
  if (self.throttledSavePending) {
2503
2578
  // if we wish to wait only so long and we have exceeded limit of our waiting, callback with false success value
@@ -2536,10 +2611,10 @@
2536
2611
  */
2537
2612
  Loki.prototype.loadDatabaseInternal = function (options, callback) {
2538
2613
  var cFun = callback || function (err, data) {
2539
- if (err) {
2540
- throw err;
2541
- }
2542
- },
2614
+ if (err) {
2615
+ throw err;
2616
+ }
2617
+ },
2543
2618
  self = this;
2544
2619
 
2545
2620
  // the persistenceAdapter should be present if all is ok, but check to be sure.
@@ -2568,8 +2643,8 @@
2568
2643
 
2569
2644
  // instanceof error means load faulted
2570
2645
  if (dbString instanceof Error) {
2571
- cFun(dbString);
2572
- return;
2646
+ cFun(dbString);
2647
+ return;
2573
2648
  }
2574
2649
 
2575
2650
  // if adapter has returned an js object (other than null or error) attempt to load from JSON object
@@ -2614,7 +2689,7 @@
2614
2689
  * });
2615
2690
  */
2616
2691
  Loki.prototype.loadDatabase = function (options, callback) {
2617
- var self=this;
2692
+ var self = this;
2618
2693
 
2619
2694
  // if throttling disabled, just call internal
2620
2695
  if (!this.throttledSaves) {
@@ -2623,12 +2698,12 @@
2623
2698
  }
2624
2699
 
2625
2700
  // try to drain any pending saves in the queue to lock it for loading
2626
- this.throttledSaveDrain(function(success) {
2701
+ this.throttledSaveDrain(function (success) {
2627
2702
  if (success) {
2628
2703
  // pause/throttle saving until loading is done
2629
2704
  self.throttledSavePending = true;
2630
2705
 
2631
- self.loadDatabaseInternal(options, function(err) {
2706
+ self.loadDatabaseInternal(options, function (err) {
2632
2707
  // now that we are finished loading, if no saves were throttled, disable flag
2633
2708
  if (self.throttledCallbacks.length === 0) {
2634
2709
  self.throttledSavePending = false;
@@ -2657,11 +2732,11 @@
2657
2732
  */
2658
2733
  Loki.prototype.saveDatabaseInternal = function (callback) {
2659
2734
  var cFun = callback || function (err) {
2660
- if (err) {
2661
- throw err;
2662
- }
2663
- return;
2664
- };
2735
+ if (err) {
2736
+ throw err;
2737
+ }
2738
+ return;
2739
+ };
2665
2740
  var self = this;
2666
2741
 
2667
2742
  // the persistenceAdapter should be present if all is ok, but check to be sure.
@@ -2670,46 +2745,59 @@
2670
2745
  return;
2671
2746
  }
2672
2747
 
2673
- // persistenceAdapter might be asynchronous, so we must clear `dirty` immediately
2674
- // or autosave won't work if an update occurs between here and the callback
2675
- // TODO: This should be stored and rolled back in case of DB save failure
2676
- // TODO: Reference mode adapter should have the same behavior
2677
- if (this.persistenceAdapter.mode !== "reference") {
2678
- this.autosaveClearFlags();
2679
- }
2680
-
2681
2748
  // run incremental, reference, or normal mode adapters, depending on what's available
2682
2749
  if (this.persistenceAdapter.mode === "incremental") {
2683
- var lokiCopy = this.copy({removeNonSerializable:true});
2684
-
2685
- // remember and clear dirty ids -- we must do it before the save so that if
2686
- // and update occurs between here and callback, it will get saved later
2687
- var cachedDirtyIds = this.collections.map(function (collection) {
2688
- return collection.dirtyIds;
2689
- });
2690
- this.collections.forEach(function (col) {
2691
- col.dirtyIds = [];
2692
- });
2750
+ var cachedDirty;
2751
+ // ignore autosave until we copy loki (only then we can clear dirty flags,
2752
+ // but if we don't do it now, autosave will be triggered a lot unnecessarily)
2753
+ this.ignoreAutosave = true;
2754
+ this.persistenceAdapter.saveDatabase(
2755
+ this.filename,
2756
+ function getLokiCopy() {
2757
+ self.ignoreAutosave = false;
2758
+ if (cachedDirty) {
2759
+ cFun(new Error('adapter error - getLokiCopy called more than once'));
2760
+ return;
2761
+ }
2762
+ var lokiCopy = self.copy({ removeNonSerializable: true });
2693
2763
 
2694
- this.persistenceAdapter.saveDatabase(this.filename, lokiCopy, function exportDatabaseCallback(err) {
2695
- if (err) {
2696
- // roll back dirty IDs to be saved later
2697
- self.collections.forEach(function (col, i) {
2698
- col.dirtyIds = col.dirtyIds.concat(cachedDirtyIds[i]);
2764
+ // remember and clear dirty ids -- we must do it before the save so that if
2765
+ // and update occurs between here and callback, it will get saved later
2766
+ cachedDirty = self.collections.map(function (collection) {
2767
+ return [collection.dirty, collection.dirtyIds];
2699
2768
  });
2700
- }
2701
- cFun(err);
2702
- });
2703
-
2769
+ self.collections.forEach(function (col) {
2770
+ col.dirty = false;
2771
+ col.dirtyIds = [];
2772
+ });
2773
+ return lokiCopy;
2774
+ },
2775
+ function exportDatabaseCallback(err) {
2776
+ self.ignoreAutosave = false;
2777
+ if (err && cachedDirty) {
2778
+ // roll back dirty IDs to be saved later
2779
+ self.collections.forEach(function (col, i) {
2780
+ var cached = cachedDirty[i];
2781
+ col.dirty = col.dirty || cached[0];
2782
+ col.dirtyIds = col.dirtyIds.concat(cached[1]);
2783
+ });
2784
+ }
2785
+ cFun(err);
2786
+ });
2704
2787
  } else if (this.persistenceAdapter.mode === "reference" && typeof this.persistenceAdapter.exportDatabase === "function") {
2788
+ // TODO: dirty should be cleared here
2705
2789
  // filename may seem redundant but loadDatabase will need to expect this same filename
2706
- this.persistenceAdapter.exportDatabase(this.filename, this.copy({removeNonSerializable:true}), function exportDatabaseCallback(err) {
2790
+ this.persistenceAdapter.exportDatabase(this.filename, this.copy({ removeNonSerializable: true }), function exportDatabaseCallback(err) {
2707
2791
  self.autosaveClearFlags();
2708
2792
  cFun(err);
2709
2793
  });
2710
2794
  }
2711
2795
  // otherwise just pass the serialized database to adapter
2712
2796
  else {
2797
+ // persistenceAdapter might be asynchronous, so we must clear `dirty` immediately
2798
+ // or autosave won't work if an update occurs between here and the callback
2799
+ // TODO: This should be stored and rolled back in case of DB save failure
2800
+ this.autosaveClearFlags();
2713
2801
  this.persistenceAdapter.saveDatabase(this.filename, this.serialize(), function saveDatabasecallback(err) {
2714
2802
  cFun(err);
2715
2803
  });
@@ -2752,12 +2840,12 @@
2752
2840
  this.throttledSavePending = true;
2753
2841
 
2754
2842
  var self = this;
2755
- this.saveDatabaseInternal(function(err) {
2843
+ this.saveDatabaseInternal(function (err) {
2756
2844
  self.throttledSavePending = false;
2757
- localCallbacks.forEach(function(pcb) {
2845
+ localCallbacks.forEach(function (pcb) {
2758
2846
  if (typeof pcb === 'function') {
2759
2847
  // Queue the callbacks so we first finish this method execution
2760
- setTimeout(function() {
2848
+ setTimeout(function () {
2761
2849
  pcb(err);
2762
2850
  }, 1);
2763
2851
  }
@@ -2852,7 +2940,12 @@
2852
2940
  // so next step will be to implement collection level dirty flags set on insert/update/remove
2853
2941
  // along with loki level isdirty() function which iterates all collections to see if any are dirty
2854
2942
 
2855
- if (self.autosaveDirty()) {
2943
+ // for pause autosave
2944
+ if (!self.autosave) {
2945
+ return;
2946
+ }
2947
+
2948
+ if (self.autosaveDirty() && !self.ignoreAutosave) {
2856
2949
  self.saveDatabase(callback);
2857
2950
  }
2858
2951
  }, delay);
@@ -3031,46 +3124,46 @@
3031
3124
  step = transform[idx];
3032
3125
 
3033
3126
  switch (step.type) {
3034
- case "find":
3035
- rs.find(step.value);
3036
- break;
3037
- case "where":
3038
- rs.where(step.value);
3039
- break;
3040
- case "simplesort":
3041
- rs.simplesort(step.property, step.desc || step.options);
3042
- break;
3043
- case "compoundsort":
3044
- rs.compoundsort(step.value);
3045
- break;
3046
- case "sort":
3047
- rs.sort(step.value);
3048
- break;
3049
- case "limit":
3050
- rs = rs.limit(step.value);
3051
- break; // limit makes copy so update reference
3052
- case "offset":
3053
- rs = rs.offset(step.value);
3054
- break; // offset makes copy so update reference
3055
- case "map":
3056
- rs = rs.map(step.value, step.dataOptions);
3057
- break;
3058
- case "eqJoin":
3059
- rs = rs.eqJoin(step.joinData, step.leftJoinKey, step.rightJoinKey, step.mapFun, step.dataOptions);
3060
- break;
3127
+ case "find":
3128
+ rs.find(step.value);
3129
+ break;
3130
+ case "where":
3131
+ rs.where(step.value);
3132
+ break;
3133
+ case "simplesort":
3134
+ rs.simplesort(step.property, step.desc || step.options);
3135
+ break;
3136
+ case "compoundsort":
3137
+ rs.compoundsort(step.value);
3138
+ break;
3139
+ case "sort":
3140
+ rs.sort(step.value);
3141
+ break;
3142
+ case "limit":
3143
+ rs = rs.limit(step.value);
3144
+ break; // limit makes copy so update reference
3145
+ case "offset":
3146
+ rs = rs.offset(step.value);
3147
+ break; // offset makes copy so update reference
3148
+ case "map":
3149
+ rs = rs.map(step.value, step.dataOptions);
3150
+ break;
3151
+ case "eqJoin":
3152
+ rs = rs.eqJoin(step.joinData, step.leftJoinKey, step.rightJoinKey, step.mapFun, step.dataOptions);
3153
+ break;
3061
3154
  // following cases break chain by returning array data so make any of these last in transform steps
3062
- case "mapReduce":
3063
- rs = rs.mapReduce(step.mapFunction, step.reduceFunction);
3064
- break;
3155
+ case "mapReduce":
3156
+ rs = rs.mapReduce(step.mapFunction, step.reduceFunction);
3157
+ break;
3065
3158
  // following cases update documents in current filtered resultset (use carefully)
3066
- case "update":
3067
- rs.update(step.value);
3068
- break;
3069
- case "remove":
3070
- rs.remove();
3071
- break;
3072
- default:
3073
- break;
3159
+ case "update":
3160
+ rs.update(step.value);
3161
+ break;
3162
+ case "remove":
3163
+ rs.remove();
3164
+ break;
3165
+ default:
3166
+ break;
3074
3167
  }
3075
3168
  }
3076
3169
 
@@ -3174,7 +3267,7 @@
3174
3267
  if (!options.disableIndexIntersect && hasBinaryIndex) {
3175
3268
 
3176
3269
  // calculate filter efficiency
3177
- eff = dc/frl;
3270
+ eff = dc / frl;
3178
3271
 
3179
3272
  // when javascript sort fallback is enabled, you generally need more than ~17% of total docs in resultset
3180
3273
  // before array intersect is determined to be the faster algorithm, otherwise leave at 10% for loki sort.
@@ -3185,17 +3278,17 @@
3185
3278
  // anything more than ratio of 10:1 (total documents/current results) should use old sort code path
3186
3279
  // So we will only use array intersection if you have more than 10% of total docs in your current resultset.
3187
3280
  if (eff <= targetEff || options.forceIndexIntersect) {
3188
- var idx, fr=this.filteredrows;
3281
+ var idx, fr = this.filteredrows;
3189
3282
  var io = {};
3190
3283
  // set up hashobject for simple 'inclusion test' with existing (filtered) results
3191
- for(idx=0; idx<frl; idx++) {
3284
+ for (idx = 0; idx < frl; idx++) {
3192
3285
  io[fr[idx]] = true;
3193
3286
  }
3194
3287
  // grab full sorted binary index array
3195
3288
  var pv = this.collection.binaryIndices[propname].values;
3196
3289
 
3197
3290
  // filter by existing results
3198
- this.filteredrows = pv.filter(function(n) { return io[n]; });
3291
+ this.filteredrows = pv.filter(function (n) { return io[n]; });
3199
3292
 
3200
3293
  if (options.desc) {
3201
3294
  this.filteredrows.reverse();
@@ -3210,7 +3303,7 @@
3210
3303
 
3211
3304
  // if we have opted to use simplified javascript comparison function...
3212
3305
  if (options.useJavascriptSorting) {
3213
- return this.sort(function(obj1, obj2) {
3306
+ return this.sort(function (obj1, obj2) {
3214
3307
  if (obj1[propname] === obj2[propname]) return 0;
3215
3308
  if (obj1[propname] > obj2[propname]) return 1;
3216
3309
  if (obj1[propname] < obj2[propname]) return -1;
@@ -3333,7 +3426,7 @@
3333
3426
  Resultset.prototype.$or = Resultset.prototype.findOr;
3334
3427
 
3335
3428
  // precompile recursively
3336
- function precompileQuery (operator, value) {
3429
+ function precompileQuery(operator, value) {
3337
3430
  // for regex ops, precompile
3338
3431
  if (operator === '$regex') {
3339
3432
  if (Array.isArray(value)) {
@@ -3492,6 +3585,12 @@
3492
3585
  index = this.collection.binaryIndices[property];
3493
3586
  }
3494
3587
 
3588
+ // opportunistically speed up $in searches from O(n*m) to O(n*log m)
3589
+ if (!searchByIndex && operator === '$in' && Array.isArray(value) && typeof Set !== 'undefined') {
3590
+ value = new Set(value);
3591
+ operator = '$inSet';
3592
+ }
3593
+
3495
3594
  // the comparison function
3496
3595
  var fun = LokiOps[operator];
3497
3596
 
@@ -3507,7 +3606,7 @@
3507
3606
  //
3508
3607
  // For performance reasons, each case has its own if block to minimize in-loop calculations
3509
3608
 
3510
- var filter, rowIdx = 0;
3609
+ var filter, rowIdx = 0, record;
3511
3610
 
3512
3611
  // If the filteredrows[] is already initialized, use it
3513
3612
  if (this.filterInitialized) {
@@ -3517,9 +3616,10 @@
3517
3616
  // currently supporting dot notation for non-indexed conditions only
3518
3617
  if (usingDotNotation) {
3519
3618
  property = property.split('.');
3520
- for(i=0; i<len; i++) {
3619
+ for (i = 0; i < len; i++) {
3521
3620
  rowIdx = filter[i];
3522
- if (dotSubScan(t[rowIdx], property, fun, value)) {
3621
+ record = t[rowIdx];
3622
+ if (dotSubScan(record, property, fun, value, record)) {
3523
3623
  result.push(rowIdx);
3524
3624
  if (firstOnly) {
3525
3625
  this.filteredrows = result;
@@ -3528,9 +3628,10 @@
3528
3628
  }
3529
3629
  }
3530
3630
  } else {
3531
- for(i=0; i<len; i++) {
3631
+ for (i = 0; i < len; i++) {
3532
3632
  rowIdx = filter[i];
3533
- if (fun(t[rowIdx][property], value)) {
3633
+ record = t[rowIdx];
3634
+ if (fun(record[property], value, record)) {
3534
3635
  result.push(rowIdx);
3535
3636
  if (firstOnly) {
3536
3637
  this.filteredrows = result;
@@ -3548,8 +3649,9 @@
3548
3649
 
3549
3650
  if (usingDotNotation) {
3550
3651
  property = property.split('.');
3551
- for(i=0; i<len; i++) {
3552
- if (dotSubScan(t[i], property, fun, value)) {
3652
+ for (i = 0; i < len; i++) {
3653
+ record = t[i];
3654
+ if (dotSubScan(record, property, fun, value, record)) {
3553
3655
  result.push(i);
3554
3656
  if (firstOnly) {
3555
3657
  this.filteredrows = result;
@@ -3559,8 +3661,9 @@
3559
3661
  }
3560
3662
  }
3561
3663
  } else {
3562
- for(i=0; i<len; i++) {
3563
- if (fun(t[i][property], value)) {
3664
+ for (i = 0; i < len; i++) {
3665
+ record = t[i];
3666
+ if (fun(record[property], value, record)) {
3564
3667
  result.push(i);
3565
3668
  if (firstOnly) {
3566
3669
  this.filteredrows = result;
@@ -3588,12 +3691,12 @@
3588
3691
  }
3589
3692
  }
3590
3693
  else {
3591
- result.push(index.values[i]);
3592
- if (firstOnly) {
3593
- this.filteredrows = result;
3594
- this.filterInitialized = true;
3595
- return this;
3596
- }
3694
+ result.push(index.values[i]);
3695
+ if (firstOnly) {
3696
+ this.filteredrows = result;
3697
+ this.filterInitialized = true;
3698
+ return this;
3699
+ }
3597
3700
  }
3598
3701
  }
3599
3702
  } else {
@@ -3716,7 +3819,8 @@
3716
3819
  }
3717
3820
 
3718
3821
  // if collection has delta changes active, then force clones and use 'parse-stringify' for effective change tracking of nested objects
3719
- if (!this.collection.disableDeltaChangesApi) {
3822
+ // if collection is immutable freeze and unFreeze takes care of cloning
3823
+ if (!this.collection.disableDeltaChangesApi && this.collection.disableFreeze) {
3720
3824
  options.forceClones = true;
3721
3825
  options.forceCloneMethod = 'parse-stringify';
3722
3826
  }
@@ -3728,7 +3832,6 @@
3728
3832
  if (this.collection.cloneObjects || options.forceClones) {
3729
3833
  len = data.length;
3730
3834
  method = options.forceCloneMethod || this.collection.cloneMethod;
3731
-
3732
3835
  for (i = 0; i < len; i++) {
3733
3836
  obj = clone(data[i], method);
3734
3837
  if (options.removeMeta) {
@@ -3798,7 +3901,7 @@
3798
3901
  // pass in each document object currently in resultset to user supplied updateFunction
3799
3902
  for (var idx = 0; idx < len; idx++) {
3800
3903
  // if we have cloning option specified or are doing differential delta changes, clone object first
3801
- if (this.collection.cloneObjects || !this.collection.disableDeltaChangesApi) {
3904
+ if (!this.disableFreeze || this.collection.cloneObjects || !this.collection.disableDeltaChangesApi) {
3802
3905
  obj = clone(rcd[this.filteredrows[idx]], this.collection.cloneMethod);
3803
3906
  updateFunction(obj);
3804
3907
  this.collection.update(obj);
@@ -4059,6 +4162,9 @@
4059
4162
 
4060
4163
  // keep ordered filter pipeline
4061
4164
  this.filterPipeline = [];
4165
+ if (!this.collection.disableFreeze) {
4166
+ Object.freeze(this.filterPipeline);
4167
+ }
4062
4168
 
4063
4169
  // sorting member variables
4064
4170
  // we only support one active search, applied using applySort() or applySimpleSort()
@@ -4071,12 +4177,23 @@
4071
4177
  // once we refactor transactions, i will tie in certain transactional events
4072
4178
 
4073
4179
  this.events = {
4074
- 'rebuild': []
4180
+ 'rebuild': [],
4181
+ 'filter': [],
4182
+ 'sort': []
4075
4183
  };
4076
4184
  }
4077
4185
 
4078
4186
  DynamicView.prototype = new LokiEventEmitter();
4187
+ DynamicView.prototype.constructor = DynamicView;
4079
4188
 
4189
+ /**
4190
+ * getSort() - used to get the current sort
4191
+ *
4192
+ * @returns function (sortFunction) or array (sortCriteria) or object (sortCriteriaSimple)
4193
+ */
4194
+ DynamicView.prototype.getSort = function () {
4195
+ return this.sortFunction || this.sortCriteria || this.sortCriteriaSimple;
4196
+ };
4080
4197
 
4081
4198
  /**
4082
4199
  * rematerialize() - internally used immediately after deserialization (loading)
@@ -4104,9 +4221,13 @@
4104
4221
  this.sortDirty = true;
4105
4222
  }
4106
4223
 
4224
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4107
4225
  if (options.hasOwnProperty('removeWhereFilters')) {
4108
4226
  // for each view see if it had any where filters applied... since they don't
4109
4227
  // serialize those functions lets remove those invalid filters
4228
+ if (wasFrozen) {
4229
+ this.filterPipeline = this.filterPipeline.slice();
4230
+ }
4110
4231
  fpl = this.filterPipeline.length;
4111
4232
  fpi = fpl;
4112
4233
  while (fpi--) {
@@ -4114,7 +4235,6 @@
4114
4235
  if (fpi !== this.filterPipeline.length - 1) {
4115
4236
  this.filterPipeline[fpi] = this.filterPipeline[this.filterPipeline.length - 1];
4116
4237
  }
4117
-
4118
4238
  this.filterPipeline.length--;
4119
4239
  }
4120
4240
  }
@@ -4127,7 +4247,10 @@
4127
4247
  // now re-apply 'find' filterPipeline ops
4128
4248
  fpl = ofp.length;
4129
4249
  for (idx = 0; idx < fpl; idx++) {
4130
- this.applyFind(ofp[idx].val);
4250
+ this.applyFind(ofp[idx].val, ofp[idx].uid);
4251
+ }
4252
+ if (wasFrozen) {
4253
+ Object.freeze(this.filterPipeline);
4131
4254
  }
4132
4255
 
4133
4256
  // during creation of unit tests, i will remove this forced refresh and leave lazy
@@ -4184,7 +4307,6 @@
4184
4307
  */
4185
4308
  DynamicView.prototype.toJSON = function () {
4186
4309
  var copy = new DynamicView(this.collection, this.name, this.options);
4187
-
4188
4310
  copy.resultset = this.resultset;
4189
4311
  copy.resultdata = []; // let's not save data (copy) to minimize size
4190
4312
  copy.resultsdirty = true;
@@ -4217,8 +4339,13 @@
4217
4339
 
4218
4340
  this.cachedresultset = null;
4219
4341
 
4342
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4343
+ var filterChanged = this.filterPipeline.length > 0;
4220
4344
  // keep ordered filter pipeline
4221
4345
  this.filterPipeline = [];
4346
+ if (wasFrozen) {
4347
+ Object.freeze(this.filterPipeline);
4348
+ }
4222
4349
 
4223
4350
  // sorting member variables
4224
4351
  // we only support one active search, applied using applySort() or applySimpleSort()
@@ -4230,6 +4357,10 @@
4230
4357
  if (options.queueSortPhase === true) {
4231
4358
  this.queueSortPhase();
4232
4359
  }
4360
+
4361
+ if (filterChanged) {
4362
+ this.emit('filter');
4363
+ }
4233
4364
  };
4234
4365
 
4235
4366
  /**
@@ -4251,6 +4382,7 @@
4251
4382
  this.sortCriteriaSimple = null;
4252
4383
 
4253
4384
  this.queueSortPhase();
4385
+ this.emit('sort');
4254
4386
 
4255
4387
  return this;
4256
4388
  };
@@ -4271,10 +4403,14 @@
4271
4403
  */
4272
4404
  DynamicView.prototype.applySimpleSort = function (propname, options) {
4273
4405
  this.sortCriteriaSimple = { propname: propname, options: options || false };
4406
+ if (!this.collection.disableFreeze) {
4407
+ deepFreeze(this.sortCriteriaSimple);
4408
+ }
4274
4409
  this.sortCriteria = null;
4275
4410
  this.sortFunction = null;
4276
4411
 
4277
4412
  this.queueSortPhase();
4413
+ this.emit('sort');
4278
4414
 
4279
4415
  return this;
4280
4416
  };
@@ -4295,11 +4431,14 @@
4295
4431
  */
4296
4432
  DynamicView.prototype.applySortCriteria = function (criteria) {
4297
4433
  this.sortCriteria = criteria;
4434
+ if (!this.collection.disableFreeze) {
4435
+ deepFreeze(this.sortCriteria);
4436
+ }
4298
4437
  this.sortCriteriaSimple = null;
4299
4438
  this.sortFunction = null;
4300
4439
 
4301
4440
  this.queueSortPhase();
4302
-
4441
+ this.emit('sort');
4303
4442
  return this;
4304
4443
  };
4305
4444
 
@@ -4370,7 +4509,17 @@
4370
4509
  * @param {object} filter - The filter object. Refer to applyFilter() for extra details.
4371
4510
  */
4372
4511
  DynamicView.prototype._addFilter = function (filter) {
4512
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4513
+ if (wasFrozen) {
4514
+ this.filterPipeline = this.filterPipeline.slice();
4515
+ }
4516
+ if (!this.collection.disableFreeze) {
4517
+ deepFreeze(filter);
4518
+ }
4373
4519
  this.filterPipeline.push(filter);
4520
+ if (wasFrozen) {
4521
+ Object.freeze(this.filterPipeline);
4522
+ }
4374
4523
  this.resultset[filter.type](filter.val);
4375
4524
  };
4376
4525
 
@@ -4389,18 +4538,22 @@
4389
4538
  }
4390
4539
 
4391
4540
  var filters = this.filterPipeline;
4541
+ var wasFrozen = Object.isFrozen(filters);
4392
4542
  this.filterPipeline = [];
4393
4543
 
4394
4544
  for (var idx = 0, len = filters.length; idx < len; idx += 1) {
4395
4545
  this._addFilter(filters[idx]);
4396
4546
  }
4547
+ if (wasFrozen) {
4548
+ Object.freeze(this.filterPipeline);
4549
+ }
4397
4550
 
4398
4551
  if (this.sortFunction || this.sortCriteria || this.sortCriteriaSimple) {
4399
4552
  this.queueSortPhase();
4400
4553
  } else {
4401
4554
  this.queueRebuildEvent();
4402
4555
  }
4403
-
4556
+ this.emit('filter');
4404
4557
  return this;
4405
4558
  };
4406
4559
 
@@ -4415,7 +4568,15 @@
4415
4568
  DynamicView.prototype.applyFilter = function (filter) {
4416
4569
  var idx = this._indexOfFilterWithId(filter.uid);
4417
4570
  if (idx >= 0) {
4571
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4572
+ if (wasFrozen) {
4573
+ this.filterPipeline = this.filterPipeline.slice();
4574
+ }
4418
4575
  this.filterPipeline[idx] = filter;
4576
+ if (wasFrozen) {
4577
+ freeze(filter);
4578
+ Object.freeze(this.filterPipeline);
4579
+ }
4419
4580
  return this.reapplyFilters();
4420
4581
  }
4421
4582
 
@@ -4433,6 +4594,7 @@
4433
4594
  this.queueRebuildEvent();
4434
4595
  }
4435
4596
 
4597
+ this.emit('filter');
4436
4598
  return this;
4437
4599
  };
4438
4600
 
@@ -4482,8 +4644,14 @@
4482
4644
  if (idx < 0) {
4483
4645
  throw new Error("Dynamic view does not contain a filter with ID: " + uid);
4484
4646
  }
4485
-
4647
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4648
+ if (wasFrozen) {
4649
+ this.filterPipeline = this.filterPipeline.slice();
4650
+ }
4486
4651
  this.filterPipeline.splice(idx, 1);
4652
+ if (wasFrozen) {
4653
+ Object.freeze(this.filterPipeline);
4654
+ }
4487
4655
  this.reapplyFilters();
4488
4656
  return this;
4489
4657
  };
@@ -4743,23 +4911,23 @@
4743
4911
 
4744
4912
  rmlen = objIndex.length;
4745
4913
  // create intersection object of data indices to remove
4746
- for(rmidx=0;rmidx<rmlen; rmidx++) {
4914
+ for (rmidx = 0; rmidx < rmlen; rmidx++) {
4747
4915
  rxo[objIndex[rmidx]] = true;
4748
4916
  }
4749
4917
 
4750
4918
  // pivot remove data indices into remove filteredrows indices and dump in hashobject
4751
- for (idx=0; idx<frlen; idx++) {
4919
+ for (idx = 0; idx < frlen; idx++) {
4752
4920
  if (rxo[fr[idx]]) fxo[idx] = true;
4753
4921
  }
4754
4922
 
4755
4923
  // if any of the removed items were in our filteredrows...
4756
4924
  if (Object.keys(fxo).length > 0) {
4757
4925
  // remove them from filtered rows
4758
- this.resultset.filteredrows = this.resultset.filteredrows.filter(function(di, idx) { return !fxo[idx]; });
4926
+ this.resultset.filteredrows = this.resultset.filteredrows.filter(function (di, idx) { return !fxo[idx]; });
4759
4927
  // if persistent...
4760
4928
  if (this.options.persistent) {
4761
4929
  // remove from resultdata
4762
- this.resultdata = this.resultdata.filter(function(obj, idx) { return !fxo[idx]; });
4930
+ this.resultdata = this.resultdata.filter(function (obj, idx) { return !fxo[idx]; });
4763
4931
  }
4764
4932
 
4765
4933
  // and queue sorts
@@ -4771,7 +4939,7 @@
4771
4939
  }
4772
4940
 
4773
4941
  // to remove holes, we need to 'shift down' indices, this filter function finds number of positions to shift
4774
- var filt = function(idx) { return function(di) { return di < drs.filteredrows[idx]; }; };
4942
+ var filt = function (idx) { return function (di) { return di < drs.filteredrows[idx]; }; };
4775
4943
 
4776
4944
  frlen = drs.filteredrows.length;
4777
4945
  for (idx = 0; idx < frlen; idx++) {
@@ -4817,6 +4985,7 @@
4817
4985
  * @param {boolean} [options.autoupdate=false] - use Object.observe to update objects automatically
4818
4986
  * @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user
4819
4987
  * @param {boolean} [options.serializableIndices=true[]] - converts date values on binary indexed properties to epoch time
4988
+ * @param {boolean} [options.disableFreeze=true] - when false all docs are frozen
4820
4989
  * @param {string} [options.cloneMethod='parse-stringify'] - 'parse-stringify', 'jquery-extend-deep', 'shallow', 'shallow-assign'
4821
4990
  * @param {int=} options.ttl - age of document (in ms.) before document is considered aged/stale.
4822
4991
  * @param {int=} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default.
@@ -4828,7 +4997,7 @@
4828
4997
  this.name = name;
4829
4998
  // the data held by the collection
4830
4999
  this.data = [];
4831
- this.idIndex = []; // index of id
5000
+ this.idIndex = null; // position->$loki index (built lazily)
4832
5001
  this.binaryIndices = {}; // user defined indexes
4833
5002
  this.constraints = {
4834
5003
  unique: {},
@@ -4836,7 +5005,7 @@
4836
5005
  };
4837
5006
 
4838
5007
  // unique contraints contain duplicate object references, so they are not persisted.
4839
- // we will keep track of properties which have unique contraint applied here, and regenerate on load
5008
+ // we will keep track of properties which have unique contraint applied here, and regenerate lazily
4840
5009
  this.uniqueNames = [];
4841
5010
 
4842
5011
  // transforms will be used to store frequently used query chains as a series of steps
@@ -4865,9 +5034,9 @@
4865
5034
  if (!Array.isArray(options.unique)) {
4866
5035
  options.unique = [options.unique];
4867
5036
  }
5037
+ // save names; actual index is built lazily
4868
5038
  options.unique.forEach(function (prop) {
4869
- self.uniqueNames.push(prop); // used to regenerate on subsequent database loads
4870
- self.constraints.unique[prop] = new UniqueIndex(prop);
5039
+ self.uniqueNames.push(prop);
4871
5040
  });
4872
5041
  }
4873
5042
 
@@ -4913,6 +5082,9 @@
4913
5082
  // same 'after' serialization as it was 'before'.
4914
5083
  this.serializableIndices = options.hasOwnProperty('serializableIndices') ? options.serializableIndices : true;
4915
5084
 
5085
+ // option to deep freeze all documents
5086
+ this.disableFreeze = options.hasOwnProperty('disableFreeze') ? options.disableFreeze : true;
5087
+
4916
5088
  //option to activate a cleaner daemon - clears "aged" documents at set intervals.
4917
5089
  this.ttl = {
4918
5090
  age: null,
@@ -4945,10 +5117,8 @@
4945
5117
  // lightweight changes tracking (loki IDs only) for optimized db saving
4946
5118
  this.dirtyIds = [];
4947
5119
 
4948
- // initialize the id index
4949
- this.ensureId();
4950
- var indices = [];
4951
5120
  // initialize optional user-supplied indices array ['age', 'lname', 'zip']
5121
+ var indices = [];
4952
5122
  if (options && options.indices) {
4953
5123
  if (Object.prototype.toString.call(options.indices) === '[object Array]') {
4954
5124
  indices = options.indices;
@@ -4983,7 +5153,7 @@
4983
5153
  return self.removeAutoUpdateObserver(object);
4984
5154
  try {
4985
5155
  self.update(object);
4986
- } catch (err) {}
5156
+ } catch (err) { }
4987
5157
  });
4988
5158
  }
4989
5159
 
@@ -5003,7 +5173,7 @@
5003
5173
 
5004
5174
  function getObjectDelta(oldObject, newObject) {
5005
5175
  var propertyNames = newObject !== null && typeof newObject === 'object' ? Object.keys(newObject) : null;
5006
- if (propertyNames && propertyNames.length && ['string', 'boolean', 'number'].indexOf(typeof(newObject)) < 0) {
5176
+ if (propertyNames && propertyNames.length && ['string', 'boolean', 'number'].indexOf(typeof (newObject)) < 0) {
5007
5177
  var delta = {};
5008
5178
  for (var i = 0; i < propertyNames.length; i++) {
5009
5179
  var propertyName = propertyNames[i];
@@ -5062,11 +5232,12 @@
5062
5232
  }
5063
5233
 
5064
5234
  Collection.prototype = new LokiEventEmitter();
5235
+ Collection.prototype.contructor = Collection;
5065
5236
 
5066
5237
  /*
5067
5238
  * For ChangeAPI default to clone entire object, for delta changes create object with only differences (+ $loki and meta)
5068
5239
  */
5069
- Collection.prototype.createChange = function(name, op, obj, old) {
5240
+ Collection.prototype.createChange = function (name, op, obj, old) {
5070
5241
  this.changes.push({
5071
5242
  name: name,
5072
5243
  operation: op,
@@ -5074,7 +5245,7 @@
5074
5245
  });
5075
5246
  };
5076
5247
 
5077
- Collection.prototype.insertMeta = function(obj) {
5248
+ Collection.prototype.insertMeta = function (obj) {
5078
5249
  var len, idx;
5079
5250
 
5080
5251
  if (this.disableMeta || !obj) {
@@ -5085,7 +5256,7 @@
5085
5256
  if (Array.isArray(obj)) {
5086
5257
  len = obj.length;
5087
5258
 
5088
- for(idx=0; idx<len; idx++) {
5259
+ for (idx = 0; idx < len; idx++) {
5089
5260
  if (!obj[idx].hasOwnProperty('meta')) {
5090
5261
  obj[idx].meta = {};
5091
5262
  }
@@ -5106,36 +5277,42 @@
5106
5277
  obj.meta.revision = 0;
5107
5278
  };
5108
5279
 
5109
- Collection.prototype.updateMeta = function(obj) {
5280
+ Collection.prototype.updateMeta = function (obj) {
5110
5281
  if (this.disableMeta || !obj) {
5111
- return;
5282
+ return obj;
5283
+ }
5284
+ if (!this.disableFreeze) {
5285
+ obj = unFreeze(obj);
5286
+ obj.meta = unFreeze(obj.meta);
5112
5287
  }
5113
5288
  obj.meta.updated = (new Date()).getTime();
5114
5289
  obj.meta.revision += 1;
5290
+ return obj;
5115
5291
  };
5116
5292
 
5117
- Collection.prototype.createInsertChange = function(obj) {
5293
+ Collection.prototype.createInsertChange = function (obj) {
5118
5294
  this.createChange(this.name, 'I', obj);
5119
5295
  };
5120
5296
 
5121
- Collection.prototype.createUpdateChange = function(obj, old) {
5297
+ Collection.prototype.createUpdateChange = function (obj, old) {
5122
5298
  this.createChange(this.name, 'U', obj, old);
5123
5299
  };
5124
5300
 
5125
- Collection.prototype.insertMetaWithChange = function(obj) {
5301
+ Collection.prototype.insertMetaWithChange = function (obj) {
5126
5302
  this.insertMeta(obj);
5127
5303
  this.createInsertChange(obj);
5128
5304
  };
5129
5305
 
5130
- Collection.prototype.updateMetaWithChange = function(obj, old) {
5131
- this.updateMeta(obj);
5306
+ Collection.prototype.updateMetaWithChange = function (obj, old, objFrozen) {
5307
+ obj = this.updateMeta(obj, objFrozen);
5132
5308
  this.createUpdateChange(obj, old);
5309
+ return obj;
5133
5310
  };
5134
5311
 
5135
5312
  Collection.prototype.lokiConsoleWrapper = {
5136
- log: function () {},
5137
- warn: function () {},
5138
- error: function () {},
5313
+ log: function () { },
5314
+ warn: function () { },
5315
+ error: function () { },
5139
5316
  };
5140
5317
 
5141
5318
  Collection.prototype.addAutoUpdateObserver = function (object) {
@@ -5422,7 +5599,7 @@
5422
5599
  options.randomSamplingFactor = 0.1;
5423
5600
  }
5424
5601
 
5425
- var valid=true, idx, iter, pos, len, biv;
5602
+ var valid = true, idx, iter, pos, len, biv;
5426
5603
 
5427
5604
  // make sure we are passed a valid binary index name
5428
5605
  if (!this.binaryIndices.hasOwnProperty(property)) {
@@ -5458,28 +5635,28 @@
5458
5635
  if (options.randomSampling) {
5459
5636
  // validate first and last
5460
5637
  if (!LokiOps.$lte(Utils.getIn(this.data[biv[0]], property, usingDotNotation),
5461
- Utils.getIn(this.data[biv[1]], property, usingDotNotation))) {
5462
- valid=false;
5638
+ Utils.getIn(this.data[biv[1]], property, usingDotNotation))) {
5639
+ valid = false;
5463
5640
  }
5464
- if (!LokiOps.$lte(Utils.getIn(this.data[biv[len-2]], property, usingDotNotation),
5465
- Utils.getIn(this.data[biv[len-1]], property, usingDotNotation))) {
5466
- valid=false;
5641
+ if (!LokiOps.$lte(Utils.getIn(this.data[biv[len - 2]], property, usingDotNotation),
5642
+ Utils.getIn(this.data[biv[len - 1]], property, usingDotNotation))) {
5643
+ valid = false;
5467
5644
  }
5468
5645
 
5469
5646
  // if first and last positions are sorted correctly with their nearest neighbor,
5470
5647
  // continue onto random sampling phase...
5471
5648
  if (valid) {
5472
5649
  // # random samplings = total count * sampling factor
5473
- iter = Math.floor((len-1) * options.randomSamplingFactor);
5650
+ iter = Math.floor((len - 1) * options.randomSamplingFactor);
5474
5651
 
5475
5652
  // for each random sampling, validate that the binary index is sequenced properly
5476
5653
  // with next higher value.
5477
- for(idx=0; idx<iter-1; idx++) {
5654
+ for (idx = 0; idx < iter - 1; idx++) {
5478
5655
  // calculate random position
5479
- pos = Math.floor(Math.random() * (len-1));
5656
+ pos = Math.floor(Math.random() * (len - 1));
5480
5657
  if (!LokiOps.$lte(Utils.getIn(this.data[biv[pos]], property, usingDotNotation),
5481
- Utils.getIn(this.data[biv[pos+1]], property, usingDotNotation))) {
5482
- valid=false;
5658
+ Utils.getIn(this.data[biv[pos + 1]], property, usingDotNotation))) {
5659
+ valid = false;
5483
5660
  break;
5484
5661
  }
5485
5662
  }
@@ -5487,10 +5664,10 @@
5487
5664
  }
5488
5665
  else {
5489
5666
  // validate that the binary index is sequenced properly
5490
- for(idx=0; idx<len-1; idx++) {
5667
+ for (idx = 0; idx < len - 1; idx++) {
5491
5668
  if (!LokiOps.$lte(Utils.getIn(this.data[biv[idx]], property, usingDotNotation),
5492
- Utils.getIn(this.data[biv[idx+1]], property, usingDotNotation))) {
5493
- valid=false;
5669
+ Utils.getIn(this.data[biv[idx + 1]], property, usingDotNotation))) {
5670
+ valid = false;
5494
5671
  break;
5495
5672
  }
5496
5673
  }
@@ -5516,6 +5693,19 @@
5516
5693
  return result;
5517
5694
  };
5518
5695
 
5696
+ /**
5697
+ * Returns a named unique index
5698
+ * @param {string} field - indexed field name
5699
+ * @param {boolean} force - if `true`, will rebuild index; otherwise, function may return null
5700
+ */
5701
+ Collection.prototype.getUniqueIndex = function (field, force) {
5702
+ var index = this.constraints.unique[field];
5703
+ if (!index && force) {
5704
+ return this.ensureUniqueIndex(field);
5705
+ }
5706
+ return index;
5707
+ };
5708
+
5519
5709
  Collection.prototype.ensureUniqueIndex = function (field) {
5520
5710
  var index = this.constraints.unique[field];
5521
5711
  if (!index) {
@@ -5585,13 +5775,17 @@
5585
5775
  * Rebuild idIndex
5586
5776
  */
5587
5777
  Collection.prototype.ensureId = function () {
5588
- var len = this.data.length,
5778
+ if (this.idIndex) {
5779
+ return;
5780
+ }
5781
+ var data = this.data,
5589
5782
  i = 0;
5590
-
5591
- this.idIndex = [];
5592
- for (i; i < len; i += 1) {
5593
- this.idIndex.push(this.data[i].$loki);
5783
+ var len = data.length;
5784
+ var index = new Array(len);
5785
+ for (i; i < len; i++) {
5786
+ index[i] = data[i].$loki;
5594
5787
  }
5788
+ this.idIndex = index;
5595
5789
  };
5596
5790
 
5597
5791
  /**
@@ -5634,7 +5828,7 @@
5634
5828
  **/
5635
5829
  Collection.prototype.removeDynamicView = function (name) {
5636
5830
  this.DynamicViews =
5637
- this.DynamicViews.filter(function(dv) { return dv.name !== name; });
5831
+ this.DynamicViews.filter(function (dv) { return dv.name !== name; });
5638
5832
  };
5639
5833
 
5640
5834
  /**
@@ -5676,7 +5870,7 @@
5676
5870
  * @param {object} filterObject - 'mongo-like' query object
5677
5871
  * @memberof Collection
5678
5872
  */
5679
- Collection.prototype.findAndRemove = function(filterObject) {
5873
+ Collection.prototype.findAndRemove = function (filterObject) {
5680
5874
  this.chain().find(filterObject).remove();
5681
5875
  };
5682
5876
 
@@ -5764,12 +5958,19 @@
5764
5958
 
5765
5959
  // if configured to clone, do so now... otherwise just use same obj reference
5766
5960
  var obj = this.cloneObjects ? clone(doc, this.cloneMethod) : doc;
5961
+ if (!this.disableFreeze) {
5962
+ obj = unFreeze(obj);
5963
+ }
5767
5964
 
5768
- if (!this.disableMeta && typeof obj.meta === 'undefined') {
5769
- obj.meta = {
5770
- revision: 0,
5771
- created: 0
5772
- };
5965
+ if (!this.disableMeta) {
5966
+ if (typeof obj.meta === 'undefined') {
5967
+ obj.meta = {
5968
+ revision: 0,
5969
+ created: 0
5970
+ };
5971
+ } else if (!this.disableFreeze) {
5972
+ obj.meta = unFreeze(obj.meta);
5973
+ }
5773
5974
  }
5774
5975
 
5775
5976
  // both 'pre-insert' and 'insert' events are passed internal data reference even when cloning
@@ -5785,18 +5986,23 @@
5785
5986
  // (moved from "insert" event listener to allow internal reference to be used)
5786
5987
  if (this.disableChangesApi) {
5787
5988
  this.insertMeta(obj);
5788
- }
5789
- else {
5989
+ } else {
5790
5990
  this.insertMetaWithChange(obj);
5791
5991
  }
5792
5992
 
5993
+ if (!this.disableFreeze) {
5994
+ deepFreeze(obj);
5995
+ }
5996
+
5793
5997
  // if cloning is enabled, emit insert event with clone of new object
5794
5998
  returnObj = this.cloneObjects ? clone(obj, this.cloneMethod) : obj;
5999
+
5795
6000
  if (!bulkInsert) {
5796
6001
  this.emit('insert', returnObj);
5797
6002
  }
5798
6003
 
5799
6004
  this.addAutoUpdateObserver(returnObj);
6005
+
5800
6006
  return returnObj;
5801
6007
  };
5802
6008
 
@@ -5812,43 +6018,31 @@
5812
6018
  options = options || {};
5813
6019
 
5814
6020
  this.data = [];
5815
- this.idIndex = [];
6021
+ this.idIndex = null;
5816
6022
  this.cachedIndex = null;
5817
6023
  this.cachedBinaryIndex = null;
5818
6024
  this.cachedData = null;
5819
6025
  this.maxId = 0;
5820
6026
  this.DynamicViews = [];
5821
6027
  this.dirty = true;
6028
+ this.constraints = {
6029
+ unique: {},
6030
+ exact: {}
6031
+ };
5822
6032
 
5823
6033
  // if removing indices entirely
5824
6034
  if (options.removeIndices === true) {
5825
6035
  this.binaryIndices = {};
5826
-
5827
- this.constraints = {
5828
- unique: {},
5829
- exact: {}
5830
- };
5831
6036
  this.uniqueNames = [];
5832
6037
  }
5833
6038
  // clear indices but leave definitions in place
5834
6039
  else {
5835
6040
  // clear binary indices
5836
6041
  var keys = Object.keys(this.binaryIndices);
5837
- keys.forEach(function(biname) {
6042
+ keys.forEach(function (biname) {
5838
6043
  self.binaryIndices[biname].dirty = false;
5839
6044
  self.binaryIndices[biname].values = [];
5840
6045
  });
5841
-
5842
- // clear entire unique indices definition
5843
- this.constraints = {
5844
- unique: {},
5845
- exact: {}
5846
- };
5847
-
5848
- // add definitions back
5849
- this.uniqueNames.forEach(function(uiname) {
5850
- self.ensureUniqueIndex(uiname);
5851
- });
5852
6046
  }
5853
6047
  };
5854
6048
 
@@ -5873,7 +6067,7 @@
5873
6067
  }
5874
6068
 
5875
6069
  try {
5876
- for (k=0; k < len; k += 1) {
6070
+ for (k = 0; k < len; k += 1) {
5877
6071
  this.update(doc[k]);
5878
6072
  }
5879
6073
  }
@@ -5907,12 +6101,12 @@
5907
6101
  position = arr[1]; // position in data array
5908
6102
 
5909
6103
  // if configured to clone, do so now... otherwise just use same obj reference
5910
- newInternal = this.cloneObjects || !this.disableDeltaChangesApi ? clone(doc, this.cloneMethod) : doc;
6104
+ newInternal = this.cloneObjects || (!this.disableDeltaChangesApi && this.disableFreeze) ? clone(doc, this.cloneMethod) : doc;
5911
6105
 
5912
6106
  this.emit('pre-update', doc);
5913
6107
 
5914
- Object.keys(this.constraints.unique).forEach(function (key) {
5915
- self.constraints.unique[key].update(oldInternal, newInternal);
6108
+ this.uniqueNames.forEach(function (key) {
6109
+ self.getUniqueIndex(key, true).update(oldInternal, newInternal);
5916
6110
  });
5917
6111
 
5918
6112
  // operate the update
@@ -5952,10 +6146,13 @@
5952
6146
 
5953
6147
  // update meta and store changes if ChangesAPI is enabled
5954
6148
  if (this.disableChangesApi) {
5955
- this.updateMeta(newInternal, null);
6149
+ newInternal = this.updateMeta(newInternal);
6150
+ } else {
6151
+ newInternal = this.updateMetaWithChange(newInternal, oldInternal);
5956
6152
  }
5957
- else {
5958
- this.updateMetaWithChange(newInternal, oldInternal);
6153
+
6154
+ if (!this.disableFreeze) {
6155
+ deepFreeze(newInternal);
5959
6156
  }
5960
6157
 
5961
6158
  var returnObj;
@@ -6004,23 +6201,23 @@
6004
6201
  this.maxId = (this.data[this.data.length - 1].$loki + 1);
6005
6202
  }
6006
6203
 
6007
- obj.$loki = this.maxId;
6204
+ var newId = this.maxId;
6205
+ obj.$loki = newId;
6008
6206
 
6009
6207
  if (!this.disableMeta) {
6010
6208
  obj.meta.version = 0;
6011
6209
  }
6012
6210
 
6013
- var key, constrUnique = this.constraints.unique;
6014
- for (key in constrUnique) {
6015
- if (hasOwnProperty.call(constrUnique, key)) {
6016
- constrUnique[key].set(obj);
6017
- }
6211
+ for (var i = 0, len = this.uniqueNames.length; i < len; i ++) {
6212
+ this.getUniqueIndex(this.uniqueNames[i], true).set(obj);
6213
+ }
6214
+
6215
+ if (this.idIndex) {
6216
+ this.idIndex.push(newId);
6018
6217
  }
6019
6218
 
6020
- // add new obj id to idIndex
6021
- this.idIndex.push(obj.$loki);
6022
6219
  if (this.isIncremental) {
6023
- this.dirtyIds.push(obj.$loki);
6220
+ this.dirtyIds.push(newId);
6024
6221
  }
6025
6222
 
6026
6223
  // add the object
@@ -6031,14 +6228,14 @@
6031
6228
  // now that we can efficiently determine the data[] position of newly added document,
6032
6229
  // submit it for all registered DynamicViews to evaluate for inclusion/exclusion
6033
6230
  var dvlen = this.DynamicViews.length;
6034
- for (var i = 0; i < dvlen; i++) {
6231
+ for (i = 0; i < dvlen; i++) {
6035
6232
  this.DynamicViews[i].evaluateDocument(addedPos, true);
6036
6233
  }
6037
6234
 
6038
6235
  if (this.adaptiveBinaryIndices) {
6039
6236
  // for each binary index defined in collection, immediately update rather than flag for lazy rebuild
6040
6237
  var bIndices = this.binaryIndices;
6041
- for (key in bIndices) {
6238
+ for (var key in bIndices) {
6042
6239
  this.adaptiveBinaryIndexInsert(addedPos, key);
6043
6240
  }
6044
6241
  }
@@ -6065,7 +6262,7 @@
6065
6262
  * @param {function} updateFunction - update function to run against filtered documents
6066
6263
  * @memberof Collection
6067
6264
  */
6068
- Collection.prototype.updateWhere = function(filterFunction, updateFunction) {
6265
+ Collection.prototype.updateWhere = function (filterFunction, updateFunction) {
6069
6266
  var results = this.where(filterFunction),
6070
6267
  i = 0,
6071
6268
  obj;
@@ -6105,21 +6302,22 @@
6105
6302
  * Internal method to remove a batch of documents from the collection.
6106
6303
  * @param {number[]} positions - data/idIndex positions to remove
6107
6304
  */
6108
- Collection.prototype.removeBatchByPositions = function(positions) {
6305
+ Collection.prototype.removeBatchByPositions = function (positions) {
6109
6306
  var len = positions.length;
6110
6307
  var xo = {};
6111
6308
  var dlen, didx, idx;
6112
- var bic=Object.keys(this.binaryIndices).length;
6113
- var uic=Object.keys(this.constraints.unique).length;
6309
+ var bic = Object.keys(this.binaryIndices).length;
6310
+ var uic = Object.keys(this.constraints.unique).length;
6114
6311
  var adaptiveOverride = this.adaptiveBinaryIndices && Object.keys(this.binaryIndices).length > 0;
6115
- var doc, self=this;
6312
+ var doc, self = this;
6116
6313
 
6117
6314
  try {
6118
6315
  this.startTransaction();
6119
6316
 
6120
6317
  // create hashobject for positional removal inclusion tests...
6121
6318
  // all keys defined in this hashobject represent $loki ids of the documents to remove.
6122
- for(idx=0; idx < len; idx++) {
6319
+ this.ensureId();
6320
+ for (idx = 0; idx < len; idx++) {
6123
6321
  xo[this.idIndex[positions[idx]]] = true;
6124
6322
  }
6125
6323
 
@@ -6148,11 +6346,14 @@
6148
6346
  }
6149
6347
 
6150
6348
  if (uic) {
6151
- Object.keys(this.constraints.unique).forEach(function (key) {
6152
- for(idx=0; idx < len; idx++) {
6153
- doc = self.data[positions[idx]];
6154
- if (doc[key] !== null && doc[key] !== undefined) {
6155
- self.constraints.unique[key].remove(doc[key]);
6349
+ this.uniqueNames.forEach(function (key) {
6350
+ var index = self.getUniqueIndex(key);
6351
+ if (index) {
6352
+ for (idx = 0; idx < len; idx++) {
6353
+ doc = self.data[positions[idx]];
6354
+ if (doc[key] !== null && doc[key] !== undefined) {
6355
+ index.remove(doc[key]);
6356
+ }
6156
6357
  }
6157
6358
  }
6158
6359
  });
@@ -6163,14 +6364,14 @@
6163
6364
  // since data not removed yet, in future we can emit single delete event with array...
6164
6365
  // for now that might be breaking change to put in potential 1.6 or LokiDB (lokijs2) version
6165
6366
  if (!this.disableChangesApi || this.events.delete.length > 1) {
6166
- for(idx=0; idx < len; idx++) {
6367
+ for (idx = 0; idx < len; idx++) {
6167
6368
  this.emit('delete', this.data[positions[idx]]);
6168
6369
  }
6169
6370
  }
6170
6371
 
6171
6372
  // remove from data[] :
6172
6373
  // filter collection data for items not in inclusion hashobject
6173
- this.data = this.data.filter(function(obj) {
6374
+ this.data = this.data.filter(function (obj) {
6174
6375
  return !xo[obj.$loki];
6175
6376
  });
6176
6377
 
@@ -6182,8 +6383,8 @@
6182
6383
 
6183
6384
  // remove from idIndex[] :
6184
6385
  // filter idIndex for items not in inclusion hashobject
6185
- this.idIndex = this.idIndex.filter(function(id) {
6186
- return !xo[id];
6386
+ this.idIndex = this.idIndex.filter(function (id) {
6387
+ return !xo[id];
6187
6388
  });
6188
6389
 
6189
6390
  if (this.adaptiveBinaryIndices && adaptiveOverride) {
@@ -6212,21 +6413,21 @@
6212
6413
  * Internal method called by remove()
6213
6414
  * @param {object[]|number[]} batch - array of documents or $loki ids to remove
6214
6415
  */
6215
- Collection.prototype.removeBatch = function(batch) {
6416
+ Collection.prototype.removeBatch = function (batch) {
6216
6417
  var len = batch.length,
6217
- dlen=this.data.length,
6418
+ dlen = this.data.length,
6218
6419
  idx;
6219
6420
  var xlt = {};
6220
6421
  var posx = [];
6221
6422
 
6222
6423
  // create lookup hashobject to translate $loki id to position
6223
- for (idx=0; idx < dlen; idx++) {
6424
+ for (idx = 0; idx < dlen; idx++) {
6224
6425
  xlt[this.data[idx].$loki] = idx;
6225
6426
  }
6226
6427
 
6227
6428
  // iterate the batch
6228
- for (idx=0; idx < len; idx++) {
6229
- if (typeof(batch[idx]) === 'object') {
6429
+ for (idx = 0; idx < len; idx++) {
6430
+ if (typeof (batch[idx]) === 'object') {
6230
6431
  posx.push(xlt[batch[idx].$loki]);
6231
6432
  }
6232
6433
  else {
@@ -6243,6 +6444,8 @@
6243
6444
  * @memberof Collection
6244
6445
  */
6245
6446
  Collection.prototype.remove = function (doc) {
6447
+ var frozen;
6448
+
6246
6449
  if (typeof doc === 'number') {
6247
6450
  doc = this.get(doc);
6248
6451
  }
@@ -6265,9 +6468,12 @@
6265
6468
  // obj = arr[0],
6266
6469
  position = arr[1];
6267
6470
  var self = this;
6268
- Object.keys(this.constraints.unique).forEach(function (key) {
6471
+ this.uniqueNames.forEach(function (key) {
6269
6472
  if (doc[key] !== null && typeof doc[key] !== 'undefined') {
6270
- self.constraints.unique[key].remove(doc[key]);
6473
+ var index = self.getUniqueIndex(key);
6474
+ if (index) {
6475
+ index.remove(doc[key]);
6476
+ }
6271
6477
  }
6272
6478
  });
6273
6479
  // now that we can efficiently determine the data[] position of newly added document,
@@ -6300,8 +6506,15 @@
6300
6506
  this.commit();
6301
6507
  this.dirty = true; // for autosave scenarios
6302
6508
  this.emit('delete', arr[0]);
6509
+
6510
+ if (!this.disableFreeze) {
6511
+ doc = unFreeze(doc);
6512
+ }
6303
6513
  delete doc.$loki;
6304
6514
  delete doc.meta;
6515
+ if (!this.disableFreeze) {
6516
+ freeze(doc);
6517
+ }
6305
6518
  return doc;
6306
6519
 
6307
6520
  } catch (err) {
@@ -6325,6 +6538,10 @@
6325
6538
  * @memberof Collection
6326
6539
  */
6327
6540
  Collection.prototype.get = function (id, returnPosition) {
6541
+ if (!this.idIndex) {
6542
+ this.ensureId();
6543
+ }
6544
+
6328
6545
  var retpos = returnPosition || false,
6329
6546
  data = this.idIndex,
6330
6547
  max = data.length - 1,
@@ -6364,7 +6581,7 @@
6364
6581
  * @param {int} dataPosition : coll.data array index/position
6365
6582
  * @param {string} binaryIndexName : index to search for dataPosition in
6366
6583
  */
6367
- Collection.prototype.getBinaryIndexPosition = function(dataPosition, binaryIndexName) {
6584
+ Collection.prototype.getBinaryIndexPosition = function (dataPosition, binaryIndexName) {
6368
6585
  var val = Utils.getIn(this.data[dataPosition], binaryIndexName, true);
6369
6586
  var index = this.binaryIndices[binaryIndexName].values;
6370
6587
 
@@ -6383,7 +6600,7 @@
6383
6600
  // narrow down the sub-segment of index values
6384
6601
  // where the indexed property value exactly matches our
6385
6602
  // value and then linear scan to find exact -index- position
6386
- for(var idx = min; idx <= max; idx++) {
6603
+ for (var idx = min; idx <= max; idx++) {
6387
6604
  if (index[idx] === dataPosition) return idx;
6388
6605
  }
6389
6606
 
@@ -6396,7 +6613,7 @@
6396
6613
  * @param {int} dataPosition : coll.data array index/position
6397
6614
  * @param {string} binaryIndexName : index to search for dataPosition in
6398
6615
  */
6399
- Collection.prototype.adaptiveBinaryIndexInsert = function(dataPosition, binaryIndexName) {
6616
+ Collection.prototype.adaptiveBinaryIndexInsert = function (dataPosition, binaryIndexName) {
6400
6617
  var usingDotNotation = (binaryIndexName.indexOf('.') !== -1);
6401
6618
  var index = this.binaryIndices[binaryIndexName].values;
6402
6619
  var val = Utils.getIn(this.data[dataPosition], binaryIndexName, usingDotNotation);
@@ -6407,7 +6624,7 @@
6407
6624
  val = Utils.getIn(this.data[dataPosition], binaryIndexName);
6408
6625
  }
6409
6626
 
6410
- var idxPos = (index.length === 0)?0:this.calculateRangeStart(binaryIndexName, val, true, usingDotNotation);
6627
+ var idxPos = (index.length === 0) ? 0 : this.calculateRangeStart(binaryIndexName, val, true, usingDotNotation);
6411
6628
 
6412
6629
  // insert new data index into our binary index at the proper sorted location for relevant property calculated by idxPos.
6413
6630
  // doing this after adjusting dataPositions so no clash with previous item at that position.
@@ -6419,14 +6636,14 @@
6419
6636
  * @param {int} dataPosition : coll.data array index/position
6420
6637
  * @param {string} binaryIndexName : index to search for dataPosition in
6421
6638
  */
6422
- Collection.prototype.adaptiveBinaryIndexUpdate = function(dataPosition, binaryIndexName) {
6639
+ Collection.prototype.adaptiveBinaryIndexUpdate = function (dataPosition, binaryIndexName) {
6423
6640
  // linear scan needed to find old position within index unless we optimize for clone scenarios later
6424
6641
  // within (my) node 5.6.0, the following for() loop with strict compare is -much- faster than indexOf()
6425
6642
  var idxPos,
6426
6643
  index = this.binaryIndices[binaryIndexName].values,
6427
- len=index.length;
6644
+ len = index.length;
6428
6645
 
6429
- for(idxPos=0; idxPos < len; idxPos++) {
6646
+ for (idxPos = 0; idxPos < len; idxPos++) {
6430
6647
  if (index[idxPos] === dataPosition) break;
6431
6648
  }
6432
6649
 
@@ -6442,7 +6659,7 @@
6442
6659
  * @param {number|number[]} dataPosition : coll.data array index/position
6443
6660
  * @param {string} binaryIndexName : index to search for dataPosition in
6444
6661
  */
6445
- Collection.prototype.adaptiveBinaryIndexRemove = function(dataPosition, binaryIndexName, removedFromIndexOnly) {
6662
+ Collection.prototype.adaptiveBinaryIndexRemove = function (dataPosition, binaryIndexName, removedFromIndexOnly) {
6446
6663
  var bi = this.binaryIndices[binaryIndexName];
6447
6664
  var len, idx, rmidx, rmlen, rxo = {};
6448
6665
  var curr, shift, idxPos;
@@ -6456,12 +6673,12 @@
6456
6673
  }
6457
6674
  // we were passed an array (batch) of documents so use this 'batch optimized' algorithm
6458
6675
  else {
6459
- for(rmidx=0;rmidx<rmlen; rmidx++) {
6676
+ for (rmidx = 0; rmidx < rmlen; rmidx++) {
6460
6677
  rxo[dataPosition[rmidx]] = true;
6461
6678
  }
6462
6679
 
6463
6680
  // remove document from index (with filter function)
6464
- bi.values = bi.values.filter(function(di) { return !rxo[di]; });
6681
+ bi.values = bi.values.filter(function (di) { return !rxo[di]; });
6465
6682
 
6466
6683
  // if we passed this optional flag parameter, we are calling from adaptiveBinaryIndexUpdate,
6467
6684
  // in which case data positions stay the same.
@@ -6470,18 +6687,18 @@
6470
6687
  }
6471
6688
 
6472
6689
  var sortedPositions = dataPosition.slice();
6473
- sortedPositions.sort(function (a, b) { return a-b; });
6690
+ sortedPositions.sort(function (a, b) { return a - b; });
6474
6691
 
6475
6692
  // to remove holes, we need to 'shift down' the index's data array positions
6476
6693
  // we need to adjust array positions -1 for each index data positions greater than removed positions
6477
6694
  len = bi.values.length;
6478
- for (idx=0; idx<len; idx++) {
6479
- curr=bi.values[idx];
6480
- shift=0;
6481
- for(rmidx=0; rmidx<rmlen && curr > sortedPositions[rmidx]; rmidx++) {
6482
- shift++;
6695
+ for (idx = 0; idx < len; idx++) {
6696
+ curr = bi.values[idx];
6697
+ shift = 0;
6698
+ for (rmidx = 0; rmidx < rmlen && curr > sortedPositions[rmidx]; rmidx++) {
6699
+ shift++;
6483
6700
  }
6484
- bi.values[idx]-=shift;
6701
+ bi.values[idx] -= shift;
6485
6702
  }
6486
6703
 
6487
6704
  // batch processed, bail out
@@ -6565,11 +6782,11 @@
6565
6782
 
6566
6783
  // if not in index and our value is less than the found one
6567
6784
  if (Comparators.lt(val, Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), false)) {
6568
- return adaptive?lbound:lbound-1;
6785
+ return adaptive ? lbound : lbound - 1;
6569
6786
  }
6570
6787
 
6571
6788
  // not in index and our value is greater than the found one
6572
- return adaptive?lbound+1:lbound;
6789
+ return adaptive ? lbound + 1 : lbound;
6573
6790
  };
6574
6791
 
6575
6792
  /**
@@ -6608,14 +6825,14 @@
6608
6825
  return ubound;
6609
6826
  }
6610
6827
 
6611
- // if not in index and our value is less than the found one
6828
+ // if not in index and our value is less than the found one
6612
6829
  if (Comparators.gt(val, Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), false)) {
6613
- return ubound+1;
6830
+ return ubound + 1;
6614
6831
  }
6615
6832
 
6616
6833
  // either hole or first nonmatch
6617
- if (Comparators.aeq(val, Utils.getIn(rcd[index[ubound-1]], prop, usingDotNotation))) {
6618
- return ubound-1;
6834
+ if (Comparators.aeq(val, Utils.getIn(rcd[index[ubound - 1]], prop, usingDotNotation))) {
6835
+ return ubound - 1;
6619
6836
  }
6620
6837
 
6621
6838
  // hole, so ubound if nearest gt than the val we were looking for
@@ -6653,94 +6870,94 @@
6653
6870
 
6654
6871
  // if value falls outside of our range return [0, -1] to designate no results
6655
6872
  switch (op) {
6656
- case '$eq':
6657
- case '$aeq':
6658
- if (Comparators.lt(val, minVal, false) || Comparators.gt(val, maxVal, false)) {
6659
- return [0, -1];
6660
- }
6661
- break;
6662
- case '$dteq':
6663
- if (Comparators.lt(val, minVal, false) || Comparators.gt(val, maxVal, false)) {
6664
- return [0, -1];
6665
- }
6666
- break;
6667
- case '$gt':
6668
- // none are within range
6669
- if (Comparators.gt(val, maxVal, true)) {
6670
- return [0, -1];
6671
- }
6672
- // all are within range
6673
- if (Comparators.gt(minVal, val, false)) {
6674
- return [min, max];
6675
- }
6676
- break;
6677
- case '$gte':
6678
- // none are within range
6679
- if (Comparators.gt(val, maxVal, false)) {
6680
- return [0, -1];
6681
- }
6682
- // all are within range
6683
- if (Comparators.gt(minVal, val, true)) {
6873
+ case '$eq':
6874
+ case '$aeq':
6875
+ if (Comparators.lt(val, minVal, false) || Comparators.gt(val, maxVal, false)) {
6876
+ return [0, -1];
6877
+ }
6878
+ break;
6879
+ case '$dteq':
6880
+ if (Comparators.lt(val, minVal, false) || Comparators.gt(val, maxVal, false)) {
6881
+ return [0, -1];
6882
+ }
6883
+ break;
6884
+ case '$gt':
6885
+ // none are within range
6886
+ if (Comparators.gt(val, maxVal, true)) {
6887
+ return [0, -1];
6888
+ }
6889
+ // all are within range
6890
+ if (Comparators.gt(minVal, val, false)) {
6684
6891
  return [min, max];
6685
- }
6686
- break;
6687
- case '$lt':
6688
- // none are within range
6689
- if (Comparators.lt(val, minVal, true)) {
6690
- return [0, -1];
6691
- }
6692
- // all are within range
6693
- if (Comparators.lt(maxVal, val, false)) {
6694
- return [min, max];
6695
- }
6696
- break;
6697
- case '$lte':
6698
- // none are within range
6699
- if (Comparators.lt(val, minVal, false)) {
6700
- return [0, -1];
6701
- }
6702
- // all are within range
6703
- if (Comparators.lt(maxVal, val, true)) {
6704
- return [min, max];
6705
- }
6706
- break;
6707
- case '$between':
6708
- // none are within range (low range is greater)
6709
- if (Comparators.gt(val[0], maxVal, false)) {
6710
- return [0, -1];
6711
- }
6712
- // none are within range (high range lower)
6713
- if (Comparators.lt(val[1], minVal, false)) {
6714
- return [0, -1];
6715
- }
6892
+ }
6893
+ break;
6894
+ case '$gte':
6895
+ // none are within range
6896
+ if (Comparators.gt(val, maxVal, false)) {
6897
+ return [0, -1];
6898
+ }
6899
+ // all are within range
6900
+ if (Comparators.gt(minVal, val, true)) {
6901
+ return [min, max];
6902
+ }
6903
+ break;
6904
+ case '$lt':
6905
+ // none are within range
6906
+ if (Comparators.lt(val, minVal, true)) {
6907
+ return [0, -1];
6908
+ }
6909
+ // all are within range
6910
+ if (Comparators.lt(maxVal, val, false)) {
6911
+ return [min, max];
6912
+ }
6913
+ break;
6914
+ case '$lte':
6915
+ // none are within range
6916
+ if (Comparators.lt(val, minVal, false)) {
6917
+ return [0, -1];
6918
+ }
6919
+ // all are within range
6920
+ if (Comparators.lt(maxVal, val, true)) {
6921
+ return [min, max];
6922
+ }
6923
+ break;
6924
+ case '$between':
6925
+ // none are within range (low range is greater)
6926
+ if (Comparators.gt(val[0], maxVal, false)) {
6927
+ return [0, -1];
6928
+ }
6929
+ // none are within range (high range lower)
6930
+ if (Comparators.lt(val[1], minVal, false)) {
6931
+ return [0, -1];
6932
+ }
6716
6933
 
6717
- lbound = this.calculateRangeStart(prop, val[0], false, usingDotNotation);
6718
- ubound = this.calculateRangeEnd(prop, val[1], usingDotNotation);
6934
+ lbound = this.calculateRangeStart(prop, val[0], false, usingDotNotation);
6935
+ ubound = this.calculateRangeEnd(prop, val[1], usingDotNotation);
6719
6936
 
6720
- if (lbound < 0) lbound++;
6721
- if (ubound > max) ubound--;
6937
+ if (lbound < 0) lbound++;
6938
+ if (ubound > max) ubound--;
6722
6939
 
6723
- if (!Comparators.gt(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val[0], true)) lbound++;
6724
- if (!Comparators.lt(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val[1], true)) ubound--;
6940
+ if (!Comparators.gt(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val[0], true)) lbound++;
6941
+ if (!Comparators.lt(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val[1], true)) ubound--;
6725
6942
 
6726
- if (ubound < lbound) return [0, -1];
6943
+ if (ubound < lbound) return [0, -1];
6727
6944
 
6728
- return ([lbound, ubound]);
6729
- case '$in':
6730
- var idxset = [],
6731
- segResult = [];
6732
- // query each value '$eq' operator and merge the seqment results.
6733
- for (var j = 0, len = val.length; j < len; j++) {
6945
+ return ([lbound, ubound]);
6946
+ case '$in':
6947
+ var idxset = [],
6948
+ segResult = [];
6949
+ // query each value '$eq' operator and merge the seqment results.
6950
+ for (var j = 0, len = val.length; j < len; j++) {
6734
6951
  var seg = this.calculateRange('$eq', prop, val[j]);
6735
6952
 
6736
6953
  for (var i = seg[0]; i <= seg[1]; i++) {
6737
- if (idxset[i] === undefined) {
6738
- idxset[i] = true;
6739
- segResult.push(i);
6740
- }
6954
+ if (idxset[i] === undefined) {
6955
+ idxset[i] = true;
6956
+ segResult.push(i);
6957
+ }
6741
6958
  }
6742
- }
6743
- return segResult;
6959
+ }
6960
+ return segResult;
6744
6961
  }
6745
6962
 
6746
6963
  // determine lbound where needed
@@ -6750,8 +6967,8 @@
6750
6967
  case '$dteq':
6751
6968
  case '$gte':
6752
6969
  case '$lt':
6753
- lbound = this.calculateRangeStart(prop, val, false, usingDotNotation);
6754
- lval = Utils.getIn(rcd[index[lbound]], prop, usingDotNotation);
6970
+ lbound = this.calculateRangeStart(prop, val, false, usingDotNotation);
6971
+ lval = Utils.getIn(rcd[index[lbound]], prop, usingDotNotation);
6755
6972
  break;
6756
6973
  default: break;
6757
6974
  }
@@ -6771,50 +6988,50 @@
6771
6988
 
6772
6989
 
6773
6990
  switch (op) {
6774
- case '$eq':
6775
- case '$aeq':
6776
- case '$dteq':
6777
- // if hole (not found)
6778
- if (!Comparators.aeq(lval, val)) {
6779
- return [0, -1];
6780
- }
6991
+ case '$eq':
6992
+ case '$aeq':
6993
+ case '$dteq':
6994
+ // if hole (not found)
6995
+ if (!Comparators.aeq(lval, val)) {
6996
+ return [0, -1];
6997
+ }
6781
6998
 
6782
- return [lbound, ubound];
6999
+ return [lbound, ubound];
6783
7000
 
6784
- case '$gt':
6785
- // if hole (not found) ub position is already greater
6786
- if (!Comparators.aeq(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val)) {
6787
- return [ubound, max];
6788
- }
6789
- // otherwise (found) so ubound is still equal, get next
6790
- return [ubound+1, max];
7001
+ case '$gt':
7002
+ // if hole (not found) ub position is already greater
7003
+ if (!Comparators.aeq(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val)) {
7004
+ return [ubound, max];
7005
+ }
7006
+ // otherwise (found) so ubound is still equal, get next
7007
+ return [ubound + 1, max];
6791
7008
 
6792
- case '$gte':
6793
- // if hole (not found) lb position marks left outside of range
6794
- if (!Comparators.aeq(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val)) {
6795
- return [lbound+1, max];
6796
- }
6797
- // otherwise (found) so lb is first position where its equal
6798
- return [lbound, max];
7009
+ case '$gte':
7010
+ // if hole (not found) lb position marks left outside of range
7011
+ if (!Comparators.aeq(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val)) {
7012
+ return [lbound + 1, max];
7013
+ }
7014
+ // otherwise (found) so lb is first position where its equal
7015
+ return [lbound, max];
6799
7016
 
6800
- case '$lt':
6801
- // if hole (not found) position already is less than
6802
- if (!Comparators.aeq(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val)) {
6803
- return [min, lbound];
6804
- }
6805
- // otherwise (found) so lb marks left inside of eq range, get previous
6806
- return [min, lbound-1];
7017
+ case '$lt':
7018
+ // if hole (not found) position already is less than
7019
+ if (!Comparators.aeq(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val)) {
7020
+ return [min, lbound];
7021
+ }
7022
+ // otherwise (found) so lb marks left inside of eq range, get previous
7023
+ return [min, lbound - 1];
6807
7024
 
6808
- case '$lte':
6809
- // if hole (not found) ub position marks right outside so get previous
6810
- if (!Comparators.aeq(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val)) {
6811
- return [min, ubound-1];
6812
- }
6813
- // otherwise (found) so ub is last position where its still equal
6814
- return [min, ubound];
7025
+ case '$lte':
7026
+ // if hole (not found) ub position marks right outside so get previous
7027
+ if (!Comparators.aeq(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val)) {
7028
+ return [min, ubound - 1];
7029
+ }
7030
+ // otherwise (found) so ub is last position where its still equal
7031
+ return [min, ubound];
6815
7032
 
6816
- default:
6817
- return [0, rcd.length - 1];
7033
+ default:
7034
+ return [0, rcd.length - 1];
6818
7035
  }
6819
7036
  };
6820
7037
 
@@ -6834,7 +7051,7 @@
6834
7051
  };
6835
7052
  }
6836
7053
 
6837
- var result = this.constraints.unique[field].get(value);
7054
+ var result = this.getUniqueIndex(field, true).get(value);
6838
7055
  if (!this.cloneObjects) {
6839
7056
  return result;
6840
7057
  } else {
@@ -6852,7 +7069,7 @@
6852
7069
  query = query || {};
6853
7070
 
6854
7071
  // Instantiate Resultset and exec find op passing firstOnly = true param
6855
- var result = this.chain().find(query,true).data();
7072
+ var result = this.chain().find(query, true).data();
6856
7073
 
6857
7074
  if (Array.isArray(result) && result.length === 0) {
6858
7075
  return null;
@@ -7335,7 +7552,7 @@
7335
7552
  };
7336
7553
  }
7337
7554
 
7338
- function KeyValueStore() {}
7555
+ function KeyValueStore() { }
7339
7556
 
7340
7557
  KeyValueStore.prototype = {
7341
7558
  keys: [],
@@ -7365,8 +7582,8 @@
7365
7582
 
7366
7583
  function UniqueIndex(uniqueField) {
7367
7584
  this.field = uniqueField;
7368
- this.keyMap = {};
7369
- this.lokiMap = {};
7585
+ this.keyMap = Object.create(null);
7586
+ this.lokiMap = Object.create(null);
7370
7587
  }
7371
7588
  UniqueIndex.prototype.keyMap = {};
7372
7589
  UniqueIndex.prototype.lokiMap = {};
@@ -7406,6 +7623,7 @@
7406
7623
  UniqueIndex.prototype.remove = function (key) {
7407
7624
  var obj = this.keyMap[key];
7408
7625
  if (obj !== null && typeof obj !== 'undefined') {
7626
+ // avoid using `delete`
7409
7627
  this.keyMap[key] = undefined;
7410
7628
  this.lokiMap[obj.$loki] = undefined;
7411
7629
  } else {
@@ -7413,12 +7631,12 @@
7413
7631
  }
7414
7632
  };
7415
7633
  UniqueIndex.prototype.clear = function () {
7416
- this.keyMap = {};
7417
- this.lokiMap = {};
7634
+ this.keyMap = Object.create(null);
7635
+ this.lokiMap = Object.create(null);
7418
7636
  };
7419
7637
 
7420
7638
  function ExactIndex(exactField) {
7421
- this.index = {};
7639
+ this.index = Object.create(null);
7422
7640
  this.field = exactField;
7423
7641
  }
7424
7642
 
@@ -7539,9 +7757,13 @@
7539
7757
  }
7540
7758
  };
7541
7759
 
7542
-
7760
+ Loki.deepFreeze = deepFreeze;
7761
+ Loki.freeze = freeze;
7762
+ Loki.unFreeze = unFreeze;
7543
7763
  Loki.LokiOps = LokiOps;
7544
7764
  Loki.Collection = Collection;
7765
+ Loki.DynamicView = DynamicView;
7766
+ Loki.Resultset = Resultset;
7545
7767
  Loki.KeyValueStore = KeyValueStore;
7546
7768
  Loki.LokiMemoryAdapter = LokiMemoryAdapter;
7547
7769
  Loki.LokiPartitioningAdapter = LokiPartitioningAdapter;