UNPKG

next

Version:

The React Framework

310 lines (308 loc) • 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { Fallback: null, createCacheMap: null, deleteFromCacheMap: null, getFromCacheMap: null, isValueExpired: null, setInCacheMap: null, setSizeInCacheMap: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { Fallback: function() { return Fallback; }, createCacheMap: function() { return createCacheMap; }, deleteFromCacheMap: function() { return deleteFromCacheMap; }, getFromCacheMap: function() { return getFromCacheMap; }, isValueExpired: function() { return isValueExpired; }, setInCacheMap: function() { return setInCacheMap; }, setSizeInCacheMap: function() { return setSizeInCacheMap; } }); const _lru = require("./lru"); const Fallback = {}; // This is a special internal key that is used for "revalidation" entries. It's // an implementation detail that shouldn't leak outside of this module. const Revalidation = {}; function createCacheMap() { const cacheMap = { parent: null, key: null, value: null, map: null, // LRU-related fields prev: null, next: null, size: 0 }; return cacheMap; } function getOrInitialize(cacheMap, keys, isRevalidation) { // Go through each level of keys until we find the entry that matches, or // create a new entry if one doesn't exist. // // This function will only return entries that match the keypath _exactly_. // Unlike getWithFallback, it will not access fallback entries unless it's // explicitly part of the keypath. let entry = cacheMap; let remainingKeys = keys; let key = null; while(true){ const previousKey = key; if (remainingKeys !== null) { key = remainingKeys.value; remainingKeys = remainingKeys.parent; } else if (isRevalidation && previousKey !== Revalidation) { // During a revalidation, we append an internal "Revalidation" key to // the end of the keypath. The "normal" entry is its parent. // However, if the parent entry is currently empty, we don't need to store // this as a revalidation entry. Just insert the revalidation into the // normal slot. if (entry.value === null) { return entry; } // Otheriwse, create a child entry. key = Revalidation; } else { break; } let map = entry.map; if (map !== null) { const existingEntry = map.get(key); if (existingEntry !== undefined) { // Found a match. Keep going. entry = existingEntry; continue; } } else { map = new Map(); entry.map = map; } // No entry exists yet at this level. Create a new one. const newEntry = { parent: entry, key, value: null, map: null, // LRU-related fields prev: null, next: null, size: 0 }; map.set(key, newEntry); entry = newEntry; } return entry; } function getFromCacheMap(now, currentCacheVersion, rootEntry, keys, isRevalidation) { const entry = getEntryWithFallbackImpl(now, currentCacheVersion, rootEntry, keys, isRevalidation, 0); if (entry === null || entry.value === null) { return null; } // This is an LRU access. Move the entry to the front of the list. (0, _lru.lruPut)(entry); return entry.value; } function isValueExpired(now, currentCacheVersion, value) { return value.staleAt <= now || value.version < currentCacheVersion; } function lazilyEvictIfNeeded(now, currentCacheVersion, entry) { // We have a matching entry, but before we can return it, we need to check if // it's still fresh. Otherwise it should be treated the same as a cache miss. if (entry.value === null) { // This entry has no value, so there's nothing to evict. return entry; } const value = entry.value; if (isValueExpired(now, currentCacheVersion, value)) { // The value expired. Lazily evict it from the cache, and return null. This // is conceptually the same as a cache miss. deleteMapEntry(entry); return null; } // The matched entry has not expired. Return it. return entry; } function getEntryWithFallbackImpl(now, currentCacheVersion, entry, keys, isRevalidation, previousKey) { // This is similar to getExactEntry, but if an exact match is not found for // a key, it will return the fallback entry instead. This is recursive at // every level, e.g. an entry with keypath [a, Fallback, c, Fallback] is // valid match for [a, b, c, d]. // // It will return the most specific match available. let key; let remainingKeys; if (keys !== null) { key = keys.value; remainingKeys = keys.parent; } else if (isRevalidation && previousKey !== Revalidation) { // During a revalidation, we append an internal "Revalidation" key to // the end of the keypath. key = Revalidation; remainingKeys = null; } else { // There are no more keys. This is the terminal entry. // TODO: When performing a lookup during a navigation, as opposed to a // prefetch, we may want to skip entries that are Pending if there's also // a Fulfilled fallback entry. Tricky to say, though, since if it's // already pending, it's likely to stream in soon. Maybe we could do this // just on slow connections and offline mode. return lazilyEvictIfNeeded(now, currentCacheVersion, entry); } const map = entry.map; if (map !== null) { const existingEntry = map.get(key); if (existingEntry !== undefined) { // Found an exact match for this key. Keep searching. const result = getEntryWithFallbackImpl(now, currentCacheVersion, existingEntry, remainingKeys, isRevalidation, key); if (result !== null) { return result; } } // No match found for this key. Check if there's a fallback. const fallbackEntry = map.get(Fallback); if (fallbackEntry !== undefined) { // Found a fallback for this key. Keep searching. return getEntryWithFallbackImpl(now, currentCacheVersion, fallbackEntry, remainingKeys, isRevalidation, key); } } return null; } function setInCacheMap(cacheMap, keys, value, isRevalidation) { // Add a value to the map at the given keypath. If the value is already // part of the map, it's removed from its previous keypath. (NOTE: This is // unlike a regular JS map, but the behavior is intentional.) const entry = getOrInitialize(cacheMap, keys, isRevalidation); setMapEntryValue(entry, value); // This is an LRU access. Move the entry to the front of the list. (0, _lru.lruPut)(entry); (0, _lru.updateLruSize)(entry, value.size); } function setMapEntryValue(entry, value) { if (entry.value !== null) { // There's already a value at the given keypath. Disconnect the old value // from the map. We're not calling `deleteMapEntry` here because the // entry itself is still in the map. We just want to overwrite its value. dropRef(entry.value); // Fill the entry with the updated value. const emptyEntry = entry; emptyEntry.value = null; fillEmptyReference(emptyEntry, value); } else { fillEmptyReference(entry, value); } } function fillEmptyReference(entry, value) { // This value may already be in the map at a different keypath. // Grab a reference before we overwrite it. const oldEntry = value.ref; const fullEntry = entry; fullEntry.value = value; value.ref = fullEntry; (0, _lru.updateLruSize)(fullEntry, value.size); if (oldEntry !== null && oldEntry !== entry && oldEntry.value === value) { // This value is already in the map at a different keypath in the map. // Values only exist at a single keypath at a time. Remove it from the // previous keypath. // // Note that only the internal map entry is garbage collected; we don't // call `dropRef` here because it's still in the map, just // at a new keypath (the one we just set, above). deleteMapEntry(oldEntry); } } function deleteFromCacheMap(value) { const entry = value.ref; if (entry === null) { // This value is not a member of any map. return; } dropRef(value); deleteMapEntry(entry); } function dropRef(value) { // Drop the value from the map by setting its `ref` backpointer to // null. This is a separate operation from `deleteMapEntry` because when // re-keying a value we need to be able to delete the old, internal map // entry without garbage collecting the value itself. value.ref = null; } function deleteMapEntry(entry) { // Delete the entry from the cache. const emptyEntry = entry; emptyEntry.value = null; (0, _lru.deleteFromLru)(entry); // Check if we can garbage collect the entry. const map = emptyEntry.map; if (map === null) { // Since this entry has no value, and also no child entries, we can // garbage collect it. Remove it from its parent, and keep garbage // collecting the parents until we reach a non-empty entry. let parent = emptyEntry.parent; let key = emptyEntry.key; while(parent !== null){ const parentMap = parent.map; if (parentMap !== null) { parentMap.delete(key); if (parentMap.size === 0) { // We just removed the last entry in the parent map. parent.map = null; if (parent.value === null) { // The parent node has no child entries, nor does it have a value // on itself. It can be garbage collected. Keep going. key = parent.key; parent = parent.parent; continue; } } } break; } } else { // Check if there's a revalidating entry. If so, promote it to a // "normal" entry, since the normal one was just deleted. const revalidatingEntry = map.get(Revalidation); if (revalidatingEntry !== undefined && revalidatingEntry.value !== null) { setMapEntryValue(emptyEntry, revalidatingEntry.value); } } } function setSizeInCacheMap(value, size) { const entry = value.ref; if (entry === null) { // This value is not a member of any map. return; } // Except during initialization (when the size is set to 0), this is the only // place the `size` field should be updated, to ensure it's in sync with the // the LRU. value.size = size; (0, _lru.updateLruSize)(entry, size); } if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') { Object.defineProperty(exports.default, '__esModule', { value: true }); Object.assign(exports.default, exports); module.exports = exports.default; } //# sourceMappingURL=cache-map.js.map