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

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,7 @@
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
+ if (self.autosaveDirty() && !self.ignoreAutosave) {
2856
2944
  self.saveDatabase(callback);
2857
2945
  }
2858
2946
  }, delay);
@@ -3031,46 +3119,46 @@
3031
3119
  step = transform[idx];
3032
3120
 
3033
3121
  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;
3122
+ case "find":
3123
+ rs.find(step.value);
3124
+ break;
3125
+ case "where":
3126
+ rs.where(step.value);
3127
+ break;
3128
+ case "simplesort":
3129
+ rs.simplesort(step.property, step.desc || step.options);
3130
+ break;
3131
+ case "compoundsort":
3132
+ rs.compoundsort(step.value);
3133
+ break;
3134
+ case "sort":
3135
+ rs.sort(step.value);
3136
+ break;
3137
+ case "limit":
3138
+ rs = rs.limit(step.value);
3139
+ break; // limit makes copy so update reference
3140
+ case "offset":
3141
+ rs = rs.offset(step.value);
3142
+ break; // offset makes copy so update reference
3143
+ case "map":
3144
+ rs = rs.map(step.value, step.dataOptions);
3145
+ break;
3146
+ case "eqJoin":
3147
+ rs = rs.eqJoin(step.joinData, step.leftJoinKey, step.rightJoinKey, step.mapFun, step.dataOptions);
3148
+ break;
3061
3149
  // 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;
3150
+ case "mapReduce":
3151
+ rs = rs.mapReduce(step.mapFunction, step.reduceFunction);
3152
+ break;
3065
3153
  // 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;
3154
+ case "update":
3155
+ rs.update(step.value);
3156
+ break;
3157
+ case "remove":
3158
+ rs.remove();
3159
+ break;
3160
+ default:
3161
+ break;
3074
3162
  }
3075
3163
  }
3076
3164
 
