Source: collections/Cache.js

  1. const { Dictionary, DictionaryMapBased, symbolDictionaryGet } = require('./Dictionary')
  2. , { EqualityComparer } = require('./EqualityComparer')
  3. , { Resolve } = require('../tools/Resolve')
  4. , { CollectionEvent } = require('./Collection')
  5. , { Observable, fromEvent } = require('rxjs')
  6. , symbolCacheEvict = Symbol('cacheEvict')
  7. , JSBI = require('jsbi');
  8. /**
  9. * This enumeration holds supported eviction policies by some of collections.
  10. *
  11. * @readonly
  12. * @enum {Number}
  13. */
  14. const EvictionPolicy = {
  15. /**
  16. * Do not evict. Requires manually freeing space in the underlying collection.
  17. */
  18. None: 0,
  19. /**
  20. * The underlying collection may evict elements indeterministically.
  21. */
  22. Undetermined: 1,
  23. /**
  24. * The underlying collection evicts the least recently used elements first.
  25. */
  26. LRU: 2,
  27. /**
  28. * The underlying collection evicts the most recently used elements first.
  29. */
  30. MRU: 3,
  31. /**
  32. * The underlying collection evicts the least frequently used elements first.
  33. */
  34. LFU: 4,
  35. /**
  36. * The underlying collection evicts the most frequently used elements first.
  37. */
  38. MFU: 5,
  39. /**
  40. * The underlying collection evicts the items inserted first (like a queue).
  41. */
  42. FIFO: 6,
  43. /**
  44. * The underlying collection evicts the items inserted last (like a stack).
  45. */
  46. LIFO: 7,
  47. /**
  48. * The underlying collection evicts the items inserted in a random order.
  49. */
  50. Random: 8
  51. };
  52. /**
  53. * @template T
  54. * @author Sebastian Hönel <development@hoenel.net>
  55. */
  56. class CacheItem {
  57. /**
  58. * @param {Cache<any, T>|CacheMapBased<any, T>|CacheWithLoad<any, T>} cache The cache this item belongs to.
  59. * @param {TKey} key
  60. * @param {T} item
  61. * @param {Number} order An integer used to order this item, assigned by the cache.
  62. * @param {Number} expireAfterMsecs An integer, in milliseconds, to expire this
  63. * item after. Only used if > 0. This item then sets a timeout and upon elapse,
  64. * evicts itself from the cache.
  65. */
  66. constructor(cache, key, item, order, expireAfterMsecs) {
  67. /** @protected */
  68. this._cache = cache;
  69. /** @protected */
  70. this._key = key;
  71. /** @protected */
  72. this._item = item;
  73. // initialize with order, so that newer items w/o last access have larger values.
  74. /**
  75. * @type {JSBI.BigInt}
  76. * @protected
  77. */
  78. this._timeStamp = JSBI.BigInt(order);
  79. /** @protected */
  80. this._order = order;
  81. /** @protected */
  82. this._accessCount = 0;
  83. /**
  84. * @type {NodeJS.Timeout}
  85. * @protected
  86. */
  87. this._expireTimeout = null;
  88. if (expireAfterMsecs > 0) {
  89. this._expireTimeout = setTimeout(() => {
  90. this._cache.delete(key);
  91. }, expireAfterMsecs);
  92. }
  93. };
  94. /**
  95. * Clears the internal eviction timeout.
  96. *
  97. * @returns {this}
  98. */
  99. clearTimeout() {
  100. clearTimeout(this._expireTimeout);
  101. this._expireTimeout = null;
  102. return this;
  103. };
  104. /**
  105. * @type {String|Symbol}
  106. */
  107. get key() {
  108. return this._key;
  109. };
  110. /**
  111. * @type {T}
  112. */
  113. get item() {
  114. return this._item;
  115. };
  116. /**
  117. * @type {JSBI.BigInt}
  118. */
  119. get timeStamp() {
  120. return this._timeStamp;
  121. };
  122. /**
  123. * @returns {void}
  124. */
  125. updateAccessTime() {
  126. const hrTime = process.hrtime()
  127. , secsAsNano = JSBI.multiply(JSBI.BigInt(hrTime[0]), JSBI.BigInt(1e9))
  128. , withNs = JSBI.add(secsAsNano, JSBI.BigInt(hrTime[1]));
  129. this._timeStamp = withNs;
  130. };
  131. /**
  132. * @type {Number}
  133. */
  134. get order() {
  135. return this._order;
  136. };
  137. /**
  138. * @type {Number}
  139. */
  140. get accessCount() {
  141. return this._accessCount;
  142. };
  143. /**
  144. * @returns {this}
  145. */
  146. increaseAccessCount() {
  147. this._accessCount++;
  148. return this;
  149. };
  150. };
  151. let __counter = 0;
  152. /**
  153. * @deprecated Use @see {CacheMapBased} instead. This class will be
  154. * removed from Version v3.0.
  155. * A Cache is similar to a @see {DictionaryMapBased}, and also allows to manually and automatically
  156. * evict items stored in it. A Cache's capacity is constrained.
  157. *
  158. * @template TKey
  159. * @template TVal
  160. * @author Sebastian Hönel <development@hoenel.net>
  161. */
  162. class Cache extends Dictionary {
  163. /**
  164. * Used to assign a running number to new cache items.
  165. * @private
  166. * @type {Number}
  167. */
  168. static get __counter() {
  169. return __counter++;
  170. };
  171. /**
  172. * Creates a new, empty Cache<T>, using the specified @see {EvictionPolicy} and
  173. * an initial capacity.
  174. *
  175. * @param {EvictionPolicy|Number} [evictPolicy] Optional. Specify the eviction policy that is
  176. * applied, if the cache has no more space left.
  177. * @param {Number} [capacity] Optional. Specify the initial capacity of this cache. The capacity
  178. * can be increased and decreased later.
  179. * @param {EqualityComparer<TVal>} [eqComparer] Optional. Defaults To EqualityComparer<TVal>.default.
  180. */
  181. constructor(evictPolicy = EvictionPolicy.None, capacity = Number.MAX_SAFE_INTEGER, eqComparer = EqualityComparer.default) {
  182. super(eqComparer);
  183. /**
  184. * @type {EvictionPolicy|Number}
  185. * @protected
  186. */
  187. this._evictPolicy = EvictionPolicy.None;
  188. this.evictionPolicy = evictPolicy;
  189. /**
  190. * @type {Number}
  191. * @protected
  192. */
  193. this._capacity = capacity;
  194. this.capacity = capacity;
  195. /**
  196. * @type {Object<String|Symbol, CacheItem<T>>}
  197. * @protected
  198. */
  199. this._dict = {};
  200. };
  201. /**
  202. * @param {EvictionPolicy|Number} policy An @see {EvictionPolicy}
  203. * @throws {Error} If the given policy is not one of @see {EvictionPolicy}.
  204. * @protected
  205. */
  206. _validateEvictionPolicy(policy) {
  207. switch (policy) {
  208. case EvictionPolicy.LRU:
  209. case EvictionPolicy.MRU:
  210. case EvictionPolicy.LFU:
  211. case EvictionPolicy.MFU:
  212. case EvictionPolicy.None:
  213. case EvictionPolicy.FIFO:
  214. case EvictionPolicy.LIFO:
  215. case EvictionPolicy.Random:
  216. case EvictionPolicy.Undetermined:
  217. break;
  218. default:
  219. throw new Error(`The eviction policy '${policy}' is not supported.`);
  220. }
  221. };
  222. /**
  223. * Get the current @see {EvictionPolicy}.
  224. *
  225. * @type {EvictionPolicy|Number}
  226. */
  227. get evictionPolicy() {
  228. return this._evictPolicy;
  229. };
  230. /**
  231. * Set the current @see {EvictionPolicy}.
  232. *
  233. * @param {EvictionPolicy|Number} policy The new policy to use for evicting items.
  234. * @type {void}
  235. */
  236. set evictionPolicy(policy) {
  237. this._validateEvictionPolicy(policy);
  238. this._evictPolicy = policy;
  239. };
  240. /**
  241. * @param {Number} capacity The capacity to check. Must be a positive integer.
  242. * @throws {Error} If the given capacity is not an integer or less than zero.
  243. * @protected
  244. */
  245. _validateCapacity(capacity) {
  246. if (!Resolve.isTypeOf(capacity, Number) || !Number.isInteger(capacity)) {
  247. throw new Error(`The value given for capacity is not a number.`);
  248. }
  249. if (capacity < 0) {
  250. throw new Error(`The capacity given is less than 0: ${capacity}`);
  251. }
  252. };
  253. /**
  254. * Returns the current capacity of this cache.
  255. *
  256. * @type {Number}
  257. */
  258. get capacity() {
  259. return this._capacity;
  260. };
  261. /**
  262. * Returns true, if the cache's capacity is all used up.
  263. *
  264. * @type {Boolean}
  265. */
  266. get isFull() {
  267. return this.size === this.capacity;
  268. };
  269. /**
  270. * @param {Number} capacity The new capacity of this cache. Must be a positive
  271. * integer (0 is allowed). Note that setting a capacity that undercuts the amount
  272. * of items currently held in the cache, will lead to the eviction of enough
  273. * items to meet the new capacity constraint.
  274. * @type {void}
  275. */
  276. set capacity(capacity) {
  277. this._validateCapacity(capacity);
  278. let toEvict = this.size - capacity;
  279. if (toEvict > 0 && this.evictionPolicy === EvictionPolicy.None) {
  280. throw new Error('Cannot decrease capacity and automatically evict items with policy set to None.');
  281. }
  282. while (toEvict > 0) {
  283. this.evict();
  284. toEvict--;
  285. }
  286. this._capacity = capacity;
  287. };
  288. /**
  289. * @returns {T} the evicted item.
  290. */
  291. evict() {
  292. if (this.isEmpty) {
  293. throw new Error('The cache is empty, cannot evict any more items.');
  294. }
  295. return this.evictMany(1)[0];
  296. };
  297. /**
  298. * @param {Number} count
  299. * @returns {Array<T>} The evicted items in an array.
  300. */
  301. evictMany(count) {
  302. let howMany = Math.min(count, this.size);
  303. /** @type {Array<T>} */
  304. const items = [];
  305. for (const item of this._evictNext()) {
  306. items.push(this.remove(item.key));
  307. if (--howMany === 0) {
  308. break;
  309. }
  310. }
  311. return items;
  312. };
  313. /**
  314. * Allows to peek at the next n items that will be evited next, without removing
  315. * them. Returns the items with as key/value pair.
  316. *
  317. * @param {Number} amount
  318. * @returns {Array<Object<String|Symbol, T>>}
  319. */
  320. peekEvict(amount) {
  321. if (!Resolve.isTypeOf(amount, Number) || !Number.isInteger(amount) || amount < 1) {
  322. throw new Error('You must peek an mount larger than 0.');
  323. }
  324. amount = Math.min(amount, this.size);
  325. const iter = this._evictNext();
  326. return Array.from(Array(amount), iter.next, iter).map(o => {
  327. const obj = {};
  328. obj[o.value.key] = o.value.item;
  329. return obj;
  330. });
  331. };
  332. /**
  333. * Returns items in the order they will be evicted, according to the policy.
  334. *
  335. * @protected
  336. * @returns {IterableIterator<CacheItem<T>>}
  337. */
  338. *_evictNext() {
  339. /** @type {Array<CacheItem<T>>} */
  340. const wrappers = [];
  341. switch (this.evictionPolicy) {
  342. case EvictionPolicy.LRU:
  343. case EvictionPolicy.MRU:
  344. // Order items by their access date:
  345. const mru = this.evictionPolicy === EvictionPolicy.MRU ? -1 : 1;
  346. this.evictionPolicy === EvictionPolicy.MRU ? -1 : 1;
  347. wrappers.unshift(
  348. ...Array.from(super.values()).sort((w1, w2) => {
  349. return mru * (JSBI.lessThan(w1.timeStamp, w2.timeStamp) ? -1 : 1);
  350. })
  351. );
  352. break;
  353. case EvictionPolicy.LFU:
  354. case EvictionPolicy.MFU:
  355. // Order items by access count and select least/most recently used items first.
  356. const mfu = this.evictionPolicy === EvictionPolicy.MFU ? -1 : 1;
  357. wrappers.unshift(
  358. ...Array.from(super.values()).sort((w1, w2) => {
  359. return mfu * (w1.accessCount - w2.accessCount);
  360. })
  361. );
  362. break;
  363. case EvictionPolicy.FIFO:
  364. case EvictionPolicy.LIFO:
  365. // Remove the item inserted first/last.
  366. const lifo = this.evictionPolicy === EvictionPolicy.LIFO ? -1 : 1;
  367. wrappers.unshift(
  368. ...Array.from(super.values()).sort((w1, w2) => {
  369. return lifo * (w1.order - w2.order);
  370. })
  371. );
  372. break;
  373. case EvictionPolicy.Undetermined:
  374. // No specific/deterministic order.
  375. yield* super.values();
  376. break;
  377. case EvictionPolicy.Random:
  378. // Randomly select items to evict:
  379. wrappers.unshift(
  380. ...Array.from(super.values()).sort(() => {
  381. return Math.random() < .5 ? 1 : -1;
  382. })
  383. );
  384. break;
  385. case EvictionPolicy.None:
  386. // Eviction not allowed, must use ::remove()
  387. throw new Error('Eviction not allowed.');
  388. }
  389. for (const wrap of wrappers) {
  390. yield wrap;
  391. }
  392. };
  393. /**
  394. * @override
  395. * @inheritdoc
  396. * @param {Number} [expireAfterMsecs] Optional. if given a positive integer, then it
  397. * will be used as the amount of milliseconds this entry will expire and remove itself
  398. * from the cache.
  399. * @throws {Error} If this cache is full and no automatic eviction policy was set.
  400. * @type {void}
  401. */
  402. set(key, val, expireAfterMsecs = void 0) {
  403. const isFull = this.size === this.capacity;
  404. if (isFull) {
  405. if (this.evictionPolicy === EvictionPolicy.None) {
  406. throw new Error(`The Cache is full and no automatic eviction policy is set.`);
  407. }
  408. this.evict(); // Remove one item according to an automatic policy.
  409. }
  410. if (!Resolve.isNumber(expireAfterMsecs) || !Number.isSafeInteger(expireAfterMsecs) || expireAfterMsecs < 0) {
  411. expireAfterMsecs = -1;
  412. }
  413. const wrap = new CacheItem(this, key, val, Cache.__counter, expireAfterMsecs);
  414. return super.set(key, wrap);
  415. };
  416. /**
  417. * @override
  418. * @inheritdoc
  419. */
  420. get(key) {
  421. /** @type {CacheItem<T>} */
  422. const wrap = super.get(key);
  423. wrap.updateAccessTime();
  424. wrap.increaseAccessCount();
  425. return wrap.item;
  426. };
  427. /**
  428. * @override
  429. * @inheritdoc
  430. */
  431. remove(key) {
  432. /** @type {CacheItem<T>} */
  433. const wrap = super.remove(key);
  434. wrap.clearTimeout();
  435. return wrap.item;
  436. };
  437. /**
  438. * @override
  439. * @inheritdoc
  440. */
  441. *values() {
  442. for (const val of super.values()) {
  443. yield val.item;
  444. }
  445. };
  446. /**
  447. * @override
  448. * @inheritdoc
  449. * @protected
  450. */
  451. *_entries(reverse = false) {
  452. for (const entry of super._entries(reverse)) {
  453. const key = Object.keys(entry).concat(Object.getOwnPropertySymbols(entry))[0];
  454. entry[key] = entry[key].item; // unwrap
  455. yield entry;
  456. }
  457. };
  458. };
  459. let __counterCb = 0;
  460. /**
  461. * @template TKey
  462. * @template TVal
  463. * @author Sebastian Hönel <development@hoenel.net>
  464. */
  465. class CacheMapBased extends DictionaryMapBased {
  466. /**
  467. * Used to assign a running number to new cache items.
  468. * @private
  469. * @type {Number}
  470. */
  471. static get __counter() {
  472. return __counterCb++;
  473. };
  474. /**
  475. * Creates a new, empty CacheMapBased<TKey, TVal>, using the specified @see {EvictionPolicy} and
  476. * an initial capacity.
  477. *
  478. * @param {EvictionPolicy|Number} [evictPolicy] Optional. Specify the eviction policy that is
  479. * applied, if the cache has no more space left.
  480. * @param {Number} [capacity] Optional. Specify the initial capacity of this cache. The capacity
  481. * can be increased and decreased later.
  482. * @param {EqualityComparer<TVal>} [eqComparer] Optional. Defaults To EqualityComparer<TVal>.default.
  483. */
  484. constructor(evictPolicy = EvictionPolicy.None, capacity = Number.MAX_SAFE_INTEGER, eqComparer = EqualityComparer.default) {
  485. super(eqComparer);
  486. /**
  487. * @type {EvictionPolicy|Number}
  488. * @protected
  489. */
  490. this._evictPolicy = EvictionPolicy.None;
  491. this.evictionPolicy = evictPolicy;
  492. /**
  493. * @type {Number}
  494. * @protected
  495. */
  496. this._capacity = capacity;
  497. this.capacity = capacity;
  498. /**
  499. * @type {Map<TKey, CacheItem<TVal>>}
  500. * @protected
  501. */
  502. this._map = new Map();
  503. /** @type {Observable<CollectionEvent<Array<TVal>>>} */
  504. this.observableEvict = Object.freeze(fromEvent(this, symbolCacheEvict));
  505. };
  506. /**
  507. * @param {EvictionPolicy|Number} policy An @see {EvictionPolicy}
  508. * @throws {Error} If the given policy is not one of @see {EvictionPolicy}.
  509. * @protected
  510. */
  511. _validateEvictionPolicy(policy) {
  512. switch (policy) {
  513. case EvictionPolicy.LRU:
  514. case EvictionPolicy.MRU:
  515. case EvictionPolicy.LFU:
  516. case EvictionPolicy.MFU:
  517. case EvictionPolicy.None:
  518. case EvictionPolicy.FIFO:
  519. case EvictionPolicy.LIFO:
  520. case EvictionPolicy.Random:
  521. case EvictionPolicy.Undetermined:
  522. break;
  523. default:
  524. throw new Error(`The eviction policy '${policy}' is not supported.`);
  525. }
  526. };
  527. /**
  528. * @type {EvictionPolicy|Number}
  529. */
  530. get evictionPolicy() {
  531. return this._evictPolicy;
  532. };
  533. /**
  534. * @param {EvictionPolicy|Number} policy The new policy to use for evicting items.
  535. */
  536. set evictionPolicy(policy) {
  537. this._validateEvictionPolicy(policy);
  538. this._evictPolicy = policy;
  539. };
  540. /**
  541. * @param {Number} capacity The capacity to check. Must be a positive integer.
  542. * @throws {Error} If the given capacity is not an integer or less than zero.
  543. * @protected
  544. */
  545. _validateCapacity(capacity) {
  546. if (!Resolve.isTypeOf(capacity, Number) || !Number.isInteger(capacity)) {
  547. throw new Error(`The value given for capacity is not a number.`);
  548. }
  549. if (capacity < 0) {
  550. throw new Error(`The capacity given is less than 0: ${capacity}`);
  551. }
  552. };
  553. /**
  554. * Returns the current capacity of this cache.
  555. *
  556. * @type {Number}
  557. */
  558. get capacity() {
  559. return this._capacity;
  560. };
  561. /**
  562. * Returns true, if the cache's capacity is all used up.
  563. *
  564. * @type {Boolean}
  565. */
  566. get isFull() {
  567. return this.size === this.capacity;
  568. };
  569. /**
  570. * @param {Number} capacity The new capacity of this cache. Must be a positive
  571. * integer (0 is allowed). Note that setting a capacity that undercuts the amount
  572. * of items currently held in the cache, will lead to the eviction of enough
  573. * items to meet the new capacity constraint.
  574. * @type {void}
  575. */
  576. set capacity(capacity) {
  577. this._validateCapacity(capacity);
  578. let toEvict = this.size - capacity;
  579. if (toEvict > 0 && this.evictionPolicy === EvictionPolicy.None) {
  580. throw new Error('Cannot decrease capacity and automatically evict items with policy set to None.');
  581. }
  582. while (toEvict > 0) {
  583. this.evict();
  584. toEvict--;
  585. }
  586. this._capacity = capacity;
  587. };
  588. /**
  589. * @returns {T} the evicted item.
  590. */
  591. evict() {
  592. if (this.isEmpty) {
  593. throw new Error('The cache is empty, cannot evict any more items.');
  594. }
  595. return this.evictMany(1)[0];
  596. };
  597. /**
  598. * @param {Number} count
  599. * @returns {Array<T>} The evicted items in an array.
  600. */
  601. evictMany(count) {
  602. let howMany = Math.min(count, this.size);
  603. /** @type {Array<T>} */
  604. const items = [];
  605. for (const item of this._evictNext()) {
  606. items.push(this.delete(item.key));
  607. if (--howMany === 0) {
  608. break;
  609. }
  610. }
  611. this.emit(symbolCacheEvict, new CollectionEvent(items.slice(0)));
  612. return items;
  613. };
  614. /**
  615. * Allows to peek at the next n items that will be evicted next, without
  616. * removing them. Returns the items as key/value pair.
  617. *
  618. * @param {Number} amount
  619. * @returns {Array<[TKey, TVal]>}
  620. */
  621. peekEvict(amount) {
  622. if (!Resolve.isTypeOf(amount, Number) || !Number.isInteger(amount) || amount < 1) {
  623. throw new Error('You must peek an mount larger than 0.');
  624. }
  625. amount = Math.min(amount, this.size);
  626. const iter = this._evictNext();
  627. return Array.from(Array(amount), iter.next, iter).map(o => {
  628. return [o.value.key, o.value.item];
  629. });
  630. };
  631. /**
  632. * Returns items in the order they will be evicted, according to the policy.
  633. *
  634. * @protected
  635. * @returns {IterableIterator<CacheItem<T>>}
  636. */
  637. *_evictNext() {
  638. /** @type {Array<CacheItem<T>>} */
  639. const wrappers = [];
  640. switch (this.evictionPolicy) {
  641. case EvictionPolicy.LRU:
  642. case EvictionPolicy.MRU:
  643. // Order items by their access date:
  644. const mru = this.evictionPolicy === EvictionPolicy.MRU ? -1 : 1;
  645. this.evictionPolicy === EvictionPolicy.MRU ? -1 : 1;
  646. wrappers.unshift(
  647. ...Array.from(super.values()).sort((w1, w2) => {
  648. return mru * (JSBI.lessThan(w1.timeStamp, w2.timeStamp) ? -1 : 1);
  649. })
  650. );
  651. break;
  652. case EvictionPolicy.LFU:
  653. case EvictionPolicy.MFU:
  654. // Order items by access count and select least/most recently used items first.
  655. const mfu = this.evictionPolicy === EvictionPolicy.MFU ? -1 : 1;
  656. wrappers.unshift(
  657. ...Array.from(super.values()).sort((w1, w2) => {
  658. return mfu * (w1.accessCount - w2.accessCount);
  659. })
  660. );
  661. break;
  662. case EvictionPolicy.FIFO:
  663. case EvictionPolicy.LIFO:
  664. // Remove the item inserted first/last.
  665. const lifo = this.evictionPolicy === EvictionPolicy.LIFO ? -1 : 1;
  666. wrappers.unshift(
  667. ...Array.from(super.values()).sort((w1, w2) => {
  668. return lifo * (w1.order - w2.order);
  669. })
  670. );
  671. break;
  672. case EvictionPolicy.Undetermined:
  673. // No specific/deterministic order.
  674. yield* super.values();
  675. break;
  676. case EvictionPolicy.Random:
  677. // Randomly select items to evict:
  678. wrappers.unshift(
  679. ...Array.from(super.values()).sort(() => {
  680. return Math.random() < .5 ? 1 : -1;
  681. })
  682. );
  683. break;
  684. case EvictionPolicy.None:
  685. // Eviction not allowed, must use ::remove()
  686. throw new Error('Eviction not allowed.');
  687. }
  688. for (const wrap of wrappers) {
  689. yield wrap;
  690. }
  691. };
  692. /**
  693. * @override
  694. * @inheritdoc
  695. * @param {Number} [expireAfterMsecs] Optional. if given a positive integer, then it
  696. * will be used as the amount of milliseconds this entry will expire and remove itself
  697. * from the cache.
  698. * @throws {Error} If this cache is full and no automatic eviction policy was set.
  699. */
  700. set(key, val, expireAfterMsecs = void 0) {
  701. const isFull = this.size === this.capacity;
  702. if (isFull) {
  703. if (this.evictionPolicy === EvictionPolicy.None) {
  704. throw new Error(`The Cache is full and no automatic eviction policy is set.`);
  705. }
  706. this.evict(); // Remove one item according to an automatic policy.
  707. }
  708. if (!Resolve.isNumber(expireAfterMsecs) || !Number.isSafeInteger(expireAfterMsecs) || expireAfterMsecs < 0) {
  709. expireAfterMsecs = -1;
  710. }
  711. const wrap = new CacheItem(this, key, val, CacheMapBased.__counter, expireAfterMsecs);
  712. return super.set(key, wrap);
  713. };
  714. /**
  715. * @override
  716. * @inheritdoc
  717. */
  718. get(key) {
  719. const wrap = this._map.get(key);
  720. wrap.updateAccessTime();
  721. wrap.increaseAccessCount();
  722. const value = this.__unwrapValue(wrap);
  723. this.emit(symbolDictionaryGet, new CollectionEvent([key, value]));
  724. return value;
  725. };
  726. /**
  727. * @override
  728. * @inheritdoc
  729. */
  730. *values() {
  731. for (const val of super.values()) {
  732. yield this.__unwrapValue(val);
  733. }
  734. };
  735. /**
  736. * @override
  737. * @inheritdoc
  738. */
  739. *entries() {
  740. for (const entry of super.entries()) {
  741. yield [entry[0], this.__unwrapValue(entry[1])];
  742. }
  743. };
  744. /**
  745. * @private
  746. * @override
  747. * @inheritdoc
  748. * @param {CacheItem<TVal>} value
  749. * @returns {TVal} The wrapped value
  750. */
  751. __unwrapValue(value) {
  752. return value.item;
  753. };
  754. };
  755. /**
  756. * @template T
  757. * @author Sebastian Hönel <development@hoenel.net>
  758. */
  759. class CacheItemWithLoad extends CacheItem {
  760. /**
  761. * @inheritdoc
  762. * @param {Number} load
  763. */
  764. constructor(cache, key, item, order, expireAfterMsecs, load) {
  765. super(cache, key, item, order, expireAfterMsecs);
  766. /** @protected */
  767. this._load = load;
  768. };
  769. /**
  770. * @type {Number}
  771. */
  772. get load() {
  773. return this._load;
  774. };
  775. };
  776. /**
  777. * A CacheWithLoad introduces a second concept to constrain the capacity
  778. * of it. This second concept of load is useful for when the size of the
  779. * items to cache is not known, and when the sheer amount of items does
  780. * not allow to estimate the load. The CacheWithLoad evicts items when
  781. * either its capacity is exhausted or the maximum load is exceeded.
  782. *
  783. * @template TKey
  784. * @template TVal
  785. * @author Sebastian Hönel <development@hoenel.net>
  786. */
  787. class CacheWithLoad extends CacheMapBased {
  788. /**
  789. * @inheritdoc
  790. * @param {EvictionPolicy|Number} evictPolicy
  791. * @param {Number} capacity
  792. * @param {Number} maxLoad The maximum load, expressed as a positive number.
  793. * @param {EqualityComparer<TVal>} eqComparer
  794. */
  795. constructor(evictPolicy = EvictionPolicy.None, capacity = Number.MAX_SAFE_INTEGER, maxLoad = Number.MAX_VALUE, eqComparer = EqualityComparer.default) {
  796. super(evictPolicy, capacity, eqComparer);
  797. /** @protected */
  798. this._maxLoad = maxLoad;
  799. this.maxLoad = maxLoad;
  800. /**
  801. * @type {Object<String|Symbol, CacheItemWithLoad<T>>}
  802. * @protected
  803. */
  804. this._dict = {};
  805. };
  806. /**
  807. * @protected
  808. * @param {Number} load
  809. */
  810. _validateLoad(load) {
  811. if (!Resolve.isTypeOf(load, Number) || !Number.isFinite(load)) {
  812. throw new Error(`The value given for load is not a number.`);
  813. }
  814. if (load <= 0) {
  815. throw new Error(`The value given for load must be greater than zero.`);
  816. }
  817. };
  818. /**
  819. * Returns the current load.
  820. *
  821. * @type {Number}
  822. */
  823. get load() {
  824. return Array.from(
  825. // To avoid unwrapping:
  826. DictionaryMapBased.prototype.values.call(this)
  827. ).map(ci => ci.load).reduce((a, b) => a + b, 0);
  828. };
  829. /**
  830. * Returns the current free load (maximum load - current load).
  831. *
  832. * @type {Number}
  833. */
  834. get loadFree() {
  835. return this.maxLoad - this.load;
  836. };
  837. /**
  838. * Set the maximum load.
  839. *
  840. * @param {Number} maxLoad
  841. * @type {void}
  842. */
  843. set maxLoad(maxLoad) {
  844. this._validateLoad(maxLoad);
  845. if (maxLoad < this.load && this.evictionPolicy === EvictionPolicy.None) {
  846. throw new Error('Cannot decrease the maximum load and automatically evict items with policy set to None.');
  847. }
  848. while (this.load > maxLoad) {
  849. this.evict();
  850. }
  851. this._maxLoad = maxLoad;
  852. };
  853. /**
  854. * Returns the maximum load.
  855. *
  856. * @type {Number}
  857. */
  858. get maxLoad() {
  859. return this._maxLoad;
  860. };
  861. /**
  862. * @override
  863. * @inheritdoc
  864. * @param {TKey} key
  865. * @param {TVal} val
  866. * @param {Number} load
  867. * @param {Number} [expireAfterMsecs] defaults to undefined. Only used if given
  868. * as a positive integer.
  869. */
  870. set(key, val, load, expireAfterMsecs = void 0) {
  871. this._validateLoad(load);
  872. if (load > this.maxLoad) {
  873. throw new Error(`The specified load exceeds this cache's maximum load.`);
  874. }
  875. const isFull = this.size === this.capacity;
  876. if (isFull) {
  877. if (this.evictionPolicy === EvictionPolicy.None) {
  878. throw new Error(`The Cache is full and no automatic eviction policy is set.`);
  879. }
  880. this.evict(); // Remove one item according to an automatic policy.
  881. }
  882. if (load + this.load > this.maxLoad) {
  883. if (this.evictionPolicy === EvictionPolicy.None) {
  884. throw new Error(`The Cache's load is too high and no automatic eviction policy is set.`);
  885. }
  886. while (load + this.load > this.maxLoad) {
  887. this.evict();
  888. }
  889. }
  890. if (!Resolve.isNumber(expireAfterMsecs) || !Number.isSafeInteger(expireAfterMsecs) || expireAfterMsecs < 0) {
  891. expireAfterMsecs = -1;
  892. }
  893. const wrap = new CacheItemWithLoad(
  894. this, key, val, CacheMapBased.__counter, expireAfterMsecs, load);
  895. return DictionaryMapBased.prototype.set.call(this, key, wrap);
  896. };
  897. };
  898. module.exports = Object.freeze({
  899. EvictionPolicy,
  900. Cache,
  901. CacheMapBased,
  902. CacheItem,
  903. CacheWithLoad,
  904. CacheItemWithLoad
  905. });