@nymphjs/nymph 1.0.0-beta.55 → 1.0.0-beta.56

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.
@@ -10,9 +10,13 @@ const strtotime_1 = __importDefault(require("locutus/php/datetime/strtotime"));
10
10
  const guid_1 = require("@nymphjs/guid");
11
11
  const errors_1 = require("../errors");
12
12
  const utils_1 = require("../utils");
13
+ // from: https://stackoverflow.com/a/6969486/664915
13
14
  function escapeRegExp(string) {
14
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
15
16
  }
17
+ /**
18
+ * A Nymph database driver.
19
+ */
16
20
  class NymphDriver {
17
21
  constructor() {
18
22
  this.nymph = new Proxy({}, {
@@ -20,15 +24,33 @@ class NymphDriver {
20
24
  throw new Error('Attempted access of Nymph instance before driver initialization!');
21
25
  },
22
26
  });
27
+ /**
28
+ * A cache to make entity retrieval faster.
29
+ */
23
30
  this.entityCache = {};
31
+ /**
32
+ * A counter for the entity cache to determine the most accessed entities.
33
+ */
24
34
  this.entityCount = {};
35
+ /**
36
+ * Protect against infinite loops.
37
+ */
25
38
  this.putDataCounter = 0;
26
39
  }
40
+ /**
41
+ * Initialize the Nymph driver.
42
+ *
43
+ * This is meant to be called internally by Nymph. Don't call this directly.
44
+ *
45
+ * @param nymph The Nymph instance.
46
+ */
27
47
  init(nymph) {
28
48
  this.nymph = nymph;
29
49
  }
30
50
  posixRegexMatch(pattern, subject, caseInsensitive = false) {
31
51
  const posixClasses = [
52
+ // '[[:<:]]',
53
+ // '[[:>:]]',
32
54
  '[:alnum:]',
33
55
  '[:alpha:]',
34
56
  '[:ascii:]',
@@ -45,17 +67,21 @@ class NymphDriver {
45
67
  '[:xdigit:]',
46
68
  ];
47
69
  const jsClasses = [
70
+ // '\b(?=\w)',
71
+ // '(?<=\w)\b',
48
72
  '[A-Za-z0-9]',
49
73
  '[A-Za-z]',
50
74
  '[\x00-\x7F]',
51
- '[ \t]',
52
- '[\x00-\x1F\x7F]',
53
- '[0-9]',
54
- '[\x21-\x7E]',
75
+ '[ \t]', // '\s',
76
+ '[\x00-\x1F\x7F]', // '[\000\001\002\003\004\005\006\007\008\009\010\011\012\013\014'.
77
+ // '\015\016\017\018\019\020\021\022\023\024\025\026\027\028\029'.
78
+ // '\030\031\032\033\034\035\036\037\177]',
79
+ '[0-9]', // '\d',
80
+ '[\x21-\x7E]', // '[A-Za-z0-9!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}\~]',
55
81
  '[a-z]',
56
- '[\x20-\x7E]',
57
- '[!"#$%&\'()*+,-./:;<=>?@[\\\\\\]^_‘{|}~]',
58
- '[ \t\r\n\v\f]',
82
+ '[\x20-\x7E]', // '[A-Za-z0-9!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}\~]',
83
+ '[!"#$%&\'()*+,-./:;<=>?@[\\\\\\]^_‘{|}~]', // '[!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}\~]',
84
+ '[ \t\r\n\v\f]', // '[\t\n\x0B\f\r ]',
59
85
  '[A-Z]',
60
86
  '[A-Za-z0-9_]',
61
87
  '[0-9A-Fa-f]',
@@ -114,9 +140,11 @@ class NymphDriver {
114
140
  const propMatch = text.match(/^\s*([^=]+)\s*=\s*(.*\S)\s*$/);
115
141
  const uidMatch = text.match(/^\s*<([^>]+)>\[(\d+)\]\s*$/);
116
142
  if (uidMatch) {
143
+ // Add the UID.
117
144
  await saveUIDCallback(uidMatch[1], Number(uidMatch[2]));
118
145
  }
119
146
  else if (entityMatch) {
147
+ // Save the current entity.
120
148
  if (guid) {
121
149
  await saveEntityCallback(guid, tags, sdata, etype);
122
150
  guid = null;
@@ -124,17 +152,21 @@ class NymphDriver {
124
152
  sdata = {};
125
153
  etype = '__undefined';
126
154
  }
155
+ // Record the new entity's info.
127
156
  guid = entityMatch[1];
128
157
  etype = entityMatch[2];
129
158
  tags = entityMatch[3].split(',');
130
159
  }
131
160
  else if (propMatch) {
161
+ // Add the variable to the new entity.
132
162
  if (guid) {
133
163
  sdata[propMatch[1]] = propMatch[2];
134
164
  }
135
165
  }
166
+ // Clear the entity cache.
136
167
  this.entityCache = {};
137
168
  }
169
+ // Save the last entity.
138
170
  if (guid) {
139
171
  await saveEntityCallback(guid, tags, sdata, etype);
140
172
  }
@@ -144,132 +176,147 @@ class NymphDriver {
144
176
  return true;
145
177
  }
146
178
  checkData(data, sdata, selectors, guid = null, tags = null) {
147
- const formattedSelectors = this.formatSelectors(selectors).selectors;
148
- for (const curSelector of formattedSelectors) {
149
- const type = curSelector.type;
150
- const typeIsNot = type === '!&' || type === '!|';
151
- const typeIsOr = type === '|' || type === '!|';
152
- let pass = !typeIsOr;
153
- for (const k in curSelector) {
154
- const key = k;
155
- const value = curSelector[key];
156
- if (key === 'type' || value == null) {
157
- continue;
158
- }
159
- const clauseNot = key.substring(0, 1) === '!';
160
- if (key === 'selector' || key === '!selector') {
161
- const tmpArr = (Array.isArray(value) ? value : [value]);
162
- pass = (0, utils_1.xor)(this.checkData(data, sdata, tmpArr, guid, tags), (0, utils_1.xor)(typeIsNot, clauseNot));
163
- }
164
- else {
165
- if (key === 'qref' || key === '!qref') {
166
- throw new Error("Can't use checkData on qref clauses.");
179
+ try {
180
+ const formattedSelectors = this.formatSelectors(selectors).selectors;
181
+ for (const curSelector of formattedSelectors) {
182
+ const type = curSelector.type;
183
+ const typeIsNot = type === '!&' || type === '!|';
184
+ const typeIsOr = type === '|' || type === '!|';
185
+ let pass = !typeIsOr;
186
+ for (const k in curSelector) {
187
+ const key = k;
188
+ const value = curSelector[key];
189
+ if (key === 'type' || value == null) {
190
+ continue;
191
+ }
192
+ const clauseNot = key.substring(0, 1) === '!';
193
+ if (key === 'selector' || key === '!selector') {
194
+ const tmpArr = (Array.isArray(value) ? value : [value]);
195
+ pass = (0, utils_1.xor)(this.checkData(data, sdata, tmpArr, guid, tags), (0, utils_1.xor)(typeIsNot, clauseNot));
167
196
  }
168
197
  else {
169
- for (const curValue of value) {
170
- if (curValue == null) {
171
- continue;
172
- }
173
- const propName = curValue[0];
174
- if (propName in sdata) {
175
- data[propName] = JSON.parse(sdata[propName]);
176
- delete sdata[propName];
177
- }
178
- switch (key) {
179
- case 'guid':
180
- case '!guid':
181
- pass = (0, utils_1.xor)(guid == propName, (0, utils_1.xor)(typeIsNot, clauseNot));
182
- break;
183
- case 'tag':
184
- case '!tag':
185
- pass = (0, utils_1.xor)((tags ?? []).indexOf(propName) !== -1, (0, utils_1.xor)(typeIsNot, clauseNot));
186
- break;
187
- case 'defined':
188
- case '!defined':
189
- pass = (0, utils_1.xor)(propName in data, (0, utils_1.xor)(typeIsNot, clauseNot));
190
- break;
191
- case 'truthy':
192
- case '!truthy':
193
- pass = (0, utils_1.xor)(propName in data && data[propName], (0, utils_1.xor)(typeIsNot, clauseNot));
194
- break;
195
- case 'ref':
196
- case '!ref':
197
- const testRefValue = curValue[1];
198
- pass = (0, utils_1.xor)(propName in data &&
199
- this.entityReferenceSearch(data[propName], testRefValue), (0, utils_1.xor)(typeIsNot, clauseNot));
200
- break;
201
- case 'equal':
202
- case '!equal':
203
- const testEqualValue = curValue[1];
204
- pass = (0, utils_1.xor)(propName in data &&
205
- JSON.stringify(data[propName]) ===
206
- JSON.stringify(testEqualValue), (0, utils_1.xor)(typeIsNot, clauseNot));
207
- break;
208
- case 'contain':
209
- case '!contain':
210
- const testContainValue = curValue[1];
211
- pass = (0, utils_1.xor)(propName in data &&
212
- JSON.stringify(data[propName]).indexOf(JSON.stringify(testContainValue)) !== -1, (0, utils_1.xor)(typeIsNot, clauseNot));
213
- break;
214
- case 'like':
215
- case '!like':
216
- const testLikeValue = curValue[1];
217
- pass = (0, utils_1.xor)(propName in data &&
218
- new RegExp('^' +
219
- escapeRegExp(testLikeValue)
220
- .replace('%', () => '.*')
221
- .replace('_', () => '.') +
222
- '$').test(data[propName]), (0, utils_1.xor)(typeIsNot, clauseNot));
223
- break;
224
- case 'match':
225
- case '!match':
226
- const testMatchValue = curValue[1];
227
- pass = (0, utils_1.xor)(propName in data &&
228
- this.posixRegexMatch(testMatchValue, data[propName]), (0, utils_1.xor)(typeIsNot, clauseNot));
229
- break;
230
- case 'imatch':
231
- case '!imatch':
232
- const testIMatchValue = curValue[1];
233
- pass = (0, utils_1.xor)(propName in data &&
234
- this.posixRegexMatch(testIMatchValue, data[propName], true), (0, utils_1.xor)(typeIsNot, clauseNot));
235
- break;
236
- case 'gt':
237
- case '!gt':
238
- const testGTValue = curValue[1];
239
- pass = (0, utils_1.xor)(propName in data && data[propName] > testGTValue, (0, utils_1.xor)(typeIsNot, clauseNot));
240
- break;
241
- case 'gte':
242
- case '!gte':
243
- const testGTEValue = curValue[1];
244
- pass = (0, utils_1.xor)(propName in data && data[propName] >= testGTEValue, (0, utils_1.xor)(typeIsNot, clauseNot));
245
- break;
246
- case 'lt':
247
- case '!lt':
248
- const testLTValue = curValue[1];
249
- pass = (0, utils_1.xor)(propName in data && data[propName] < testLTValue, (0, utils_1.xor)(typeIsNot, clauseNot));
250
- break;
251
- case 'lte':
252
- case '!lte':
253
- const testLTEValue = curValue[1];
254
- pass = (0, utils_1.xor)(propName in data && data[propName] <= testLTEValue, (0, utils_1.xor)(typeIsNot, clauseNot));
198
+ if (key === 'qref' || key === '!qref') {
199
+ throw new Error("Can't use checkData on qref clauses.");
200
+ }
201
+ else {
202
+ // Check if it doesn't pass any for &, check if it passes any for |.
203
+ for (const curValue of value) {
204
+ if (curValue == null) {
205
+ continue;
206
+ }
207
+ const propName = curValue[0];
208
+ // Unserialize the data for this variable.
209
+ if (propName in sdata) {
210
+ data[propName] = JSON.parse(sdata[propName]);
211
+ delete sdata[propName];
212
+ }
213
+ switch (key) {
214
+ case 'guid':
215
+ case '!guid':
216
+ pass = (0, utils_1.xor)(guid == propName, (0, utils_1.xor)(typeIsNot, clauseNot));
217
+ break;
218
+ case 'tag':
219
+ case '!tag':
220
+ pass = (0, utils_1.xor)((tags ?? []).indexOf(propName) !== -1, (0, utils_1.xor)(typeIsNot, clauseNot));
221
+ break;
222
+ case 'defined':
223
+ case '!defined':
224
+ pass = (0, utils_1.xor)(propName in data, (0, utils_1.xor)(typeIsNot, clauseNot));
225
+ break;
226
+ case 'truthy':
227
+ case '!truthy':
228
+ pass = (0, utils_1.xor)(propName in data && data[propName], (0, utils_1.xor)(typeIsNot, clauseNot));
229
+ break;
230
+ case 'ref':
231
+ case '!ref':
232
+ const testRefValue = curValue[1];
233
+ pass = (0, utils_1.xor)(propName in data &&
234
+ this.entityReferenceSearch(data[propName], testRefValue), (0, utils_1.xor)(typeIsNot, clauseNot));
235
+ break;
236
+ case 'equal':
237
+ case '!equal':
238
+ const testEqualValue = curValue[1];
239
+ pass = (0, utils_1.xor)(propName in data &&
240
+ JSON.stringify(data[propName]) ===
241
+ JSON.stringify(testEqualValue), (0, utils_1.xor)(typeIsNot, clauseNot));
242
+ break;
243
+ case 'contain':
244
+ case '!contain':
245
+ const testContainValue = curValue[1];
246
+ pass = (0, utils_1.xor)(propName in data &&
247
+ JSON.stringify(data[propName]).indexOf(JSON.stringify(testContainValue)) !== -1, (0, utils_1.xor)(typeIsNot, clauseNot));
248
+ break;
249
+ case 'like':
250
+ case '!like':
251
+ const testLikeValue = curValue[1];
252
+ pass = (0, utils_1.xor)(propName in data &&
253
+ new RegExp('^' +
254
+ escapeRegExp(testLikeValue)
255
+ .replace('%', () => '.*')
256
+ .replace('_', () => '.') +
257
+ '$').test(data[propName]), (0, utils_1.xor)(typeIsNot, clauseNot));
258
+ break;
259
+ case 'match':
260
+ case '!match':
261
+ const testMatchValue = curValue[1];
262
+ // Convert a POSIX regex to a JS regex.
263
+ pass = (0, utils_1.xor)(propName in data &&
264
+ this.posixRegexMatch(testMatchValue, data[propName]), (0, utils_1.xor)(typeIsNot, clauseNot));
265
+ break;
266
+ case 'imatch':
267
+ case '!imatch':
268
+ const testIMatchValue = curValue[1];
269
+ // Convert a POSIX regex to a JS regex.
270
+ pass = (0, utils_1.xor)(propName in data &&
271
+ this.posixRegexMatch(testIMatchValue, data[propName], true), (0, utils_1.xor)(typeIsNot, clauseNot));
272
+ break;
273
+ case 'gt':
274
+ case '!gt':
275
+ const testGTValue = curValue[1];
276
+ pass = (0, utils_1.xor)(propName in data && data[propName] > testGTValue, (0, utils_1.xor)(typeIsNot, clauseNot));
277
+ break;
278
+ case 'gte':
279
+ case '!gte':
280
+ const testGTEValue = curValue[1];
281
+ pass = (0, utils_1.xor)(propName in data && data[propName] >= testGTEValue, (0, utils_1.xor)(typeIsNot, clauseNot));
282
+ break;
283
+ case 'lt':
284
+ case '!lt':
285
+ const testLTValue = curValue[1];
286
+ pass = (0, utils_1.xor)(propName in data && data[propName] < testLTValue, (0, utils_1.xor)(typeIsNot, clauseNot));
287
+ break;
288
+ case 'lte':
289
+ case '!lte':
290
+ const testLTEValue = curValue[1];
291
+ pass = (0, utils_1.xor)(propName in data && data[propName] <= testLTEValue, (0, utils_1.xor)(typeIsNot, clauseNot));
292
+ break;
293
+ }
294
+ if (!(0, utils_1.xor)(typeIsOr, pass)) {
255
295
  break;
256
- }
257
- if (!(0, utils_1.xor)(typeIsOr, pass)) {
258
- break;
296
+ }
259
297
  }
260
298
  }
261
299
  }
300
+ if (!(0, utils_1.xor)(typeIsOr, pass)) {
301
+ break;
302
+ }
262
303
  }
263
- if (!(0, utils_1.xor)(typeIsOr, pass)) {
264
- break;
304
+ if (!pass) {
305
+ return false;
265
306
  }
266
307
  }
267
- if (!pass) {
268
- return false;
269
- }
308
+ return true;
309
+ }
310
+ catch (e) {
311
+ this.nymph.config.debugError('nymph', `Failed to check entity data: ${e}`);
312
+ throw e;
270
313
  }
271
- return true;
272
314
  }
315
+ /**
316
+ * Remove all copies of an entity from the cache.
317
+ *
318
+ * @param guid The GUID of the entity to remove.
319
+ */
273
320
  cleanCache(guid) {
274
321
  delete this.entityCache[guid];
275
322
  }
@@ -286,7 +333,15 @@ class NymphDriver {
286
333
  }
287
334
  return ret;
288
335
  }
336
+ /**
337
+ * Search through a value for an entity reference.
338
+ *
339
+ * @param value Any value to search.
340
+ * @param entity An entity, GUID, or array of either to search for.
341
+ * @returns True if the reference is found, false otherwise.
342
+ */
289
343
  entityReferenceSearch(value, entity) {
344
+ // Get the GUID, if the passed $entity is an object.
290
345
  const guids = [];
291
346
  if (Array.isArray(entity)) {
292
347
  if (entity[0] === 'nymph_entity_reference') {
@@ -317,6 +372,7 @@ class NymphDriver {
317
372
  guids.indexOf(value[1]) !== -1) {
318
373
  return true;
319
374
  }
375
+ // Search through arrays and objects looking for the reference.
320
376
  if (Array.isArray(value) || typeof value === 'object') {
321
377
  for (const key in value) {
322
378
  if (this.entityReferenceSearch(value[key], guids)) {
@@ -371,19 +427,23 @@ class NymphDriver {
371
427
  qrefs.push(...newSelectors.qrefs);
372
428
  }
373
429
  else if (!Array.isArray(value)) {
430
+ // @ts-ignore: ts doesn't know what value is here.
374
431
  newSelector[key] = [[value]];
375
432
  }
376
433
  else if (!Array.isArray(value[0])) {
377
434
  if (value.length === 3 &&
378
435
  value[1] == null &&
379
436
  typeof value[2] === 'string') {
437
+ // @ts-ignore: ts doesn't know what value is here.
380
438
  newSelector[key] = [[value[0], (0, strtotime_1.default)(value[2]) * 1000]];
381
439
  }
382
440
  else {
441
+ // @ts-ignore: ts doesn't know what value is here.
383
442
  newSelector[key] = [value];
384
443
  }
385
444
  }
386
445
  else {
446
+ // @ts-ignore: ts doesn't know what value is here.
387
447
  newSelector[key] = value.map((curValue) => {
388
448
  if (curValue.length === 3 &&
389
449
  curValue[1] == null &&
@@ -440,11 +500,14 @@ class NymphDriver {
440
500
  let count = 0;
441
501
  const EntityClass = options.class ?? this.nymph.getEntityClass('Entity');
442
502
  const etype = EntityClass.ETYPE;
503
+ // Check if the requested entity is cached.
443
504
  if (this.nymph.config.cache &&
444
505
  !options.skipCache &&
445
506
  selectors.length &&
446
507
  'guid' in selectors[0] &&
447
508
  typeof selectors[0].guid === 'number') {
509
+ // Only safe to use the cache option with no other selectors than a GUID
510
+ // and tags.
448
511
  if (selectors.length === 1 &&
449
512
  selectors[0].type === '&' &&
450
513
  (Object.keys(selectors[0]).length === 2 ||
@@ -502,9 +565,13 @@ class NymphDriver {
502
565
  const cdate = tagsAndDates.cdate;
503
566
  const mdate = tagsAndDates.mdate;
504
567
  let dataNameAndSValue = getDataNameAndSValueCallback(row);
568
+ // Data.
505
569
  const data = {};
570
+ // Serialized data.
506
571
  const sdata = {};
507
572
  if (dataNameAndSValue.name !== '') {
573
+ // This do will keep going and adding the data until the
574
+ // next entity is reached. $row will end on the next entity.
508
575
  do {
509
576
  dataNameAndSValue = getDataNameAndSValueCallback(row);
510
577
  sdata[dataNameAndSValue.name] = dataNameAndSValue.svalue;
@@ -512,6 +579,7 @@ class NymphDriver {
512
579
  } while (row != null && getGUIDCallback(row) === guid);
513
580
  }
514
581
  else {
582
+ // Make sure that $row is incremented :)
515
583
  row = rowFetchCallback();
516
584
  }
517
585
  const entity = EntityClass.factorySync();
@@ -532,6 +600,7 @@ class NymphDriver {
532
600
  if (this.nymph.config.cache) {
533
601
  this.pushCache(guid, cdate, mdate, tags, data, sdata);
534
602
  }
603
+ // @ts-ignore: ts doesn't know the return type here.
535
604
  entities.push(entity);
536
605
  }
537
606
  }
@@ -550,6 +619,7 @@ class NymphDriver {
550
619
  }
551
620
  }
552
621
  async saveEntityRowLike(entity, saveNewEntityCallback, saveExistingEntityCallback, startTransactionCallback = null, commitTransactionCallback = null) {
622
+ // Get a modified date.
553
623
  const mdate = Date.now();
554
624
  const tags = (0, lodash_1.difference)(entity.tags, ['']);
555
625
  const data = entity.$getData();
@@ -571,6 +641,7 @@ class NymphDriver {
571
641
  }
572
642
  }
573
643
  else {
644
+ // Removed any cached versions of this entity.
574
645
  if (this.nymph.config.cache) {
575
646
  this.cleanCache(entity.guid);
576
647
  }
@@ -582,12 +653,22 @@ class NymphDriver {
582
653
  if (commitTransactionCallback) {
583
654
  success = await commitTransactionCallback(success);
584
655
  }
656
+ // Cache the entity.
585
657
  if (success && this.nymph.config.cache) {
586
658
  this.pushCache(entity.guid, entity.cdate, entity.mdate, entity.tags, data, sdata);
587
659
  }
588
660
  return success;
589
661
  }
662
+ /**
663
+ * Pull an entity from the cache.
664
+ *
665
+ * @param guid The entity's GUID.
666
+ * @param className The entity's class.
667
+ * @param useSkipAc Whether to tell the entity to use skip_ac.
668
+ * @returns The entity or null if it's not cached.
669
+ */
590
670
  pullCache(guid, className, useSkipAc = false) {
671
+ // Increment the entity access count.
591
672
  if (!(guid in this.entityCount)) {
592
673
  this.entityCount[guid] = 0;
593
674
  }
@@ -607,17 +688,30 @@ class NymphDriver {
607
688
  }
608
689
  return null;
609
690
  }
691
+ /**
692
+ * Push an entity onto the cache.
693
+ *
694
+ * @param guid The entity's GUID.
695
+ * @param cdate The entity's cdate.
696
+ * @param mdate The entity's mdate.
697
+ * @param tags The entity's tags.
698
+ * @param data The entity's data.
699
+ * @param sdata The entity's sdata.
700
+ */
610
701
  pushCache(guid, cdate, mdate, tags, data, sdata) {
611
702
  if (guid == null) {
612
703
  return;
613
704
  }
705
+ // Increment the entity access count.
614
706
  if (!(guid in this.entityCount)) {
615
707
  this.entityCount[guid] = 0;
616
708
  }
617
709
  this.entityCount[guid]++;
710
+ // Check the threshold.
618
711
  if (this.entityCount[guid] < this.nymph.config.cacheThreshold) {
619
712
  return;
620
713
  }
714
+ // Cache the entity.
621
715
  this.entityCache[guid] = {
622
716
  cdate: cdate,
623
717
  mdate: mdate,
@@ -627,12 +721,15 @@ class NymphDriver {
627
721
  };
628
722
  while (this.nymph.config.cacheLimit &&
629
723
  Object.keys(this.entityCache).length >= this.nymph.config.cacheLimit) {
724
+ // Find which entity has been accessed the least.
630
725
  const least = Object.entries(this.entityCount)
631
726
  .sort(([guidA, countA], [guidB, countB]) => countA - countB)
632
727
  .pop()?.[0] ?? null;
633
728
  if (least == null) {
729
+ // This should never happen.
634
730
  return;
635
731
  }
732
+ // Remove it.
636
733
  delete this.entityCache[least];
637
734
  delete this.entityCount[least];
638
735
  }