@@ -3174,7 +3262,7 @@
3174
3262
  if (!options.disableIndexIntersect && hasBinaryIndex) {
3175
3263
 
3176
3264
  // calculate filter efficiency
3177
- eff = dc/frl;
3265
+ eff = dc / frl;
3178
3266
 
3179
3267
  // when javascript sort fallback is enabled, you generally need more than ~17% of total docs in resultset
3180
3268
  // before array intersect is determined to be the faster algorithm, otherwise leave at 10% for loki sort.
@@ -3185,17 +3273,17 @@
3185
3273
  // anything more than ratio of 10:1 (total documents/current results) should use old sort code path
3186
3274
  // So we will only use array intersection if you have more than 10% of total docs in your current resultset.
3187
3275
  if (eff <= targetEff || options.forceIndexIntersect) {
3188
- var idx, fr=this.filteredrows;
3276
+ var idx, fr = this.filteredrows;
3189
3277
  var io = {};
3190
3278
  // set up hashobject for simple 'inclusion test' with existing (filtered) results
3191
- for(idx=0; idx<frl; idx++) {
3279
+ for (idx = 0; idx < frl; idx++) {
3192
3280
  io[fr[idx]] = true;
3193
3281
  }
3194
3282
  // grab full sorted binary index array
3195
3283
  var pv = this.collection.binaryIndices[propname].values;
3196
3284
 
3197
3285
  // filter by existing results
3198
- this.filteredrows = pv.filter(function(n) { return io[n]; });
3286
+ this.filteredrows = pv.filter(function (n) { return io[n]; });
3199
3287
 
3200
3288
  if (options.desc) {
3201
3289
  this.filteredrows.reverse();
@@ -3210,7 +3298,7 @@
3210
3298
 
3211
3299
  // if we have opted to use simplified javascript comparison function...
3212
3300
  if (options.useJavascriptSorting) {
3213
- return this.sort(function(obj1, obj2) {
3301
+ return this.sort(function (obj1, obj2) {
3214
3302
  if (obj1[propname] === obj2[propname]) return 0;
3215
3303
  if (obj1[propname] > obj2[propname]) return 1;
3216
3304
  if (obj1[propname] < obj2[propname]) return -1;
@@ -3333,7 +3421,7 @@
3333
3421
  Resultset.prototype.$or = Resultset.prototype.findOr;
3334
3422
 
3335
3423
  // precompile recursively
3336
- function precompileQuery (operator, value) {
3424
+ function precompileQuery(operator, value) {
3337
3425
  // for regex ops, precompile
3338
3426
  if (operator === '$regex') {
3339
3427
  if (Array.isArray(value)) {
@@ -3492,6 +3580,12 @@
3492
3580
  index = this.collection.binaryIndices[property];
3493
3581
  }
3494
3582
 
3583
+ // opportunistically speed up $in searches from O(n*m) to O(n*log m)
3584
+ if (!searchByIndex && operator === '$in' && Array.isArray(value) && typeof Set !== 'undefined') {
3585
+ value = new Set(value);
3586
+ operator = '$inSet';
3587
+ }
3588
+
3495
3589
  // the comparison function
3496
3590
  var fun = LokiOps[operator];
3497
3591
 
@@ -3507,7 +3601,7 @@
3507
3601
  //
3508
3602
  // For performance reasons, each case has its own if block to minimize in-loop calculations
3509
3603
 
3510
- var filter, rowIdx = 0;
3604
+ var filter, rowIdx = 0, record;
3511
3605
 
3512
3606
  // If the filteredrows[] is already initialized, use it
3513
3607
  if (this.filterInitialized) {
@@ -3517,9 +3611,10 @@
3517
3611
  // currently supporting dot notation for non-indexed conditions only
3518
3612
  if (usingDotNotation) {
3519
3613
  property = property.split('.');
3520
- for(i=0; i<len; i++) {
3614
+ for (i = 0; i < len; i++) {
3521
3615
  rowIdx = filter[i];
3522
- if (dotSubScan(t[rowIdx], property, fun, value)) {
3616
+ record = t[rowIdx];
3617
+ if (dotSubScan(record, property, fun, value, record)) {
3523
3618
  result.push(rowIdx);
3524
3619
  if (firstOnly) {
3525
3620
  this.filteredrows = result;
@@ -3528,9 +3623,10 @@
3528
3623
  }
3529
3624
  }
3530
3625
  } else {
3531
- for(i=0; i<len; i++) {
3626
+ for (i = 0; i < len; i++) {
3532
3627
  rowIdx = filter[i];
3533
- if (fun(t[rowIdx][property], value)) {
3628
+ record = t[rowIdx];
3629
+ if (fun(record[property], value, record)) {
3534
3630
  result.push(rowIdx);
3535
3631
  if (firstOnly) {
3536
3632
  this.filteredrows = result;
@@ -3548,8 +3644,9 @@
3548
3644
 
3549
3645
  if (usingDotNotation) {
3550
3646
  property = property.split('.');
3551
- for(i=0; i<len; i++) {
3552
- if (dotSubScan(t[i], property, fun, value)) {
3647
+ for (i = 0; i < len; i++) {
3648
+ record = t[i];
3649
+ if (dotSubScan(record, property, fun, value, record)) {
3553
3650
  result.push(i);
3554
3651
  if (firstOnly) {
3555
3652
  this.filteredrows = result;
@@ -3559,8 +3656,9 @@
3559
3656
  }
3560
3657
  }
3561
3658
  } else {
3562
- for(i=0; i<len; i++) {
3563
- if (fun(t[i][property], value)) {
3659
+ for (i = 0; i < len; i++) {
3660
+ record = t[i];
3661
+ if (fun(record[property], value, record)) {
3564
3662
  result.push(i);
3565
3663
  if (firstOnly) {
3566
3664
  this.filteredrows = result;
@@ -3588,12 +3686,12 @@
3588
3686
  }
3589
3687
  }
3590
3688
  else {
3591
- result.push(index.values[i]);
3592
- if (firstOnly) {
3593
- this.filteredrows = result;
3594
- this.filterInitialized = true;
3595
- return this;
3596
- }
3689
+ result.push(index.values[i]);
3690
+ if (firstOnly) {
3691
+ this.filteredrows = result;
3692
+ this.filterInitialized = true;
3693
+ return this;
3694
+ }
3597
3695
  }
3598
3696
  }
3599
3697
  } else {
@@ -3716,7 +3814,8 @@
3716
3814
  }
3717
3815
 
3718
3816
  // 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) {
3817
+ // if collection is immutable freeze and unFreeze takes care of cloning
3818
+ if (!this.collection.disableDeltaChangesApi && this.collection.disableFreeze) {
3720
3819
  options.forceClones = true;
3721
3820
  options.forceCloneMethod = 'parse-stringify';
3722
3821
  }
@@ -3728,7 +3827,6 @@
3728
3827
  if (this.collection.cloneObjects || options.forceClones) {
3729
3828
  len = data.length;
3730
3829
  method = options.forceCloneMethod || this.collection.cloneMethod;
3731
-
3732
3830
  for (i = 0; i < len; i++) {
3733
3831
  obj = clone(data[i], method);
3734
3832
  if (options.removeMeta) {
@@ -3798,7 +3896,7 @@
3798
3896
  // pass in each document object currently in resultset to user supplied updateFunction
3799
3897
  for (var idx = 0; idx < len; idx++) {
3800
3898
  // if we have cloning option specified or are doing differential delta changes, clone object first
3801
- if (this.collection.cloneObjects || !this.collection.disableDeltaChangesApi) {
3899
+ if (!this.disableFreeze || this.collection.cloneObjects || !this.collection.disableDeltaChangesApi) {
3802
3900
  obj = clone(rcd[this.filteredrows[idx]], this.collection.cloneMethod);
3803
3901
  updateFunction(obj);
3804
3902
  this.collection.update(obj);
@@ -4059,6 +4157,9 @@
4059
4157
 
4060
4158
  // keep ordered filter pipeline
4061
4159
  this.filterPipeline = [];
4160
+ if (!this.collection.disableFreeze) {
4161
+ Object.freeze(this.filterPipeline);
4162
+ }
4062
4163
 
4063
4164
  // sorting member variables
4064
4165
  // we only support one active search, applied using applySort() or applySimpleSort()
@@ -4071,12 +4172,23 @@
4071
4172
  // once we refactor transactions, i will tie in certain transactional events
4072
4173
 
4073
4174
  this.events = {
4074
- 'rebuild': []
4175
+ 'rebuild': [],
4176
+ 'filter': [],
4177
+ 'sort': []
4075
4178
  };
4076
4179
  }
4077
4180
 
4078
4181
  DynamicView.prototype = new LokiEventEmitter();
4182
+ DynamicView.prototype.constructor = DynamicView;
4079
4183
 
4184
+ /**
4185
+ * getSort() - used to get the current sort
4186
+ *
4187
+ * @returns function (sortFunction) or array (sortCriteria) or object (sortCriteriaSimple)
4188
+ */
4189
+ DynamicView.prototype.getSort = function () {
4190
+ return this.sortFunction || this.sortCriteria || this.sortCriteriaSimple;
4191
+ };
4080
4192
 
4081
4193
  /**
4082
4194
  * rematerialize() - internally used immediately after deserialization (loading)
@@ -4104,9 +4216,13 @@
4104
4216
  this.sortDirty = true;
4105
4217
  }
4106
4218
 
4219
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4107
4220
  if (options.hasOwnProperty('removeWhereFilters')) {
4108
4221
  // for each view see if it had any where filters applied... since they don't
4109
4222
  // serialize those functions lets remove those invalid filters
4223
+ if (wasFrozen) {
4224
+ this.filterPipeline = this.filterPipeline.slice();
4225
+ }
4110
4226
  fpl = this.filterPipeline.length;
4111
4227
  fpi = fpl;
4112
4228
  while (fpi--) {
@@ -4114,7 +4230,6 @@
4114
4230
  if (fpi !== this.filterPipeline.length - 1) {
4115
4231
  this.filterPipeline[fpi] = this.filterPipeline[this.filterPipeline.length - 1];
4116
4232
  }
4117
-
4118
4233
  this.filterPipeline.length--;
4119
4234
  }
4120
4235
  }
@@ -4127,7 +4242,10 @@
4127
4242
  // now re-apply 'find' filterPipeline ops
4128
4243
  fpl = ofp.length;
4129
4244
  for (idx = 0; idx < fpl; idx++) {
4130
- this.applyFind(ofp[idx].val);
4245
+ this.applyFind(ofp[idx].val, ofp[idx].uid);
4246
+ }
4247
+ if (wasFrozen) {
4248
+ Object.freeze(this.filterPipeline);
4131
4249
  }
4132
4250
 
4133
4251
  // during creation of unit tests, i will remove this forced refresh and leave lazy
@@ -4184,7 +4302,6 @@
4184
4302
  */
4185
4303
  DynamicView.prototype.toJSON = function () {
4186
4304
  var copy = new DynamicView(this.collection, this.name, this.options);
4187
-
4188
4305
  copy.resultset = this.resultset;
4189
4306
  copy.resultdata = []; // let's not save data (copy) to minimize size
4190
4307
  copy.resultsdirty = true;
@@ -4217,8 +4334,13 @@
4217
4334
 
4218
4335
  this.cachedresultset = null;
4219
4336
 
4337
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4338
+ var filterChanged = this.filterPipeline.length > 0;
4220
4339
  // keep ordered filter pipeline
4221
4340
  this.filterPipeline = [];
4341
+ if (wasFrozen) {
4342
+ Object.freeze(this.filterPipeline);
4343
+ }
4222
4344
 
4223
4345
  // sorting member variables
4224
4346
  // we only support one active search, applied using applySort() or applySimpleSort()
@@ -4230,6 +4352,10 @@
4230
4352
  if (options.queueSortPhase === true) {
4231
4353
  this.queueSortPhase();
4232
4354
  }
4355
+
4356
+ if (filterChanged) {
4357
+ this.emit('filter');
4358
+ }
4233
4359
  };
4234
4360
 
4235
4361
  /**
@@ -4251,6 +4377,7 @@
4251
4377
  this.sortCriteriaSimple = null;
4252
4378
 
4253
4379
  this.queueSortPhase();
4380
+ this.emit('sort');
4254
4381
 
4255
4382
  return this;
4256
4383
  };
@@ -4271,10 +4398,14 @@
4271
4398
  */
4272
4399
  DynamicView.prototype.applySimpleSort = function (propname, options) {
4273
4400
  this.sortCriteriaSimple = { propname: propname, options: options || false };
4401
+ if (!this.collection.disableFreeze) {
4402
+ deepFreeze(this.sortCriteriaSimple);
4403
+ }
4274
4404
  this.sortCriteria = null;
4275
4405
  this.sortFunction = null;
4276
4406
 
4277
4407
  this.queueSortPhase();
4408
+ this.emit('sort');
4278
4409
 
4279
4410
  return this;
4280
4411
  };
@@ -4295,11 +4426,14 @@
4295
4426
  */
4296
4427
  DynamicView.prototype.applySortCriteria = function (criteria) {
4297
4428
  this.sortCriteria = criteria;
4429
+ if (!this.collection.disableFreeze) {
4430
+ deepFreeze(this.sortCriteria);
4431
+ }
4298
4432
  this.sortCriteriaSimple = null;
4299
4433
  this.sortFunction = null;
4300
4434
 
4301
4435
  this.queueSortPhase();
4302
-
4436
+ this.emit('sort');
4303
4437
  return this;
4304
4438
  };
4305
4439
 
@@ -4370,7 +4504,17 @@
4370
4504
  * @param {object} filter - The filter object. Refer to applyFilter() for extra details.
4371
4505
  */
4372
4506
  DynamicView.prototype._addFilter = function (filter) {
4507
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4508
+ if (wasFrozen) {
4509
+ this.filterPipeline = this.filterPipeline.slice();
4510
+ }
4511
+ if (!this.collection.disableFreeze) {
4512
+ deepFreeze(filter);
4513
+ }
4373
4514
  this.filterPipeline.push(filter);
4515
+ if (wasFrozen) {
4516
+ Object.freeze(this.filterPipeline);
4517
+ }
4374
4518
  this.resultset[filter.type](filter.val);
4375
4519
  };
4376
4520
 
@@ -4389,18 +4533,22 @@
4389
4533
  }
4390
4534
 
4391
4535
  var filters = this.filterPipeline;
4536
+ var wasFrozen = Object.isFrozen(filters);
4392
4537
  this.filterPipeline = [];
4393
4538
 
4394
4539
  for (var idx = 0, len = filters.length; idx < len; idx += 1) {
4395
4540
  this._addFilter(filters[idx]);
4396
4541
  }
4542
+ if (wasFrozen) {
4543
+ Object.freeze(this.filterPipeline);
4544
+ }
4397
4545
 
4398
4546
  if (this.sortFunction || this.sortCriteria || this.sortCriteriaSimple) {
4399
4547
  this.queueSortPhase();
4400
4548
  } else {
4401
4549
  this.queueRebuildEvent();
4402
4550
  }
4403
-
4551
+ this.emit('filter');
4404
4552
  return this;
4405
4553
  };
4406
4554
 
@@ -4415,7 +4563,15 @@
4415
4563
  DynamicView.prototype.applyFilter = function (filter) {
4416
4564
  var idx = this._indexOfFilterWithId(filter.uid);
4417
4565
  if (idx >= 0) {
4566
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4567
+ if (wasFrozen) {
4568
+ this.filterPipeline = this.filterPipeline.slice();
4569
+ }
4418
4570
  this.filterPipeline[idx] = filter;
4571
+ if (wasFrozen) {
4572
+ freeze(filter);
4573
+ Object.freeze(this.filterPipeline);
4574
+ }
4419
4575
  return this.reapplyFilters();
4420
4576
  }
4421
4577
 
@@ -4433,6 +4589,7 @@
4433
4589
  this.queueRebuildEvent();
4434
4590
  }
4435
4591
 
4592
+ this.emit('filter');
4436
4593
  return this;
4437
4594
  };
4438
4595
 
@@ -4482,8 +4639,14 @@
4482
4639
  if (idx < 0) {
4483
4640
  throw new Error("Dynamic view does not contain a filter with ID: " + uid);
4484
4641
  }
4485
-
4642
+ var wasFrozen = Object.isFrozen(this.filterPipeline);
4643
+ if (wasFrozen) {
4644
+ this.filterPipeline = this.filterPipeline.slice();
4645
+ }
4486
4646
  this.filterPipeline.splice(idx, 1);
4647
+ if (wasFrozen) {
4648
+ Object.freeze(this.filterPipeline);
4649
+ }
4487
4650
  this.reapplyFilters();
4488
4651
  return this;
4489
4652
  };
@@ -4743,23 +4906,23 @@
4743
4906
 
4744
4907
  rmlen = objIndex.length;
4745
4908
  // create intersection object of data indices to remove
4746
- for(rmidx=0;rmidx<rmlen; rmidx++) {
4909
+ for (rmidx = 0; rmidx < rmlen; rmidx++) {
4747
4910
  rxo[objIndex[rmidx]] = true;
4748
4911
  }
4749
4912
 
4750
4913
  // pivot remove data indices into remove filteredrows indices and dump in hashobject
4751
- for (idx=0; idx<frlen; idx++) {
4914
+ for (idx = 0; idx < frlen; idx++) {
4752
4915
  if (rxo[fr[idx]]) fxo[idx] = true;
4753
4916
  }
4754
4917
 
4755
4918
  // if any of the removed items were in our filteredrows...
4756
4919
  if (Object.keys(fxo).length > 0) {
4757
4920
  // remove them from filtered rows
4758
- this.resultset.filteredrows = this.resultset.filteredrows.filter(function(di, idx) { return !fxo[idx]; });
4921
+ this.resultset.filteredrows = this.resultset.filteredrows.filter(function (di, idx) { return !fxo[idx]; });
4759
4922
  // if persistent...
4760
4923
  if (this.options.persistent) {
4761
4924
  // remove from resultdata
4762
- this.resultdata = this.resultdata.filter(function(obj, idx) { return !fxo[idx]; });
4925
+ this.resultdata = this.resultdata.filter(function (obj, idx) { return !fxo[idx]; });
4763
4926
  }
4764
4927
 
4765
4928
  // and queue sorts
@@ -4771,7 +4934,7 @@
4771
4934
  }
4772
4935
 
4773
4936
  // 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]; }; };
4937
+ var filt = function (idx) { return function (di) { return di < drs.filteredrows[idx]; }; };
4775
4938
 
4776
4939
  frlen = drs.filteredrows.length;
4777
4940
  for (idx = 0; idx < frlen; idx++) {
@@ -4817,6 +4980,7 @@
4817
4980
  * @param {boolean} [options.autoupdate=false] - use Object.observe to update objects automatically
4818
4981
  * @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user
4819
4982
  * @param {boolean} [options.serializableIndices=true[]] - converts date values on binary indexed properties to epoch time
4983
+ * @param {boolean} [options.disableFreeze=true] - when false all docs are frozen
4820
4984
  * @param {string} [options.cloneMethod='parse-stringify'] - 'parse-stringify', 'jquery-extend-deep', 'shallow', 'shallow-assign'
4821
4985
  * @param {int=} options.ttl - age of document (in ms.) before document is considered aged/stale.
4822
4986
  * @param {int=} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default.
@@ -4828,7 +4992,7 @@
4828
4992
  this.name = name;
4829
4993
  // the data held by the collection
4830
4994
  this.data = [];
4831
- this.idIndex = []; // index of id
4995
+ this.idIndex = null; // position->$loki index (built lazily)
4832
4996
  this.binaryIndices = {}; // user defined indexes
4833
4997
  this.constraints = {
4834
4998
  unique: {},
@@ -4836,7 +5000,7 @@
4836
5000
  };
4837
5001
 
4838
5002
  // 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
5003
+ // we will keep track of properties which have unique contraint applied here, and regenerate lazily
4840
5004
  this.uniqueNames = [];
4841
5005
 
4842
5006
  // transforms will be used to store frequently used query chains as a series of steps
@@ -4865,9 +5029,9 @@
4865
5029
  if (!Array.isArray(options.unique)) {
4866
5030
  options.unique = [options.unique];
4867
5031
  }
5032
+ // save names; actual index is built lazily
4868
5033
  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);
5034
+ self.uniqueNames.push(prop);
4871
5035
  });
4872
5036
  }
4873
5037
 
@@ -4913,6 +5077,9 @@
4913
5077
  // same 'after' serialization as it was 'before'.
4914
5078
  this.serializableIndices = options.hasOwnProperty('serializableIndices') ? options.serializableIndices : true;
4915
5079
 
5080
+ // option to deep freeze all documents
5081
+ this.disableFreeze = options.hasOwnProperty('disableFreeze') ? options.disableFreeze : true;
5082
+
4916
5083
  //option to activate a cleaner daemon - clears "aged" documents at set intervals.
4917
5084
  this.ttl = {
4918
5085
  age: null,
@@ -4945,10 +5112,8 @@
4945
5112
  // lightweight changes tracking (loki IDs only) for optimized db saving
4946
5113
  this.dirtyIds = [];
4947
5114
 
4948
- // initialize the id index
4949
- this.ensureId();
4950
- var indices = [];
4951
5115
  // initialize optional user-supplied indices array ['age', 'lname', 'zip']
5116
+ var indices = [];
4952
5117
  if (options && options.indices) {
4953
5118
  if (Object.prototype.toString.call(options.indices) === '[object Array]') {
4954
5119
  indices = options.indices;
@@ -4983,7 +5148,7 @@
4983
5148
  return self.removeAutoUpdateObserver(object);
4984
5149
  try {
4985
5150
  self.update(object);
4986
- } catch (err) {}
5151
+ } catch (err) { }
4987
5152
  });
4988
5153
  }
4989
5154
 
@@ -5003,7 +5168,7 @@
5003
5168
 
5004
5169
  function getObjectDelta(oldObject, newObject) {
5005
5170
  var propertyNames = newObject !== null && typeof newObject === 'object' ? Object.keys(newObject) : null;
5006
- if (propertyNames && propertyNames.length && ['string', 'boolean', 'number'].indexOf(typeof(newObject)) < 0) {
5171
+ if (propertyNames && propertyNames.length && ['string', 'boolean', 'number'].indexOf(typeof (newObject)) < 0) {
5007
5172
  var delta = {};
5008
5173
  for (var i = 0; i < propertyNames.length; i++) {
5009
5174
  var propertyName = propertyNames[i];
@@ -5062,11 +5227,12 @@
5062
5227
  }
5063
5228
 
5064
5229
  Collection.prototype = new LokiEventEmitter();
5230
+ Collection.prototype.contructor = Collection;
5065
5231
 
5066
5232
  /*
5067
5233
  * For ChangeAPI default to clone entire object, for delta changes create object with only differences (+ $loki and meta)
5068
5234
  */
5069
- Collection.prototype.createChange = function(name, op, obj, old) {
5235
+ Collection.prototype.createChange = function (name, op, obj, old) {
5070
5236
  this.changes.push({
5071
5237
  name: name,
5072
5238
  operation: op,
@@ -5074,7 +5240,7 @@
5074
5240
  });
5075
5241
  };
5076
5242
 
5077
- Collection.prototype.insertMeta = function(obj) {
5243
+ Collection.prototype.insertMeta = function (obj) {
5078
5244
  var len, idx;
5079
5245
 
5080
5246
  if (this.disableMeta || !obj) {
@@ -5085,7 +5251,7 @@
5085
5251
  if (Array.isArray(obj)) {
5086
5252
  len = obj.length;
5087
5253
 
5088
- for(idx=0; idx<len; idx++) {
5254
+ for (idx = 0; idx < len; idx++) {
5089
5255
  if (!obj[idx].hasOwnProperty('meta')) {
5090
5256
  obj[idx].meta = {};
5091
5257
  }
@@ -5106,36 +5272,42 @@
5106
5272
  obj.meta.revision = 0;
5107
5273
  };
5108
5274
 
5109
- Collection.prototype.updateMeta = function(obj) {
5275
+ Collection.prototype.updateMeta = function (obj) {
5110
5276
  if (this.disableMeta || !obj) {
5111
- return;
5277
+ return obj;
5278
+ }
5279
+ if (!this.disableFreeze) {
5280
+ obj = unFreeze(obj);
5281
+ obj.meta = unFreeze(obj.meta);
5112
5282
  }
5113
5283
  obj.meta.updated = (new Date()).getTime();
5114
5284
  obj.meta.revision += 1;
5285
+ return obj;
5115
5286
  };
5116
5287
 
5117
- Collection.prototype.createInsertChange = function(obj) {
5288
+ Collection.prototype.createInsertChange = function (obj) {
5118
5289
  this.createChange(this.name, 'I', obj);
5119
5290
  };
5120
5291
 
5121
- Collection.prototype.createUpdateChange = function(obj, old) {
5292
+ Collection.prototype.createUpdateChange = function (obj, old) {
5122
5293
  this.createChange(this.name, 'U', obj, old);
5123
5294
  };
5124
5295
 
5125
- Collection.prototype.insertMetaWithChange = function(obj) {
5296
+ Collection.prototype.insertMetaWithChange = function (obj) {
5126
5297
  this.insertMeta(obj);
5127
5298
  this.createInsertChange(obj);
5128
5299
  };
5129
5300
 
5130
- Collection.prototype.updateMetaWithChange = function(obj, old) {
5131
- this.updateMeta(obj);
5301
+ Collection.prototype.updateMetaWithChange = function (obj, old, objFrozen) {
5302
+ obj = this.updateMeta(obj, objFrozen);
5132
5303
  this.createUpdateChange(obj, old);
5304
+ return obj;
5133
5305
  };
5134
5306
 
5135
5307
  Collection.prototype.lokiConsoleWrapper = {
5136
- log: function () {},
5137
- warn: function () {},
5138
- error: function () {},
5308
+ log: function () { },
5309
+ warn: function () { },
5310
+ error: function () { },
5139
5311
  };
5140
5312
 
5141
5313
  Collection.prototype.addAutoUpdateObserver = function (object) {
@@ -5422,7 +5594,7 @@
5422
5594
  options.randomSamplingFactor = 0.1;
5423
5595
  }
5424
5596
 
5425
- var valid=true, idx, iter, pos, len, biv;
5597
+ var valid = true, idx, iter, pos, len, biv;
5426
5598
 
5427
5599
  // make sure we are passed a valid binary index name
5428
5600
  if (!this.binaryIndices.hasOwnProperty(property)) {
@@ -5458,28 +5630,28 @@
5458
5630
  if (options.randomSampling) {
5459
5631
  // validate first and last
5460
5632
  if (!LokiOps.$lte(Utils.getIn(this.data[biv[0]], property, usingDotNotation),
5461
- Utils.getIn(this.data[biv[1]], property, usingDotNotation))) {
5462
- valid=false;
5633
+ Utils.getIn(this.data[biv[1]], property, usingDotNotation))) {
5634
+ valid = false;
5463
5635
  }
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;
5636
+ if (!LokiOps.$lte(Utils.getIn(this.data[biv[len - 2]], property, usingDotNotation),
5637
+ Utils.getIn(this.data[biv[len - 1]], property, usingDotNotation))) {
5638
+ valid = false;
5467
5639
  }
5468
5640
 
5469
5641
  // if first and last positions are sorted correctly with their nearest neighbor,
5470
5642
  // continue onto random sampling phase...
5471
5643
  if (valid) {
5472
5644
  // # random samplings = total count * sampling factor
5473
- iter = Math.floor((len-1) * options.randomSamplingFactor);
5645
+ iter = Math.floor((len - 1) * options.randomSamplingFactor);
5474
5646
 
5475
5647
  // for each random sampling, validate that the binary index is sequenced properly
5476
5648
  // with next higher value.
5477
- for(idx=0; idx<iter-1; idx++) {
5649
+ for (idx = 0; idx < iter - 1; idx++) {
5478
5650
  // calculate random position
5479
- pos = Math.floor(Math.random() * (len-1));
5651
+ pos = Math.floor(Math.random() * (len - 1));
5480
5652
  if (!LokiOps.$lte(Utils.getIn(this.data[biv[pos]], property, usingDotNotation),
5481
- Utils.getIn(this.data[biv[pos+1]], property, usingDotNotation))) {
5482
- valid=false;
5653
+ Utils.getIn(this.data[biv[pos + 1]], property, usingDotNotation))) {
5654
+ valid = false;
5483
5655
  break;
5484
5656
  }
5485
5657
  }
@@ -5487,10 +5659,10 @@
5487
5659
  }
5488
5660
  else {
5489
5661
  // validate that the binary index is sequenced properly
5490
- for(idx=0; idx<len-1; idx++) {
5662
+ for (idx = 0; idx < len - 1; idx++) {
5491
5663
  if (!LokiOps.$lte(Utils.getIn(this.data[biv[idx]], property, usingDotNotation),
5492
- Utils.getIn(this.data[biv[idx+1]], property, usingDotNotation))) {
5493
- valid=false;
5664
+ Utils.getIn(this.data[biv[idx + 1]], property, usingDotNotation))) {
5665
+ valid = false;
5494
5666
  break;
5495
5667
  }
5496
5668
  }
@@ -5516,6 +5688,19 @@
5516
5688
  return result;
5517
5689
  };
5518
5690
 
5691
+ /**
5692
+ * Returns a named unique index
5693
+ * @param {string} field - indexed field name
5694
+ * @param {boolean} force - if `true`, will rebuild index; otherwise, function may return null
5695
+ */
5696
+ Collection.prototype.getUniqueIndex = function (field, force) {
5697
+ var index = this.constraints.unique[field];
5698
+ if (!index && force) {
5699
+ return this.ensureUniqueIndex(field);
5700
+ }
5701
+ return index;
5702
+ };
5703
+
5519
5704
  Collection.prototype.ensureUniqueIndex = function (field) {
5520
5705
  var index = this.constraints.unique[field];
5521
5706
  if (!index) {
@@ -5585,13 +5770,17 @@
5585
5770
  * Rebuild idIndex
5586
5771
  */
5587
5772
  Collection.prototype.ensureId = function () {
5588
- var len = this.data.length,
5773
+ if (this.idIndex) {
5774
+ return;
5775
+ }
5776
+ var data = this.data,
5589
5777
  i = 0;
5590
-
5591
- this.idIndex = [];
5592
- for (i; i < len; i += 1) {
5593
- this.idIndex.push(this.data[i].$loki);
5778
+ var len = data.length;
5779
+ var index = new Array(len);
5780
+ for (i; i < len; i++) {
5781
+ index[i] = data[i].$loki;
5594
5782
  }
5783
+ this.idIndex = index;
5595
5784
  };
5596
5785
 
5597
5786
  /**
@@ -5634,7 +5823,7 @@
5634
5823
  **/
5635
5824
  Collection.prototype.removeDynamicView = function (name) {
5636
5825
  this.DynamicViews =
5637
- this.DynamicViews.filter(function(dv) { return dv.name !== name; });
5826
+ this.DynamicViews.filter(function (dv) { return dv.name !== name; });
5638
5827
  };
5639
5828
 
5640
5829
  /**
@@ -5676,7 +5865,7 @@
5676
5865
  * @param {object} filterObject - 'mongo-like' query object
5677
5866
  * @memberof Collection
5678
5867
  */
5679
- Collection.prototype.findAndRemove = function(filterObject) {
5868
+ Collection.prototype.findAndRemove = function (filterObject) {
5680
5869
  this.chain().find(filterObject).remove();
5681
5870
  };
5682
5871
 
@@ -5764,12 +5953,19 @@
5764
5953
 
5765
5954
  // if configured to clone, do so now... otherwise just use same obj reference
5766
5955
  var obj = this.cloneObjects ? clone(doc, this.cloneMethod) : doc;
5956
+ if (!this.disableFreeze) {
5957
+ obj = unFreeze(obj);
5958
+ }
5767
5959
 
5768
- if (!this.disableMeta && typeof obj.meta === 'undefined') {
5769
- obj.meta = {
5770
- revision: 0,
5771
- created: 0
5772
- };
5960
+ if (!this.disableMeta) {
5961
+ if (typeof obj.meta === 'undefined') {
5962
+ obj.meta = {
5963
+ revision: 0,
5964
+ created: 0
5965
+ };
5966
+ } else if (!this.disableFreeze) {
5967
+ obj.meta = unFreeze(obj.meta);
5968
+ }
5773
5969
  }
5774
5970
 
5775
5971
  // both 'pre-insert' and 'insert' events are passed internal data reference even when cloning
@@ -5785,18 +5981,23 @@
5785
5981
  // (moved from "insert" event listener to allow internal reference to be used)
5786
5982
  if (this.disableChangesApi) {
5787
5983
  this.insertMeta(obj);
5788
- }
5789
- else {
5984
+ } else {
5790
5985
  this.insertMetaWithChange(obj);
5791
5986
  }
5792
5987
 
5988
+ if (!this.disableFreeze) {
5989
+ deepFreeze(obj);
5990
+ }
5991
+
5793
5992
  // if cloning is enabled, emit insert event with clone of new object
5794
5993
  returnObj = this.cloneObjects ? clone(obj, this.cloneMethod) : obj;
5994
+
5795
5995
  if (!bulkInsert) {
5796
5996
  this.emit('insert', returnObj);
5797
5997
  }
5798
5998
 
5799
5999
  this.addAutoUpdateObserver(returnObj);
6000
+
5800
6001
  return returnObj;
5801
6002
  };
5802
6003
 
@@ -5812,43 +6013,31 @@
5812
6013
  options = options || {};
5813
6014
 
5814
6015
  this.data = [];
5815
- this.idIndex = [];
6016
+ this.idIndex = null;
5816
6017
  this.cachedIndex = null;
5817
6018
  this.cachedBinaryIndex = null;
5818
6019
  this.cachedData = null;
5819
6020
  this.maxId = 0;
5820
6021
  this.DynamicViews = [];
5821
6022
  this.dirty = true;
6023
+ this.constraints = {
6024
+ unique: {},
6025
+ exact: {}
6026
+ };
5822
6027
 
5823
6028
  // if removing indices entirely
5824
6029
  if (options.removeIndices === true) {
5825
6030
  this.binaryIndices = {};
5826
-
5827
- this.constraints = {
5828
- unique: {},
5829
- exact: {}
5830
- };
5831
6031
  this.uniqueNames = [];
5832
6032
  }
5833
6033
  // clear indices but leave definitions in place
5834
6034
  else {
5835
6035
  // clear binary indices
5836
6036
  var keys = Object.keys(this.binaryIndices);
5837
- keys.forEach(function(biname) {
6037
+ keys.forEach(function (biname) {
5838
6038
  self.binaryIndices[biname].dirty = false;
5839
6039
  self.binaryIndices[biname].values = [];
5840
6040
  });
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
6041
  }
5853
6042
  };
5854
6043
 
@@ -5873,7 +6062,7 @@
5873
6062
  }
5874
6063
 
5875
6064
  try {
5876
- for (k=0; k < len; k += 1) {
6065
+ for (k = 0; k < len; k += 1) {
5877
6066
  this.update(doc[k]);
5878
6067
  }
5879
6068
  }
@@ -5907,12 +6096,12 @@
5907
6096
  position = arr[1]; // position in data array
5908
6097
 
5909
6098
  // if configured to clone, do so now... otherwise just use same obj reference
5910
- newInternal = this.cloneObjects || !this.disableDeltaChangesApi ? clone(doc, this.cloneMethod) : doc;
6099
+ newInternal = this.cloneObjects || (!this.disableDeltaChangesApi && this.disableFreeze) ? clone(doc, this.cloneMethod) : doc;
5911
6100
 
5912
6101
  this.emit('pre-update', doc);
5913
6102
 
5914
- Object.keys(this.constraints.unique).forEach(function (key) {
5915
- self.constraints.unique[key].update(oldInternal, newInternal);
6103
+ this.uniqueNames.forEach(function (key) {
6104
+ self.getUniqueIndex(key, true).update(oldInternal, newInternal);
5916
6105
  });
5917
6106
 
5918
6107
  // operate the update
@@ -5952,10 +6141,13 @@
5952
6141
 
5953
6142
  // update meta and store changes if ChangesAPI is enabled
5954
6143
  if (this.disableChangesApi) {
5955
- this.updateMeta(newInternal, null);
6144
+ newInternal = this.updateMeta(newInternal);
6145
+ } else {
6146
+ newInternal = this.updateMetaWithChange(newInternal, oldInternal);
5956
6147
  }
5957
- else {
5958
- this.updateMetaWithChange(newInternal, oldInternal);
6148
+
6149
+ if (!this.disableFreeze) {
6150
+ deepFreeze(newInternal);
5959
6151
  }
5960
6152
 
5961
6153
  var returnObj;
@@ -6004,23 +6196,23 @@
6004
6196
  this.maxId = (this.data[this.data.length - 1].$loki + 1);
6005
6197
  }
6006
6198
 
6007
- obj.$loki = this.maxId;
6199
+ var newId = this.maxId;
6200
+ obj.$loki = newId;
6008
6201
 
6009
6202
  if (!this.disableMeta) {
6010
6203
  obj.meta.version = 0;
6011
6204
  }
6012
6205
 
6013
- var key, constrUnique = this.constraints.unique;
6014
- for (key in constrUnique) {
6015
- if (hasOwnProperty.call(constrUnique, key)) {
6016
- constrUnique[key].set(obj);
6017
- }
6206
+ for (var i = 0, len = this.uniqueNames.length; i < len; i ++) {
6207
+ this.getUniqueIndex(this.uniqueNames[i], true).set(obj);
6208
+ }
6209
+
6210
+ if (this.idIndex) {
6211
+ this.idIndex.push(newId);
6018
6212
  }
6019
6213
 
6020
- // add new obj id to idIndex
6021
- this.idIndex.push(obj.$loki);
6022
6214
  if (this.isIncremental) {
6023
- this.dirtyIds.push(obj.$loki);
6215
+ this.dirtyIds.push(newId);
6024
6216
  }
6025
6217
 
6026
6218
  // add the object
@@ -6031,14 +6223,14 @@
6031
6223
  // now that we can efficiently determine the data[] position of newly added document,
6032
6224
  // submit it for all registered DynamicViews to evaluate for inclusion/exclusion
6033
6225
  var dvlen = this.DynamicViews.length;
6034
- for (var i = 0; i < dvlen; i++) {
6226
+ for (i = 0; i < dvlen; i++) {
6035
6227
  this.DynamicViews[i].evaluateDocument(addedPos, true);
6036
6228
  }
6037
6229
 
6038
6230
  if (this.adaptiveBinaryIndices) {
6039
6231
  // for each binary index defined in collection, immediately update rather than flag for lazy rebuild
6040
6232
  var bIndices = this.binaryIndices;
6041
- for (key in bIndices) {
6233
+ for (var key in bIndices) {
6042
6234
  this.adaptiveBinaryIndexInsert(addedPos, key);
6043
6235
  }
6044
6236
  }
@@ -6065,7 +6257,7 @@
6065
6257
  * @param {function} updateFunction - update function to run against filtered documents
6066
6258
  * @memberof Collection
6067
6259
  */
6068
- Collection.prototype.updateWhere = function(filterFunction, updateFunction) {
6260
+ Collection.prototype.updateWhere = function (filterFunction, updateFunction) {
6069
6261
  var results = this.where(filterFunction),
6070
6262
  i = 0,
6071
6263
  obj;
@@ -6105,21 +6297,22 @@
6105
6297
  * Internal method to remove a batch of documents from the collection.
6106
6298
  * @param {number[]} positions - data/idIndex positions to remove
6107
6299
  */
6108
- Collection.prototype.removeBatchByPositions = function(positions) {
6300
+ Collection.prototype.removeBatchByPositions = function (positions) {
6109
6301
  var len = positions.length;
6110
6302
  var xo = {};
6111
6303
  var dlen, didx, idx;
6112
- var bic=Object.keys(this.binaryIndices).length;
6113
- var uic=Object.keys(this.constraints.unique).length;
6304
+ var bic = Object.keys(this.binaryIndices).length;
6305
+ var uic = Object.keys(this.constraints.unique).length;
6114
6306
  var adaptiveOverride = this.adaptiveBinaryIndices && Object.keys(this.binaryIndices).length > 0;
6115
- var doc, self=this;
6307
+ var doc, self = this;
6116
6308
 
6117
6309
  try {
6118
6310
  this.startTransaction();
6119
6311
 
6120
6312
  // create hashobject for positional removal inclusion tests...
6121
6313
  // all keys defined in this hashobject represent $loki ids of the documents to remove.
6122
- for(idx=0; idx < len; idx++) {
6314
+ this.ensureId();
6315
+ for (idx = 0; idx < len; idx++) {
6123
6316
  xo[this.idIndex[positions[idx]]] = true;
6124
6317
  }
6125
6318
 
@@ -6148,11 +6341,14 @@
6148
6341
  }
6149
6342
 
6150
6343
  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]);
6344
+ this.uniqueNames.forEach(function (key) {
6345
+ var index = self.getUniqueIndex(key);
6346
+ if (index) {
6347
+ for (idx = 0; idx < len; idx++) {
6348
+ doc = self.data[positions[idx]];
6349
+ if (doc[key] !== null && doc[key] !== undefined) {
6350
+ index.remove(doc[key]);
6351
+ }
6156
6352
  }
6157
6353
  }
6158
6354
  });
@@ -6163,14 +6359,14 @@
6163
6359
  // since data not removed yet, in future we can emit single delete event with array...
6164
6360
  // for now that might be breaking change to put in potential 1.6 or LokiDB (lokijs2) version
6165
6361
  if (!this.disableChangesApi || this.events.delete.length > 1) {
6166
- for(idx=0; idx < len; idx++) {
6362
+ for (idx = 0; idx < len; idx++) {
6167
6363
  this.emit('delete', this.data[positions[idx]]);
6168
6364
  }
6169
6365
  }
6170
6366
 
6171
6367
  // remove from data[] :
6172
6368
  // filter collection data for items not in inclusion hashobject
6173
- this.data = this.data.filter(function(obj) {
6369
+ this.data = this.data.filter(function (obj) {
6174
6370
  return !xo[obj.$loki];
6175
6371
  });
6176
6372
 
@@ -6182,8 +6378,8 @@
6182
6378
 
6183
6379
  // remove from idIndex[] :
6184
6380
  // filter idIndex for items not in inclusion hashobject
6185
- this.idIndex = this.idIndex.filter(function(id) {
6186
- return !xo[id];
6381
+ this.idIndex = this.idIndex.filter(function (id) {
6382
+ return !xo[id];
6187
6383
  });
6188
6384
 
6189
6385
  if (this.adaptiveBinaryIndices && adaptiveOverride) {
@@ -6212,21 +6408,21 @@
6212
6408
  * Internal method called by remove()
6213
6409
  * @param {object[]|number[]} batch - array of documents or $loki ids to remove
6214
6410
  */
6215
- Collection.prototype.removeBatch = function(batch) {
6411
+ Collection.prototype.removeBatch = function (batch) {
6216
6412
  var len = batch.length,
6217
- dlen=this.data.length,
6413
+ dlen = this.data.length,
6218
6414
  idx;
6219
6415
  var xlt = {};
6220
6416
  var posx = [];
6221
6417
 
6222
6418
  // create lookup hashobject to translate $loki id to position
6223
- for (idx=0; idx < dlen; idx++) {
6419
+ for (idx = 0; idx < dlen; idx++) {
6224
6420
  xlt[this.data[idx].$loki] = idx;
6225
6421
  }
6226
6422
 
6227
6423
  // iterate the batch
6228
- for (idx=0; idx < len; idx++) {
6229
- if (typeof(batch[idx]) === 'object') {
6424
+ for (idx = 0; idx < len; idx++) {
6425
+ if (typeof (batch[idx]) === 'object') {
6230
6426
  posx.push(xlt[batch[idx].$loki]);
6231
6427
  }
6232
6428
  else {
@@ -6243,6 +6439,8 @@
6243
6439
  * @memberof Collection
6244
6440
  */
6245
6441
  Collection.prototype.remove = function (doc) {
6442
+ var frozen;
6443
+
6246
6444
  if (typeof doc === 'number') {
6247
6445
  doc = this.get(doc);
6248
6446
  }
@@ -6265,9 +6463,12 @@
6265
6463
  // obj = arr[0],
6266
6464
  position = arr[1];
6267
6465
  var self = this;
6268
- Object.keys(this.constraints.unique).forEach(function (key) {
6466
+ this.uniqueNames.forEach(function (key) {
6269
6467
  if (doc[key] !== null && typeof doc[key] !== 'undefined') {
6270
- self.constraints.unique[key].remove(doc[key]);
6468
+ var index = self.getUniqueIndex(key);
6469
+ if (index) {
6470
+ index.remove(doc[key]);
6471
+ }
6271
6472
  }
6272
6473
  });
6273
6474
  // now that we can efficiently determine the data[] position of newly added document,
@@ -6300,8 +6501,15 @@
6300
6501
  this.commit();
6301
6502
  this.dirty = true; // for autosave scenarios
6302
6503
  this.emit('delete', arr[0]);
6504
+
6505
+ if (!this.disableFreeze) {
6506
+ doc = unFreeze(doc);
6507
+ }
6303
6508
  delete doc.$loki;
6304
6509
  delete doc.meta;
6510
+ if (!this.disableFreeze) {
6511
+ freeze(doc);
6512
+ }
6305
6513
  return doc;
6306
6514
 
6307
6515
  } catch (err) {
@@ -6325,6 +6533,10 @@
6325
6533
  * @memberof Collection
6326
6534
  */
6327
6535
  Collection.prototype.get = function (id, returnPosition) {
6536
+ if (!this.idIndex) {
6537
+ this.ensureId();
6538
+ }
6539
+
6328
6540
  var retpos = returnPosition || false,
6329
6541
  data = this.idIndex,
6330
6542
  max = data.length - 1,
@@ -6364,7 +6576,7 @@
6364
6576
  * @param {int} dataPosition : coll.data array index/position
6365
6577
  * @param {string} binaryIndexName : index to search for dataPosition in
6366
6578
  */
6367
- Collection.prototype.getBinaryIndexPosition = function(dataPosition, binaryIndexName) {
6579
+ Collection.prototype.getBinaryIndexPosition = function (dataPosition, binaryIndexName) {
6368
6580
  var val = Utils.getIn(this.data[dataPosition], binaryIndexName, true);
6369
6581
  var index = this.binaryIndices[binaryIndexName].values;
6370
6582
 
@@ -6383,7 +6595,7 @@
6383
6595
  // narrow down the sub-segment of index values
6384
6596
  // where the indexed property value exactly matches our
6385
6597
  // value and then linear scan to find exact -index- position
6386
- for(var idx = min; idx <= max; idx++) {
6598
+ for (var idx = min; idx <= max; idx++) {
6387
6599
  if (index[idx] === dataPosition) return idx;
6388
6600
  }
6389
6601
 
@@ -6396,7 +6608,7 @@
6396
6608
  * @param {int} dataPosition : coll.data array index/position
6397
6609
  * @param {string} binaryIndexName : index to search for dataPosition in
6398
6610
  */
6399
- Collection.prototype.adaptiveBinaryIndexInsert = function(dataPosition, binaryIndexName) {
6611
+ Collection.prototype.adaptiveBinaryIndexInsert = function (dataPosition, binaryIndexName) {
6400
6612
  var usingDotNotation = (binaryIndexName.indexOf('.') !== -1);
6401
6613
  var index = this.binaryIndices[binaryIndexName].values;
6402
6614
  var val = Utils.getIn(this.data[dataPosition], binaryIndexName, usingDotNotation);
@@ -6407,7 +6619,7 @@
6407
6619
  val = Utils.getIn(this.data[dataPosition], binaryIndexName);
6408
6620
  }
6409
6621
 
6410
- var idxPos = (index.length === 0)?0:this.calculateRangeStart(binaryIndexName, val, true, usingDotNotation);
6622
+ var idxPos = (index.length === 0) ? 0 : this.calculateRangeStart(binaryIndexName, val, true, usingDotNotation);
6411
6623
 
6412
6624
  // insert new data index into our binary index at the proper sorted location for relevant property calculated by idxPos.
6413
6625
  // doing this after adjusting dataPositions so no clash with previous item at that position.
@@ -6419,14 +6631,14 @@
6419
6631
  * @param {int} dataPosition : coll.data array index/position
6420
6632
  * @param {string} binaryIndexName : index to search for dataPosition in
6421
6633
  */
6422
- Collection.prototype.adaptiveBinaryIndexUpdate = function(dataPosition, binaryIndexName) {
6634
+ Collection.prototype.adaptiveBinaryIndexUpdate = function (dataPosition, binaryIndexName) {
6423
6635
  // linear scan needed to find old position within index unless we optimize for clone scenarios later
6424
6636
  // within (my) node 5.6.0, the following for() loop with strict compare is -much- faster than indexOf()
6425
6637
  var idxPos,
6426
6638
  index = this.binaryIndices[binaryIndexName].values,
6427
- len=index.length;
6639
+ len = index.length;
6428
6640
 
6429
- for(idxPos=0; idxPos < len; idxPos++) {
6641
+ for (idxPos = 0; idxPos < len; idxPos++) {
6430
6642
  if (index[idxPos] === dataPosition) break;
6431
6643
  }
6432
6644
 
@@ -6442,7 +6654,7 @@
6442
6654
  * @param {number|number[]} dataPosition : coll.data array index/position
6443
6655
  * @param {string} binaryIndexName : index to search for dataPosition in
6444
6656
  */
6445
- Collection.prototype.adaptiveBinaryIndexRemove = function(dataPosition, binaryIndexName, removedFromIndexOnly) {
6657
+ Collection.prototype.adaptiveBinaryIndexRemove = function (dataPosition, binaryIndexName, removedFromIndexOnly) {
6446
6658
  var bi = this.binaryIndices[binaryIndexName];
6447
6659
  var len, idx, rmidx, rmlen, rxo = {};
6448
6660
  var curr, shift, idxPos;
@@ -6456,12 +6668,12 @@
6456
6668
  }
6457
6669
  // we were passed an array (batch) of documents so use this 'batch optimized' algorithm
6458
6670
  else {
6459
- for(rmidx=0;rmidx<rmlen; rmidx++) {
6671
+ for (rmidx = 0; rmidx < rmlen; rmidx++) {
6460
6672
  rxo[dataPosition[rmidx]] = true;
6461
6673
  }
6462
6674
 
6463
6675
  // remove document from index (with filter function)
6464
- bi.values = bi.values.filter(function(di) { return !rxo[di]; });
6676
+ bi.values = bi.values.filter(function (di) { return !rxo[di]; });
6465
6677
 
6466
6678
  // if we passed this optional flag parameter, we are calling from adaptiveBinaryIndexUpdate,
6467
6679
  // in which case data positions stay the same.
@@ -6470,18 +6682,18 @@
6470
6682
  }
6471
6683
 
6472
6684
  var sortedPositions = dataPosition.slice();
6473
- sortedPositions.sort(function (a, b) { return a-b; });
6685
+ sortedPositions.sort(function (a, b) { return a - b; });
6474
6686
 
6475
6687
  // to remove holes, we need to 'shift down' the index's data array positions
6476
6688
  // we need to adjust array positions -1 for each index data positions greater than removed positions
6477
6689
  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++;
6690
+ for (idx = 0; idx < len; idx++) {
6691
+ curr = bi.values[idx];
6692
+ shift = 0;
6693
+ for (rmidx = 0; rmidx < rmlen && curr > sortedPositions[rmidx]; rmidx++) {
6694
+ shift++;
6483
6695
  }
6484
- bi.values[idx]-=shift;
6696
+ bi.values[idx] -= shift;
6485
6697
  }
6486
6698
 
6487
6699
  // batch processed, bail out
@@ -6565,11 +6777,11 @@
6565
6777
 
6566
6778
  // if not in index and our value is less than the found one
6567
6779
  if (Comparators.lt(val, Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), false)) {
6568
- return adaptive?lbound:lbound-1;
6780
+ return adaptive ? lbound : lbound - 1;
6569
6781
  }
6570
6782
 
6571
6783
  // not in index and our value is greater than the found one
6572
- return adaptive?lbound+1:lbound;
6784
+ return adaptive ? lbound + 1 : lbound;
6573
6785
  };
6574
6786
 
6575
6787
  /**
@@ -6608,14 +6820,14 @@
6608
6820
  return ubound;
6609
6821
  }
6610
6822
 
6611
- // if not in index and our value is less than the found one
6823
+ // if not in index and our value is less than the found one
6612
6824
  if (Comparators.gt(val, Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), false)) {
6613
- return ubound+1;
6825
+ return ubound + 1;
6614
6826
  }
6615
6827
 
6616
6828
  // either hole or first nonmatch
6617
- if (Comparators.aeq(val, Utils.getIn(rcd[index[ubound-1]], prop, usingDotNotation))) {
6618
- return ubound-1;
6829
+ if (Comparators.aeq(val, Utils.getIn(rcd[index[ubound - 1]], prop, usingDotNotation))) {
6830
+ return ubound - 1;
6619
6831
  }
6620
6832
 
6621
6833
  // hole, so ubound if nearest gt than the val we were looking for
@@ -6653,94 +6865,94 @@
6653
6865
 
6654
6866
  // if value falls outside of our range return [0, -1] to designate no results
6655
6867
  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)) {
6868
+ case '$eq':
6869
+ case '$aeq':
6870
+ if (Comparators.lt(val, minVal, false) || Comparators.gt(val, maxVal, false)) {
6871
+ return [0, -1];
6872
+ }
6873
+ break;
6874
+ case '$dteq':
6875
+ if (Comparators.lt(val, minVal, false) || Comparators.gt(val, maxVal, false)) {
6876
+ return [0, -1];
6877
+ }
6878
+ break;
6879
+ case '$gt':
6880
+ // none are within range
6881
+ if (Comparators.gt(val, maxVal, true)) {
6882
+ return [0, -1];
6883
+ }
6884
+ // all are within range
6885
+ if (Comparators.gt(minVal, val, false)) {
6684
6886
  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
- }
6887
+ }
6888
+ break;
6889
+ case '$gte':
6890
+ // none are within range
6891
+ if (Comparators.gt(val, maxVal, false)) {
6892
+ return [0, -1];
6893
+ }
6894
+ // all are within range
6895
+ if (Comparators.gt(minVal, val, true)) {
6896
+ return [min, max];
6897
+ }
6898
+ break;
6899
+ case '$lt':
6900
+ // none are within range
6901
+ if (Comparators.lt(val, minVal, true)) {
6902
+ return [0, -1];
6903
+ }
6904
+ // all are within range
6905
+ if (Comparators.lt(maxVal, val, false)) {
6906
+ return [min, max];
6907
+ }
6908
+ break;
6909
+ case '$lte':
6910
+ // none are within range
6911
+ if (Comparators.lt(val, minVal, false)) {
6912
+ return [0, -1];
6913
+ }
6914
+ // all are within range
6915
+ if (Comparators.lt(maxVal, val, true)) {
6916
+ return [min, max];
6917
+ }
6918
+ break;
6919
+ case '$between':
6920
+ // none are within range (low range is greater)
6921
+ if (Comparators.gt(val[0], maxVal, false)) {
6922
+ return [0, -1];
6923
+ }
6924
+ // none are within range (high range lower)
6925
+ if (Comparators.lt(val[1], minVal, false)) {
6926
+ return [0, -1];
6927
+ }
6716
6928
 
6717
- lbound = this.calculateRangeStart(prop, val[0], false, usingDotNotation);
6718
- ubound = this.calculateRangeEnd(prop, val[1], usingDotNotation);
6929
+ lbound = this.calculateRangeStart(prop, val[0], false, usingDotNotation);
6930
+ ubound = this.calculateRangeEnd(prop, val[1], usingDotNotation);
6719
6931
 
6720
- if (lbound < 0) lbound++;
6721
- if (ubound > max) ubound--;
6932
+ if (lbound < 0) lbound++;
6933
+ if (ubound > max) ubound--;
6722
6934
 
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--;
6935
+ if (!Comparators.gt(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val[0], true)) lbound++;
6936
+ if (!Comparators.lt(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val[1], true)) ubound--;
6725
6937
 
6726
- if (ubound < lbound) return [0, -1];
6938
+ if (ubound < lbound) return [0, -1];
6727
6939
 
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++) {
6940
+ return ([lbound, ubound]);
6941
+ case '$in':
6942
+ var idxset = [],
6943
+ segResult = [];
6944
+ // query each value '$eq' operator and merge the seqment results.
6945
+ for (var j = 0, len = val.length; j < len; j++) {
6734
6946
  var seg = this.calculateRange('$eq', prop, val[j]);
6735
6947
 
6736
6948
  for (var i = seg[0]; i <= seg[1]; i++) {
6737
- if (idxset[i] === undefined) {
6738
- idxset[i] = true;
6739
- segResult.push(i);
6740
- }
6949
+ if (idxset[i] === undefined) {
6950
+ idxset[i] = true;
6951
+ segResult.push(i);
6952
+ }
6741
6953
  }
6742
- }
6743
- return segResult;
6954
+ }
6955
+ return segResult;
6744
6956
  }
6745
6957
 
6746
6958
  // determine lbound where needed
@@ -6750,8 +6962,8 @@
6750
6962
  case '$dteq':
6751
6963
  case '$gte':
6752
6964
  case '$lt':
6753
- lbound = this.calculateRangeStart(prop, val, false, usingDotNotation);
6754
- lval = Utils.getIn(rcd[index[lbound]], prop, usingDotNotation);
6965
+ lbound = this.calculateRangeStart(prop, val, false, usingDotNotation);
6966
+ lval = Utils.getIn(rcd[index[lbound]], prop, usingDotNotation);
6755
6967
  break;
6756
6968
  default: break;
6757
6969
  }
@@ -6771,50 +6983,50 @@
6771
6983
 
6772
6984
 
6773
6985
  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
- }
6986
+ case '$eq':
6987
+ case '$aeq':
6988
+ case '$dteq':
6989
+ // if hole (not found)
6990
+ if (!Comparators.aeq(lval, val)) {
6991
+ return [0, -1];
6992
+ }
6781
6993
 
6782
- return [lbound, ubound];
6994
+ return [lbound, ubound];
6783
6995
 
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];
6996
+ case '$gt':
6997
+ // if hole (not found) ub position is already greater
6998
+ if (!Comparators.aeq(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val)) {
6999
+ return [ubound, max];
7000
+ }
7001
+ // otherwise (found) so ubound is still equal, get next
7002
+ return [ubound + 1, max];
6791
7003
 
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];
7004
+ case '$gte':
7005
+ // if hole (not found) lb position marks left outside of range
7006
+ if (!Comparators.aeq(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val)) {
7007
+ return [lbound + 1, max];
7008
+ }
7009
+ // otherwise (found) so lb is first position where its equal
7010
+ return [lbound, max];
6799
7011
 
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];
7012
+ case '$lt':
7013
+ // if hole (not found) position already is less than
7014
+ if (!Comparators.aeq(Utils.getIn(rcd[index[lbound]], prop, usingDotNotation), val)) {
7015
+ return [min, lbound];
7016
+ }
7017
+ // otherwise (found) so lb marks left inside of eq range, get previous
7018
+ return [min, lbound - 1];
6807
7019
 
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];
7020
+ case '$lte':
7021
+ // if hole (not found) ub position marks right outside so get previous
7022
+ if (!Comparators.aeq(Utils.getIn(rcd[index[ubound]], prop, usingDotNotation), val)) {
7023
+ return [min, ubound - 1];
7024
+ }
7025
+ // otherwise (found) so ub is last position where its still equal
7026
+ return [min, ubound];
6815
7027
 
6816
- default:
6817
- return [0, rcd.length - 1];
7028
+ default:
7029
+ return [0, rcd.length - 1];
6818
7030
  }
6819
7031
  };
6820
7032
 
@@ -6834,7 +7046,7 @@
6834
7046
  };
6835
7047
  }
6836
7048
 
6837
- var result = this.constraints.unique[field].get(value);
7049
+ var result = this.getUniqueIndex(field, true).get(value);
6838
7050
  if (!this.cloneObjects) {
6839
7051
  return result;
6840
7052
  } else {
@@ -6852,7 +7064,7 @@
6852
7064
  query = query || {};
6853
7065
 
6854
7066
  // Instantiate Resultset and exec find op passing firstOnly = true param
6855
- var result = this.chain().find(query,true).data();
7067
+ var result = this.chain().find(query, true).data();
6856
7068
 
6857
7069
  if (Array.isArray(result) && result.length === 0) {
6858
7070
  return null;
@@ -7335,7 +7547,7 @@
7335
7547
  };
7336
7548
  }
7337
7549
 
7338
- function KeyValueStore() {}
7550
+ function KeyValueStore() { }
7339
7551
 
7340
7552
  KeyValueStore.prototype = {
7341
7553
  keys: [],
@@ -7365,8 +7577,8 @@
7365
7577
 
7366
7578
  function UniqueIndex(uniqueField) {
7367
7579
  this.field = uniqueField;
7368
- this.keyMap = {};
7369
- this.lokiMap = {};
7580
+ this.keyMap = Object.create(null);
7581
+ this.lokiMap = Object.create(null);
7370
7582
  }
7371
7583
  UniqueIndex.prototype.keyMap = {};
7372
7584
  UniqueIndex.prototype.lokiMap = {};
@@ -7406,6 +7618,7 @@
7406
7618
  UniqueIndex.prototype.remove = function (key) {
7407
7619
  var obj = this.keyMap[key];
7408
7620
  if (obj !== null && typeof obj !== 'undefined') {
7621
+ // avoid using `delete`
7409
7622
  this.keyMap[key] = undefined;
7410
7623
  this.lokiMap[obj.$loki] = undefined;
7411
7624
  } else {
@@ -7413,12 +7626,12 @@
7413
7626
  }
7414
7627
  };
7415
7628
  UniqueIndex.prototype.clear = function () {
7416
- this.keyMap = {};
7417
- this.lokiMap = {};
7629
+ this.keyMap = Object.create(null);
7630
+ this.lokiMap = Object.create(null);
7418
7631
  };
7419
7632
 
7420
7633
  function ExactIndex(exactField) {
7421
- this.index = {};
7634
+ this.index = Object.create(null);
7422
7635
  this.field = exactField;
7423
7636
  }
7424
7637
 
@@ -7539,9 +7752,13 @@
7539
7752
  }
7540
7753
  };
7541
7754
 
7542
-
7755
+ Loki.deepFreeze = deepFreeze;
7756
+ Loki.freeze = freeze;
7757
+ Loki.unFreeze = unFreeze;
7543
7758
  Loki.LokiOps = LokiOps;
7544
7759
  Loki.Collection = Collection;
7760
+ Loki.DynamicView = DynamicView;
7761
+ Loki.Resultset = Resultset;
7545
7762
  Loki.KeyValueStore = KeyValueStore;
7546
7763
  Loki.LokiMemoryAdapter = LokiMemoryAdapter;
7547
7764
  Loki.LokiPartitioningAdapter = LokiPartitioningAdapter;