cache.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. export default function initCache({
  2. lifetime: defaultLifetime = 3000,
  3. onDispose,
  4. } = {}) {
  5. let cache = Object.create(null);
  6. // setTimeout call is very expensive when done frequently,
  7. // 1000 calls performed for 50 scripts consume 50ms on each tab load,
  8. // so we'll schedule trim() just once per event loop cycle,
  9. // and then trim() will trim the cache and reschedule itself to the earliest expiry time.
  10. let timer;
  11. let minLifetime = -1;
  12. // same goes for the performance.now() used by hit() and put() which is why we expose batch(true)
  13. // to start an operation that reuses the same value of now(), and batch(false) to end it
  14. let batchStarted;
  15. let batchStartTime;
  16. // eslint-disable-next-line no-return-assign
  17. const getNow = () => batchStarted && batchStartTime || (batchStartTime = performance.now());
  18. /** @namespace VMCache */
  19. const exports = {
  20. batch, get, getValues, pop, put, del, has, hit, destroy,
  21. };
  22. if (process.env.DEV) Object.defineProperty(exports, 'data', { get: () => cache });
  23. return exports;
  24. function batch(enable) {
  25. batchStarted = enable;
  26. batchStartTime = 0;
  27. }
  28. function get(key, def, shouldHit = true) {
  29. const item = cache[key];
  30. if (item && shouldHit) {
  31. reschedule(item, item.lifetime);
  32. }
  33. return item ? item.value : def;
  34. }
  35. function getValues() {
  36. return Object.values(cache).map(item => item.value);
  37. }
  38. function pop(key, def) {
  39. const value = get(key, def);
  40. del(key);
  41. return value;
  42. }
  43. function put(key, value, lifetime) {
  44. reschedule(cache[key] = lifetime ? { value, lifetime } : { value }, lifetime);
  45. return value;
  46. }
  47. function del(key) {
  48. const data = cache[key];
  49. if (data) {
  50. delete cache[key];
  51. onDispose?.(data.value, key);
  52. }
  53. }
  54. function has(key) {
  55. return cache[key];
  56. }
  57. function hit(key, lifetime) {
  58. const entry = cache[key];
  59. if (entry) {
  60. reschedule(entry, lifetime);
  61. }
  62. }
  63. function destroy() {
  64. // delete all keys to make sure onDispose is called for each value
  65. if (onDispose) {
  66. // cache inherits null so we don't need to check hasOwnProperty
  67. // eslint-disable-next-line guard-for-in
  68. for (const key in cache) {
  69. del(key);
  70. }
  71. } else {
  72. cache = Object.create(null);
  73. }
  74. clearTimeout(timer);
  75. timer = 0;
  76. }
  77. function reschedule(entry, lifetime = defaultLifetime) {
  78. entry.expiry = lifetime + getNow();
  79. if (timer) {
  80. if (lifetime >= minLifetime) return;
  81. clearTimeout(timer);
  82. }
  83. minLifetime = lifetime;
  84. timer = setTimeout(trim, lifetime);
  85. }
  86. function trim() {
  87. // next timer won't be able to run earlier than 10ms
  88. // so we'll sweep the upcoming expired entries in this run
  89. const now = performance.now() + 10;
  90. let closestExpiry = Number.MAX_SAFE_INTEGER;
  91. // eslint-disable-next-line guard-for-in
  92. for (const key in cache) {
  93. const { expiry } = cache[key];
  94. if (expiry < now) {
  95. del(key);
  96. } else if (expiry < closestExpiry) {
  97. closestExpiry = expiry;
  98. }
  99. }
  100. minLifetime = closestExpiry - now;
  101. timer = closestExpiry < Number.MAX_SAFE_INTEGER
  102. ? setTimeout(trim, minLifetime)
  103. : 0;
  104. }
  105. }