index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. export default class QuickLRU extends Map {
  2. constructor(options = {}) {
  3. super();
  4. if (!(options.maxSize && options.maxSize > 0)) {
  5. throw new TypeError('`maxSize` must be a number greater than 0');
  6. }
  7. if (typeof options.maxAge === 'number' && options.maxAge === 0) {
  8. throw new TypeError('`maxAge` must be a number greater than 0');
  9. }
  10. // TODO: Use private class fields when ESLint supports them.
  11. this.maxSize = options.maxSize;
  12. this.maxAge = options.maxAge || Number.POSITIVE_INFINITY;
  13. this.onEviction = options.onEviction;
  14. this.cache = new Map();
  15. this.oldCache = new Map();
  16. this._size = 0;
  17. }
  18. // TODO: Use private class methods when targeting Node.js 16.
  19. _emitEvictions(cache) {
  20. if (typeof this.onEviction !== 'function') {
  21. return;
  22. }
  23. for (const [key, item] of cache) {
  24. this.onEviction(key, item.value);
  25. }
  26. }
  27. _deleteIfExpired(key, item) {
  28. if (typeof item.expiry === 'number' && item.expiry <= Date.now()) {
  29. if (typeof this.onEviction === 'function') {
  30. this.onEviction(key, item.value);
  31. }
  32. return this.delete(key);
  33. }
  34. return false;
  35. }
  36. _getOrDeleteIfExpired(key, item) {
  37. const deleted = this._deleteIfExpired(key, item);
  38. if (deleted === false) {
  39. return item.value;
  40. }
  41. }
  42. _getItemValue(key, item) {
  43. return item.expiry ? this._getOrDeleteIfExpired(key, item) : item.value;
  44. }
  45. _peek(key, cache) {
  46. const item = cache.get(key);
  47. return this._getItemValue(key, item);
  48. }
  49. _set(key, value) {
  50. this.cache.set(key, value);
  51. this._size++;
  52. if (this._size >= this.maxSize) {
  53. this._size = 0;
  54. this._emitEvictions(this.oldCache);
  55. this.oldCache = this.cache;
  56. this.cache = new Map();
  57. }
  58. }
  59. _moveToRecent(key, item) {
  60. this.oldCache.delete(key);
  61. this._set(key, item);
  62. }
  63. * _entriesAscending() {
  64. for (const item of this.oldCache) {
  65. const [key, value] = item;
  66. if (!this.cache.has(key)) {
  67. const deleted = this._deleteIfExpired(key, value);
  68. if (deleted === false) {
  69. yield item;
  70. }
  71. }
  72. }
  73. for (const item of this.cache) {
  74. const [key, value] = item;
  75. const deleted = this._deleteIfExpired(key, value);
  76. if (deleted === false) {
  77. yield item;
  78. }
  79. }
  80. }
  81. get(key) {
  82. if (this.cache.has(key)) {
  83. const item = this.cache.get(key);
  84. return this._getItemValue(key, item);
  85. }
  86. if (this.oldCache.has(key)) {
  87. const item = this.oldCache.get(key);
  88. if (this._deleteIfExpired(key, item) === false) {
  89. this._moveToRecent(key, item);
  90. return item.value;
  91. }
  92. }
  93. }
  94. set(key, value, {maxAge = this.maxAge} = {}) {
  95. const expiry =
  96. typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY ?
  97. Date.now() + maxAge :
  98. undefined;
  99. if (this.cache.has(key)) {
  100. this.cache.set(key, {
  101. value,
  102. expiry
  103. });
  104. } else {
  105. this._set(key, {value, expiry});
  106. }
  107. }
  108. has(key) {
  109. if (this.cache.has(key)) {
  110. return !this._deleteIfExpired(key, this.cache.get(key));
  111. }
  112. if (this.oldCache.has(key)) {
  113. return !this._deleteIfExpired(key, this.oldCache.get(key));
  114. }
  115. return false;
  116. }
  117. peek(key) {
  118. if (this.cache.has(key)) {
  119. return this._peek(key, this.cache);
  120. }
  121. if (this.oldCache.has(key)) {
  122. return this._peek(key, this.oldCache);
  123. }
  124. }
  125. delete(key) {
  126. const deleted = this.cache.delete(key);
  127. if (deleted) {
  128. this._size--;
  129. }
  130. return this.oldCache.delete(key) || deleted;
  131. }
  132. clear() {
  133. this.cache.clear();
  134. this.oldCache.clear();
  135. this._size = 0;
  136. }
  137. resize(newSize) {
  138. if (!(newSize && newSize > 0)) {
  139. throw new TypeError('`maxSize` must be a number greater than 0');
  140. }
  141. const items = [...this._entriesAscending()];
  142. const removeCount = items.length - newSize;
  143. if (removeCount < 0) {
  144. this.cache = new Map(items);
  145. this.oldCache = new Map();
  146. this._size = items.length;
  147. } else {
  148. if (removeCount > 0) {
  149. this._emitEvictions(items.slice(0, removeCount));
  150. }
  151. this.oldCache = new Map(items.slice(removeCount));
  152. this.cache = new Map();
  153. this._size = 0;
  154. }
  155. this.maxSize = newSize;
  156. }
  157. * keys() {
  158. for (const [key] of this) {
  159. yield key;
  160. }
  161. }
  162. * values() {
  163. for (const [, value] of this) {
  164. yield value;
  165. }
  166. }
  167. * [Symbol.iterator]() {
  168. for (const item of this.cache) {
  169. const [key, value] = item;
  170. const deleted = this._deleteIfExpired(key, value);
  171. if (deleted === false) {
  172. yield [key, value.value];
  173. }
  174. }
  175. for (const item of this.oldCache) {
  176. const [key, value] = item;
  177. if (!this.cache.has(key)) {
  178. const deleted = this._deleteIfExpired(key, value);
  179. if (deleted === false) {
  180. yield [key, value.value];
  181. }
  182. }
  183. }
  184. }
  185. * entriesDescending() {
  186. let items = [...this.cache];
  187. for (let i = items.length - 1; i >= 0; --i) {
  188. const item = items[i];
  189. const [key, value] = item;
  190. const deleted = this._deleteIfExpired(key, value);
  191. if (deleted === false) {
  192. yield [key, value.value];
  193. }
  194. }
  195. items = [...this.oldCache];
  196. for (let i = items.length - 1; i >= 0; --i) {
  197. const item = items[i];
  198. const [key, value] = item;
  199. if (!this.cache.has(key)) {
  200. const deleted = this._deleteIfExpired(key, value);
  201. if (deleted === false) {
  202. yield [key, value.value];
  203. }
  204. }
  205. }
  206. }
  207. * entriesAscending() {
  208. for (const [key, value] of this._entriesAscending()) {
  209. yield [key, value.value];
  210. }
  211. }
  212. get size() {
  213. if (!this._size) {
  214. return this.oldCache.size;
  215. }
  216. let oldCacheSize = 0;
  217. for (const key of this.oldCache.keys()) {
  218. if (!this.cache.has(key)) {
  219. oldCacheSize++;
  220. }
  221. }
  222. return Math.min(this._size + oldCacheSize, this.maxSize);
  223. }
  224. entries() {
  225. return this.entriesAscending();
  226. }
  227. forEach(callbackFunction, thisArgument = this) {
  228. for (const [key, value] of this.entriesAscending()) {
  229. callbackFunction.call(thisArgument, value, key, this);
  230. }
  231. }
  232. get [Symbol.toStringTag]() {
  233. return JSON.stringify([...this.entriesAscending()]);
  234. }
  235. }