.
This commit is contained in:
+305
@@ -0,0 +1,305 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
0 && (module.exports = {
|
||||
Fallback: null,
|
||||
createCacheMap: null,
|
||||
deleteFromCacheMap: null,
|
||||
deleteMapEntry: 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;
|
||||
},
|
||||
deleteMapEntry: function() {
|
||||
return deleteMapEntry;
|
||||
},
|
||||
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);
|
||||
entry.value = null;
|
||||
}
|
||||
// This value may already be in the map at a different keypath.
|
||||
// Grab a reference before we overwrite it.
|
||||
const oldEntry = value.ref;
|
||||
entry.value = value;
|
||||
value.ref = entry;
|
||||
(0, _lru.updateLruSize)(entry, 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.
|
||||
entry.value = null;
|
||||
(0, _lru.deleteFromLru)(entry);
|
||||
// Check if we can garbage collect the entry.
|
||||
const map = entry.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 = entry.parent;
|
||||
let key = entry.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(entry, 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
|
||||
Reference in New Issue
Block a user