StackedCacheMap.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. /**
  7. * The StackedCacheMap is a data structure designed as an alternative to a Map
  8. * in situations where you need to handle multiple item additions and
  9. * frequently access the largest map.
  10. *
  11. * It is particularly optimized for efficiently adding multiple items
  12. * at once, which can be achieved using the `addAll` method.
  13. *
  14. * It has a fallback Map that is used when the map to be added is mutable.
  15. *
  16. * Note: `delete` and `has` are not supported for performance reasons.
  17. * @example
  18. * ```js
  19. * const map = new StackedCacheMap();
  20. * map.addAll(new Map([["a", 1], ["b", 2]]), true);
  21. * map.addAll(new Map([["c", 3], ["d", 4]]), true);
  22. * map.get("a"); // 1
  23. * map.get("d"); // 4
  24. * for (const [key, value] of map) {
  25. * console.log(key, value);
  26. * }
  27. * ```
  28. * @template K
  29. * @template V
  30. */
  31. class StackedCacheMap {
  32. /**
  33. * Initializes the mutable fallback map and the stack of immutable cache
  34. * layers.
  35. */
  36. constructor() {
  37. /** @type {Map<K, V>} */
  38. this.map = new Map();
  39. /** @type {ReadonlyMap<K, V>[]} */
  40. this.stack = [];
  41. }
  42. /**
  43. * Adds another cache layer. Immutable maps are retained by reference and
  44. * reordered so larger layers are checked first, while mutable maps are
  45. * copied into the fallback map.
  46. * @param {ReadonlyMap<K, V>} map map to add
  47. * @param {boolean=} immutable if 'map' is immutable and StackedCacheMap can keep referencing it
  48. */
  49. addAll(map, immutable) {
  50. if (immutable) {
  51. this.stack.push(map);
  52. // largest map should go first
  53. for (let i = this.stack.length - 1; i > 0; i--) {
  54. const beforeLast = this.stack[i - 1];
  55. if (beforeLast.size >= map.size) break;
  56. this.stack[i] = beforeLast;
  57. this.stack[i - 1] = map;
  58. }
  59. } else {
  60. for (const [key, value] of map) {
  61. this.map.set(key, value);
  62. }
  63. }
  64. }
  65. /**
  66. * Stores or overrides a value in the mutable fallback map.
  67. * @param {K} item the key of the element to add
  68. * @param {V} value the value of the element to add
  69. * @returns {void}
  70. */
  71. set(item, value) {
  72. this.map.set(item, value);
  73. }
  74. /**
  75. * Rejects deletions because this data structure is optimized for append-only
  76. * cache layers.
  77. * @param {K} item the item to delete
  78. * @returns {void}
  79. */
  80. delete(item) {
  81. throw new Error("Items can't be deleted from a StackedCacheMap");
  82. }
  83. /**
  84. * Rejects `has` checks because they would force the same layered lookup work
  85. * as `get` without returning the cached value.
  86. * @param {K} item the item to test
  87. * @returns {boolean} true if the item exists in this set
  88. */
  89. has(item) {
  90. throw new Error(
  91. "Checking StackedCacheMap.has before reading is inefficient, use StackedCacheMap.get and check for undefined"
  92. );
  93. }
  94. /**
  95. * Looks up a key by scanning immutable cache layers first and then the
  96. * mutable fallback map.
  97. * @param {K} item the key of the element to return
  98. * @returns {V | undefined} the value of the element
  99. */
  100. get(item) {
  101. for (const map of this.stack) {
  102. const value = map.get(item);
  103. if (value !== undefined) return value;
  104. }
  105. return this.map.get(item);
  106. }
  107. /**
  108. * Removes every cache layer and clears the mutable fallback map.
  109. */
  110. clear() {
  111. this.stack.length = 0;
  112. this.map.clear();
  113. }
  114. /**
  115. * Returns the total number of entries across the fallback map and all stacked
  116. * cache layers.
  117. * @returns {number} size of the map
  118. */
  119. get size() {
  120. let size = this.map.size;
  121. for (const map of this.stack) {
  122. size += map.size;
  123. }
  124. return size;
  125. }
  126. /**
  127. * Iterates over the fallback map first and then each stacked cache layer.
  128. * @returns {Iterator<[K, V]>} iterator
  129. */
  130. [Symbol.iterator]() {
  131. const iterators = this.stack.map((map) => map[Symbol.iterator]());
  132. let current = this.map[Symbol.iterator]();
  133. return {
  134. next() {
  135. let result = current.next();
  136. while (result.done && iterators.length > 0) {
  137. current = /** @type {MapIterator<[K, V]>} */ (iterators.pop());
  138. result = current.next();
  139. }
  140. return result;
  141. }
  142. };
  143. }
  144. }
  145. module.exports = StackedCacheMap;