/**
* Caches loaded entries and keeps track of which entries that need to be updated (refreshed).
* The cache also provides a listener functionality that allows you to be notified of
* when entries are updated.
*
* @exports store/Cache
*/
export default class Cache {
constructor() {
/**
* @type {Map<string, Function>}
* @private
*/
this._listenersIdx = new Map();
/**
* @type {Map<string, Entry>}
* @private
*/
this._cacheIdx = new Map();
/**
* @type {Map<string, Set<Entry>>}
* @private
*/
this._cacheIdxResource = new Map();
/**
* @type {Map<string, object>}
* @private
*/
this._cacheCtrl = new Map();
this._listenerCounter = 0;
this._cacheLoadPromise = {};
}
/**
* Add or update the entry to the cache.
* All listeners will be notified unless silently is specified.
*
* @param {Entry} entry
* @param {Boolean=} silently - listeners will be notified unless true is specified.
*/
cache(entry, silently) {
const entryURI = entry.getURI();
const previouslyCached = this._cacheIdx.has(entryURI);
this._cacheIdx.set(entryURI, entry);
const entryRURI = entry.getResourceURI();
const entriesSet = this._cacheIdxResource.has(entryRURI) ? this._cacheIdxResource.get(entryRURI) : new Set();
if (!entriesSet.has(entry)) {
entriesSet.add(entry);
}
this._cacheIdxResource.set(entryRURI, entriesSet);
this._cacheCtrl.set(entryURI, {
date: new Date().getTime(),
});
if (previouslyCached && silently !== true) {
this.messageListeners('refreshed', entry);
}
}
/**
* Removes a single entry from the cache.
* @param {Entry} entry the entry to remove.
*/
unCache(entry) {
const entryURI = entry.getURI();
const entryRURI = entry.getResourceURI();
this._cacheIdx.delete(entryURI);
const entriesSet = this._cacheIdxResource.get(entryRURI);
if (entriesSet.size > 0) {
entriesSet.delete(entry);
if (entriesSet.size === 0) {
this._cacheIdxResource.delete(entryRURI);
}
}
}
/**
* Marks an entry as in need of refresh from the store.
* All listeners are notified of the entry now being in need of refreshing unless
* silently is set to true.
*
* @param {Entry} entry
* @param {Boolean=} silently
*/
setRefreshNeeded(entry, silently) {
const entryURI = entry.getURI();
const ctrl = this._cacheCtrl.get(entryURI);
if (ctrl == null) {
throw new Error(`No cache control of existing entry: ${entryURI}`);
}
ctrl.stale = true;
if (silently !== true) {
this.messageListeners('needRefresh', entry);
}
}
/**
* A convenience method for caching multiple entries.
*
* @param {Entry[]} entryArr
* @param {Boolean=} silently
* @see Cache#cache
*/
cacheAll(entryArr, silently) {
entryArr.forEach((entry) => {
this.cache(entry, silently);
});
}
/**
* Retrieve the entry from it's URI.
*
* @param {String} entryURI
* @returns {Entry|undefined}
*/
get(entryURI) {
return this._cacheIdx.get(entryURI);
}
/**
* Retrieve all entries that have the specified uri as resource.
* Note that since several entries (e.g. links) may have the same uri
* as resource this method returns an array. However, in many situations
* there will be zero or one entry per uri.
*
* @param {String} uri
* @returns {Set<Entry>} always returns a set, may be empty though.
*/
getByResourceURI(uri) {
return new Set(this._cacheIdxResource.get(uri));
}
/**
* Retrieve a load promise.
*
* @param {String} loadID
* @returns {Promise|undefined}
*/
getPromise(loadID) {
return this._cacheLoadPromise[loadID];
}
/**
* Store the promise for loading something.
*
* @param {String} loadID
* @param {Promise} loadPromise
*/
addPromise(loadID, loadPromise) {
this._cacheLoadPromise[loadID] = loadPromise;
this._cacheLoadPromise[loadID].catch(() => {});
}
/**
* Remove the promise responsible for loading something.
*
* @param {String} loadID
*/
removePromise(loadID) {
delete this._cacheLoadPromise[loadID];
}
/**
* Tells whether the entry is in need of a refresh from the repository.
*
* @param {Entry} entry
* @returns {boolean}
*/
needRefresh(entry) {
const entryURI = entry.getURI();
const ctrl = this._cacheCtrl.get(entryURI);
if (ctrl == null) {
throw Error(`No cache control of existing entry: ${entryURI}`);
}
return ctrl.stale === true;
}
/**
* @param {Function} listener
*/
addCacheUpdateListener(listener) {
if (listener.__clid != null) {
listener.__clid = `idx_${this._listenerCounter}`;
this._listenerCounter += 1;
}
this._listenersIdx.set(listener.__clid, listener);
}
/**
* @param {Function} listener
*/
removeCacheUpdateListener(listener) {
if (listener.__clid != null) {
this._listenersIdx.delete(listener.__clid);
}
}
/**
* Agreed topics are:
* allEntriesNeedRefresh - all entries are now in need of refresh,
* typically happens after a change of user(sign in)
* needRefresh - the specified entry need to be refreshed.
* refreshed - the specified entry have been refreshed.
*
* @param {String} topic
* @param {Entry=} affectedEntry
*/
messageListeners(topic, affectedEntry) {
this._listenersIdx.forEach((func) => {
func(topic, affectedEntry);
});
}
/**
* Marks all entries as in need of refresh and consequently messages all listeners
* with the allEntriesNeedRefresh topic.
*/
allNeedRefresh() {
this._cacheIdx.forEach((entry, uri) => {
// Do not messageListeners for every entry.
this.setRefreshNeeded(this._cacheIdx.get(uri), true);
}, this);
this.messageListeners('allEntriesNeedRefresh');
}
/**
* Clears the cache from all cached entries.
* Warning: all references to entries needs to be discarded as they will not be
* kept in sync with changes.
*/
clear() {
this._cacheIdx = new Map();
this._cacheIdxResource = new Map();
this._cacheCtrl = new Map();
}
}