@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,000 lines (852 loc) • 35.9 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define(["sap/base/config", "sap/base/Log", "sap/ui/performance/Measurement", "sap/ui/core/Core"],
function(BaseConfig, Log, Measurement, Core) {
"use strict";
/**
* @classdesc
* This object provides cache functionality with persistence in IndexedDB.
* The component is private and restricted to the factory class sap.ui.core.cache.CacheManager.
* Do not use outside UI5 framework itself.
* This implementation works with entries corresponding to a single ui5 version.
* If the cache is loaded with different ui5 version, all previous entries will be deleted. The latter behavior is about of a further changes (feature requests)
*
* Do not use it directly, use {@link sap.ui.core.cache.CacheManager} instead
* @private
* @ui5-restricted sap.ui.core.cache.CacheManager
* @since 1.40.0
* @namespace
* @alias sap.ui.core.cache.LRUPersistentCache
*/
var LRUPersistentCache = {
name: "LRUPersistentCache",
logResolved: function(sFnName) {
Log.debug("Cache Manager: " + sFnName + " completed successfully.");
},
defaultOptions: {
databaseName: "ui5-cachemanager-db",
_contentStoreName: "content-store",
_metadataStoreName: "metadata-store",
_metadataKey: "metadataKey"
},
_db: {},
init: function () {
this._metadata = {};
/**
* The mru index whose value is always assigned to the last added item
*/
this._mru = -1;
/**
* Least recently used. Index of the item that is used less and is the next potential item that will be deleted
*/
this._lru = -1;
return initIndexedDB(this);
},
/*
* Retrieves the version property defined on the Core.
* Can be stubbed in the QUnit test to fix a certain version expectation.
* @private
*/
_getVersion() {
return Core.version;
},
_destroy: function () {
if (this._db.close) {
this._db.close();
}
this._metadata = null;
this._ui5version = null;
},
set: function (key, value) {
if (keyMatchesExclusionStrings(key)) {
Log.warning("Cache Manager ignored 'set' for key [" + key + "]");
return Promise.resolve();
}
if (key == null) { //undefined or null
return Promise.reject("Cache Manager does not accept undefined or null as key");
}
if (typeof value === "undefined") {
return Promise.reject("Cache Manager does not accept undefined as value");
}
Log.debug("Cache Manager LRUPersistentCache: adding item with key [" + key + "]...");
var self = this,
sMsrTotal = "[sync ] fnSet: total[sync] key [" + key + "]",
sMsrOpeningTx = "[sync ] fnSet: txStart[sync] key [" + key + "]",
sMsrOpeningStores = "[sync ] fnSet: storeOpen[sync] key [" + key + "]",
sMsrPutContent = "[sync ] fnSet: putContent[sync] key [" + key + "]",
sMsrPutMetadata = "[sync ] fnSet: putMetadata[sync] key [" + key + "]",
sMsrSerialize = "[sync ] fnSet: serialize[sync] key [" + key + "]";
return new Promise(function fnSet(resolve, reject) {
Measurement.start(sMsrTotal, "CM", sMsrCatSet);
var objectStore, objectStoreRequest,
objectMetadataStore,
oItem, backupMetadata;
backupMetadata = cloneMetadata(self._metadata);
oItem = new Item(key, value, typeof value, ++self._mru, sMsrSerialize, sMsrCatSet).serialize();
Measurement.start(sMsrOpeningTx, "CM", sMsrCatSet);
var transaction = self._db.transaction([self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName], "readwrite");
Measurement.end(sMsrOpeningTx);
transaction.onerror = function (event) {
var sMessage = "Cache Manager cannot complete add/put transaction for entry with key: " + oItem.oData.key + ". Details: " + collectErrorData(event);
Log.error(sMessage);
self._metadata = backupMetadata;
assignRUCounters(self);
reject(sMessage);
};
transaction.onabort = function (event) {
self._metadata = backupMetadata;
assignRUCounters(self);
var iOriginalItemCount = getItemCount(self);
if (isQuotaExceeded(event) && iOriginalItemCount > 0) {
Log.warning("Cache Manager is trying to free some space to add/put new item");
cleanAndStore(self, key, value).then(
function () {
Log.debug("Cache Manager LRUPersistentCache: set completed after freeing space. ItemCount changed from " +
iOriginalItemCount + " to " + getItemCount(self));
resolve();
},
function (sMessage) {
var sMsg = "Cache Manager LRUPersistentCache: set unsuccessful. Cannot free space to add/put entry. Details: " + sMessage;
Log.error(sMsg);
reject(sMsg);
});
} else {
var sErrMsg = "Cache Manager LRUPersistentCache: set failed: " + collectErrorData(event);
Log.error(sErrMsg);
reject(sErrMsg);
}
};
transaction.oncomplete = function () {
Log.debug("Cache Manager LRUPersistentCache: adding item with key [" + key + "]... done");
resolve();
};
Measurement.start(sMsrOpeningStores, "CM", sMsrCatSet);
objectStore = transaction.objectStore(self.defaultOptions._contentStoreName);
objectMetadataStore = transaction.objectStore(self.defaultOptions._metadataStoreName);
Measurement.end(sMsrOpeningStores);
Measurement.start(sMsrPutContent, "CM", sMsrCatSet);
objectStoreRequest = objectStore.put(oItem.oData, oItem.oData.key);
Measurement.end(sMsrPutContent);
Measurement.end(sMsrTotal);
objectStoreRequest.onsuccess = function () {
updateItemUsage(self, oItem);
Measurement.start(sMsrPutMetadata, "CM", sMsrCatSet);
objectMetadataStore.put(self._metadata, self.defaultOptions._metadataKey);
Measurement.end(sMsrPutMetadata);
};
if (Log.getLevel() >= Log.Level.DEBUG) {
Log.debug("Cache Manager LRUPersistentCache: measurements: "
+ sMsrTotal + ": " + Measurement.getMeasurement(sMsrTotal).duration
+ "; " + sMsrSerialize + ": " + Measurement.getMeasurement(sMsrSerialize).duration
+ "; " + sMsrOpeningTx + ": " + Measurement.getMeasurement(sMsrOpeningTx).duration
+ "; " + sMsrOpeningStores + ": " + Measurement.getMeasurement(sMsrOpeningStores).duration
+ "; " + sMsrPutContent + ": " + Measurement.getMeasurement(sMsrPutContent).duration
+ "; " + sMsrPutMetadata + ": " + Measurement.getMeasurement(sMsrPutMetadata).duration
);
}
});
},
has: function (key) {
if (keyMatchesExclusionStrings(key)) {
Log.warning("Cache Manager ignored 'has' for key [" + key + "]");
return Promise.resolve(false);
}
return this.get(key).then(function(value) {
var result = typeof value !== "undefined";
Log.debug("Cache Manager: has key [" + key + "] returned " + result);
return result;
});
},
/**
* Returns the current item count.
* @returns {Promise} a resolved Promise with value corresponding to the item count.
* @private
*/
_getCount: function () {
return Promise.resolve(getItemCount(this));
},
/**
* Retrieves all items.
* @param {boolean} bDeserialize whether to deserialize the content or not
* @returns {Promise} a promise that would be resolved in case of successful operation or rejected with
* value of the error message if the operation fails. When resolved the Promise will return the array of all
* entries in the following format: <code>{key: <myKey>, value: <myValue>}</code>
* @private
*/
_getAll: function (bDeserialize) {
var self = this, oItem,
sMsrDeserialize = "[sync ] _getAll: deserialize";
return new Promise(function (resolve, reject) {
var entries = [],
transaction = self._db.transaction([self.defaultOptions._contentStoreName], "readonly"),
objectStore = transaction.objectStore(self.defaultOptions._contentStoreName);
transaction.onerror = function (event) {
reject(collectErrorData(event));
};
transaction.oncomplete = function (event) {
resolve(entries);
};
objectStore.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor && cursor.value) { //cursor.value is ItemData
oItem = new Item(cursor.value, sMsrDeserialize, sMsrCatGet).deserialize();
entries.push({
key: oItem.oData.key,
value: oItem.oData.value
});
cursor.continue();
}
};
});
},
_loadMetaStructure: function () {
var self = this;
return new Promise(function (resolve, reject) {
var transaction = self._db.transaction([self.defaultOptions._metadataStoreName], "readonly");
transaction.onerror = function (event) {
if (!transaction.errorHandled) {
transaction.errorHandled = true;
var sMessage = "Cache Manager cannot complete transaction for read metadata. Details: " + transaction.error;
Log.error(sMessage);
reject(sMessage);
}
};
var objectStore = transaction.objectStore(self.defaultOptions._metadataStoreName);
try {
var objectStoreRequest = objectStore.get(self.defaultOptions._metadataKey);
objectStoreRequest.onsuccess = function (event) {
self._metadata = objectStoreRequest.result ? objectStoreRequest.result : initMetadata(self._ui5version);
if (self._metadata.__ui5version !== self._ui5version) {
self.reset().then(resolve, function (e) {
Log.error("Cannot reset the cache. Details:" + e);
transaction.abort();
});
} else {
if (!self._metadata.timestamps) {
self._metadata.timestamps = {};
}
resolve();
}
};
objectStoreRequest.onerror = function (event) {
Log.error("Cache Manager cannot complete transaction for read metadata items. Details: " + event.message);
reject(event.message);
};
} catch (e) {
Log.error("Cache Manager cannot read metadata entries behind key: " + self.defaultOptions._metadataKey + ". Details: " + e.message);
reject(e.message);
}
});
},
get: function (key) {
if (keyMatchesExclusionStrings(key)) {
Log.warning("Cache Manager ignored 'get' for key [" + key + "]");
return Promise.resolve();
}
return get(this, key);
},
del: function (key) {
if (keyMatchesExclusionStrings(key)) {
Log.warning("Cache Manager ignored 'del' for key [" + key + "]");
return Promise.resolve();
}
return del(this, key);
},
delWithFilters: function(filters) {
var self = this,
oFilters = filters || {};
return new Promise(function (resolve, reject) {
var oMetadataBackup = cloneMetadata(self._metadata),
oTransaction = self._db.transaction([ self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName ], "readwrite"),
oContentStore = oTransaction.objectStore(self.defaultOptions._contentStoreName),
oMetadataStore = oTransaction.objectStore(self.defaultOptions._metadataStoreName),
oContentCursor = oContentStore.openCursor(),
sPrefix = oFilters.prefix || "";
function restoreMetadata() {
self._metadata = oMetadataBackup;
assignRUCounters(self);
}
function onTransactionFail(event) {
restoreMetadata();
reject(collectErrorData(event));
}
oTransaction.onerror = onTransactionFail;
oTransaction.onabort = onTransactionFail;
oTransaction.oncomplete = function(event) {
resolve();
};
oContentCursor.onsuccess = function(event) {
var oCursor = event.target.result,
sKey,
oRequest;
if (!oCursor) {
oMetadataStore.put(self._metadata, self.defaultOptions._metadataKey);
return;
}
sKey = oCursor.value.key;
if (sKey.indexOf(sPrefix) === 0
&& (!oFilters.olderThan || !(sKey in self._metadata.timestamps)
|| self._metadata.timestamps[sKey] <= oFilters.olderThan)) {
oRequest = oCursor.delete();
oRequest.onsuccess = function() {
Log.debug('Deleted ' + sKey + '!');
deleteMetadataForEntry(self, sKey);
};
}
oCursor.continue();
};
});
},
reset: function () {
var self = this;
return new Promise(function (resolve, reject) {
var genericStore, metadataStore, clearGenericStoreReq, clearMetadataStoreReq, transaction;
transaction = self._db.transaction([self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName], "readwrite");
transaction.onerror = transaction.onabort = function (event) {
if (!transaction.errorHandled) {
transaction.errorHandled = true;
var sMessage = "Cache Manager LRUPersistentCache: transaction for reset() failed. Details: " + transaction.error;
Log.error(sMessage);
reject(sMessage);
}
};
transaction.oncomplete = function (event) {
resolve();
};
genericStore = transaction.objectStore(self.defaultOptions._contentStoreName);
metadataStore = transaction.objectStore(self.defaultOptions._metadataStoreName);
try {
clearGenericStoreReq = genericStore.clear();
clearGenericStoreReq.onerror = function () {
transaction.abort();
};
clearGenericStoreReq.onsuccess = function () {
clearMetadataStoreReq = metadataStore.clear();
clearMetadataStoreReq.onerror = function () {
transaction.abort();
};
clearMetadataStoreReq.onsuccess = function () {
self._metadata = initMetadata(self._getVersion());
assignRUCounters(self);
};
};
} catch (e) {
transaction.abort();
}
});
}
};
var sMsrCatGet = "LRUPersistentCache,get",
sMsrCatSet = "LRUPersistentCache,set",
iMsrCounter = 0;
function scheduleMetadataSave(self, oItem) {//an async store of the metadata , the caller should not be interested in the result, since no reporting status back is supported
var transaction;
self._metadata.timestamps[oItem.oData.key] = Date.now();
//locking both stores as no further modification is required. This will block any further metadata update and sets, but this is the way to keep the metadata consistent
transaction = self._db.transaction([self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName], "readwrite");
transaction.onerror = transaction.onabort = function (event) {
Log.warning("Cache Manager cannot persist the information about usage of an entry. This may lead to earlier removal of the entry if browser storage space is over. Details: " + transaction.error);
};
try {
transaction.objectStore(self.defaultOptions._metadataStoreName).put(self._metadata, self.defaultOptions._metadataKey);
} catch (e) {
Log.warning("Cache Manager cannot persist the information about usage of an entry. This may lead to earlier removal of the entry if browser storage space is over. Details: " + e.message);
}
}
function del(self, key) {
return new Promise(function (resolve, reject) {
var tx, oMetadataBackup;
tx = self._db.transaction([self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName], "readwrite");
oMetadataBackup = cloneMetadata(self._metadata);
function errorHandler(event) {
self._metadata = oMetadataBackup;
assignRUCounters(self);
var sMessage = "Cache Manager LRUPersistentCache: cannot delete item with key: " + key + ". Details: " + collectErrorData(event);
Log.error(sMessage);
reject(sMessage);
}
tx.onerror = errorHandler;
tx.onabort = errorHandler;
tx.oncomplete = function () {
// The cache is empty so its obvious we can reset the counters
if (getItemCount(self) === 0) {
self._lru = -1;
self._mru = -1;
self._metadata = initMetadata(self._ui5version);
}
Log.debug("Cache Manager LRUPersistentCache: item with key " + key + " deleted");
resolve();
};
Log.debug("Cache Manager LRUPersistentCache: deleting item [" + key + "]");
var oDeleteRst = tx.objectStore(self.defaultOptions._contentStoreName).delete(key);
oDeleteRst.onsuccess = function () {
Log.debug("Cache Manager LRUPersistentCache: request for deleting item [" + key + "] is successful, updating metadata...");
deleteMetadataForEntry(self, key);
tx.objectStore(self.defaultOptions._metadataStoreName).put(self._metadata, self.defaultOptions._metadataKey);
};
});
}
function get(self, key) {
if (self.getCounter === undefined) {
self.getCounter = 0;
}
self.getCounter++;
var sMsrTotal = "[sync ] fnGet" + self.getCounter + ": total[sync] key [" + key + "]",
sMsrOpeningTx = "[sync ] fnGet" + self.getCounter + ": txStart[sync] key [" + key + "]",
sMsrOpeningStores = "[sync ] fnGet" + self.getCounter + ": storeOpen[sync] key [" + key + "]",
sMsrAccessingResult = "[sync ] fnGet" + self.getCounter + ": access result[sync] key [" + key + "]",
sMsrPutMetadata = "[sync ] fnGet" + self.getCounter + ": putMetadata[sync] key [" + key + "]",
sMsrDeserialize = "[sync ] fnGet" + self.getCounter + ": deserialize[sync] key [" + key + "]",
sMsrImplementationGet = "[sync ] _instance.get",
sMsrGetRequestOnSuccess = "[sync ] getRequest.onSuccess";
Log.debug("Cache Manager LRUPersistentCache: get for key [" + key + "]...");
Measurement.start(sMsrImplementationGet, "CM", sMsrCatGet);
var p = new Promise(function fnGet(resolve, reject) {
var result, transaction, getRequest, oItem;
Measurement.start(sMsrTotal, "CM", sMsrCatGet);
Measurement.start(sMsrOpeningTx, "CM", sMsrCatGet);
transaction = self._db.transaction([self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName], "readwrite");
Measurement.end(sMsrOpeningTx);
transaction.onerror = function (event) {
var sMessage = "Cache Manager cannot complete delete transaction for entry with key: " + key + ". Details: " + transaction.error;
Log.error(sMessage);
reject(sMessage);
};
try {
Measurement.start(sMsrOpeningStores, "CM", sMsrCatGet);
getRequest = transaction.objectStore(self.defaultOptions._contentStoreName).get(key);
Measurement.end(sMsrOpeningStores);
getRequest.onsuccess = function (event) {
Measurement.start(sMsrGetRequestOnSuccess, "CM", sMsrCatGet);
Measurement.start(sMsrAccessingResult, "CM", sMsrCatGet);
oItem = new Item(getRequest.result, sMsrDeserialize, sMsrCatGet);
Measurement.end(sMsrAccessingResult);
debugMsr("Cache Manager LRUPersistentCache: accessing the result", key, sMsrAccessingResult);
if (oItem.oData) {
Measurement.start(sMsrPutMetadata, "CM", sMsrCatGet);
if (oItem.oData.lu !== self._mru) { // Update the usage data only if the item is not already the most used one
oItem.oData.lu = ++self._mru;
updateItemUsage(self, oItem);
}
//postponed as update of the metadata is not crucial here
scheduleMetadataSave(self, oItem);
Measurement.end(sMsrPutMetadata);
result = oItem.deserialize().oData.value;
}
Measurement.end(sMsrGetRequestOnSuccess);
Log.debug("Cache Manager LRUPersistentCache: get for key [" + key + "]...done");
resolve(result); // whatever it is (null, undefined, real value from the storage) - we return it back
};
getRequest.onerror = function (event) {
Log.error("Cache Manager cannot get entry with key: " + key + ". Details: " + event.message);
reject(event.message);
};
} catch (e) {
Log.error("Cache Manager cannot get entry with key: " + key + ". Details: " + e.message);
reject(e.message);
return;
}
Measurement.end(sMsrTotal);
});
Measurement.end(sMsrImplementationGet);
return p;
}
function deleteItemAndUpdateMetadata(self) {
var vKeyToDelete = getNextItemToDelete(self);
if (vKeyToDelete == undefined) {
var sErrMsg = "Cache Manager LRUPersistentCache: deleteItemAndUpdateMetadata cannot find item to delete";
Log.debug(sErrMsg);
return Promise.reject(sErrMsg);
}
return internalDel(self, vKeyToDelete).then(function () {
return Promise.resolve().then(function () {
deleteMetadataForEntry(self, vKeyToDelete);
return persistMetadata(self).then(function () {
return vKeyToDelete;
}, function () {
Log.warning("Cache Manager LRUPersistentCache: Free space algorithm deleted item " +
"but the metadata changes could not be persisted. This won't break the functionality.");
return vKeyToDelete;
});
});
});
}
function persistMetadata(self) {
return new Promise(function (resolve, reject) {
try {
var tx = self._db.transaction([self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName], "readwrite");
tx.onerror = errorHandler;
tx.onabort = errorHandler;
tx.oncomplete = function () {
Log.debug("Cache Manager LRUPersistentCache: persistMetadata - metadata was successfully updated");
resolve();
};
tx.objectStore(self.defaultOptions._metadataStoreName).put(self._metadata, self.defaultOptions._metadataKey);
} catch (e) {
errorHandler(null, e);
}
function errorHandler(event, exception) {
var sErrMsg = "Cache Manager LRUPersistentCache: persistMetadata error - metadata was not successfully persisted. Details: " +
collectErrorData(event) + ". Exception: " + (exception ? exception.message : "");
Log.debug(sErrMsg);
reject(sErrMsg);
}
});
}
function internalDel(self, key) {
return new Promise(function (resolve, reject) {
var tx = self._db.transaction([self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName], "readwrite");
function errorHandler(event) {
var sMessage = "Cache Manager LRUPersistentCache: internalDel cannot complete delete transaction for entry with key: "
+ key + ". Details: " + collectErrorData(event);
Log.warning(sMessage);
reject(event);
}
tx.onerror = errorHandler;
tx.onabort = errorHandler;
tx.oncomplete = function () {
// The cache is empty so its obvious we can reset the counters
if (getItemCount(self) === 0) {
self._lru = 0;
self._mru = 0;
self._metadata = initMetadata(self._ui5version);
}
Log.debug("Cache Manager LRUPersistentCache: internalDel deleting item [" + key + "]...done");
resolve();
};
Log.debug("Cache Manager LRUPersistentCache: internalDel deleting item [" + key + "]...");
tx.objectStore(self.defaultOptions._contentStoreName).delete(key);
});
}
function internalSet(self, key, value) {
return new Promise(function (resolve, reject) {
var objectStoreRequest,
transaction, backupMetadata,
sMsrSerialize = "[sync ] internalSet: serialize[sync] key [" + key + "]";
backupMetadata = cloneMetadata(self._metadata);
var oItem = new Item(key, value, typeof value, ++self._mru, sMsrSerialize, sMsrCatSet).serialize();
Log.debug("Cache Manager: LRUPersistentCache: internal set with parameters: key [" + oItem.oData.key + "], access index [" + oItem.oData.lu + "]");
//store in database
transaction = self._db.transaction([self.defaultOptions._contentStoreName, self.defaultOptions._metadataStoreName], "readwrite");
transaction.onerror = errorHandler;
transaction.onabort = errorHandler;
function errorHandler(event) {
Log.debug("Cache Manager: LRUPersistentCache: internal set failed. Details: " + collectErrorData(event));
self._metadata = backupMetadata;
assignRUCounters(self);
reject(event);
}
transaction.oncomplete = function () {
Log.debug("Cache Manager: LRUPersistentCache: Internal set transaction completed. ItemCount: " + getItemCount(self));
resolve();
};
objectStoreRequest = transaction.objectStore(self.defaultOptions._contentStoreName).put(oItem.oData, oItem.oData.key);
objectStoreRequest.onsuccess = function () {
updateItemUsage(self, oItem);
transaction.objectStore(self.defaultOptions._metadataStoreName).put(self._metadata, self.defaultOptions._metadataKey);
};
}
);
}
/**
* Set/updates item's usage by setting LRU indexes and moves the LRU pointer if needed.
* @param {sap.ui.core.cache.LRUPersistenceCache} self the <code>this</code> instance
* @param {Item} oItem the item to update indexes for
*/
function updateItemUsage(self, oItem) {
if (self._metadata.__byKey__[oItem.oData.key] != null) { // ItemData already exists, we need to remove the old position index
var oldIndex = self._metadata.__byKey__[oItem.oData.key];
delete self._metadata.__byIndex__[oldIndex];
Log.debug("Cache Manager LRUPersistentCache: set/internalset - item already exists, so its indexes are updated");
}
self._metadata.__byIndex__[oItem.oData.lu] = oItem.oData.key;
self._metadata.__byKey__[oItem.oData.key] = oItem.oData.lu;
seekMetadataLRU(self);
}
function initIndexedDB(instance) {
instance._ui5version = instance._getVersion();
return new Promise(function executorInitIndexedDB(resolve, reject) {
var DBOpenRequest;
Log.debug("Cache Manager " + "_initIndexedDB started");
function openDB() {
try {
DBOpenRequest = window.indexedDB.open(instance.defaultOptions.databaseName, 1);
} catch (e) {
Log.error("Could not open Cache Manager database. Details: " + e.message);
reject(e.message);
}
}
openDB();
DBOpenRequest.onerror = function (event) {
Log.error("Could not initialize Cache Manager database. Details: " + event.message);
reject(event.error);
};
DBOpenRequest.onsuccess = function (event) {
var oMsr = startMeasurements("init_onsuccess");
instance._db = DBOpenRequest.result;
instance._db.onversionchange = function (event) {
if (!event.newVersion) { /* Means database is about to be deleted. See http://www.w3.org/TR/IndexedDB/#dfn-steps-for-deleting-a-database */
event.target.close();
}
};
instance._loadMetaStructure().then(
function () {
Log.debug("Cache Manager " + " metadataLoaded. Serialization support: " + isSerializationSupportOn() + ", resolving initIndexDb promise");
resolve(instance);
}, reject);
oMsr.endSync();
};
DBOpenRequest.onupgradeneeded = function (event) {
var db = event.target.result;
db.onerror = function (event) {
Log.error("Cache Manager error. Details: " + event.message);
reject(db.error);
};
try {
var objectStore = db.createObjectStore(instance.defaultOptions._contentStoreName);
db.createObjectStore(instance.defaultOptions._metadataStoreName);
} catch (e) {
Log.error("Could not initialize Cache Manager object store. Details: " + e.message);
throw e;
}
objectStore.createIndex("ui5version", "ui5version", {unique: false});
};
});
}
function ItemData(key, value, type, lastUsedIndex) {
this.key = key;
this.sOrigType = type;
this.value = value;
this.lu = lastUsedIndex;
}
function Item(key, value, type, lastUsedIndex, sMeasureId, sMsrCat) {
if (arguments.length === 3) { //ItemData constructor, usually used when getting ItemData from DB
this.oData = key;
this.sMeasureId = value;
this.sMsrCat = type;
} else { //
this.oData = new ItemData(key, value, type, lastUsedIndex);
}
}
/**
* Deserializes the value if serialization support is switched on
* @returns {Item} <code>this</code>
*/
Item.prototype.deserialize = function () {
if (isSerializationSupportOn() && this.oData.sOrigType === "object") {
Measurement.start(this.sMeasureId, this.sMeasureId, this.sMsrCat);
this.oData.value = JSON.parse(this.oData.value);
Measurement.end(this.sMeasureId);
debugMsr("Cache Manager LRUPersistentCache: de-serialization the result", this.oData.key, this.sMeasureId);
}
return this;
};
/**
* Serializes the value if serialization support is switched on
* @returns {Item} <code>this</code>
*/
Item.prototype.serialize = function () {
if (isSerializationSupportOn() && this.oData.sOrigType === "object") {
Measurement.start(this.sMeasureId, this.sMeasureId, this.sMsrCat);
this.oData.value = JSON.stringify(this.oData.value);
Measurement.end(this.sMeasureId);
debugMsr("Cache Manager LRUPersistentCache: serialization of the value", this.oData.key, this.sMeasureId);
}
return this;
};
function initMetadata(ui5version) {
return {
timestamps: {},
__byKey__: {},
__byIndex__: {},
__ui5version: ui5version
};
}
/**
* Clones a given metadata instance
* @param source the instance to clone
* @returns {*} cloned metadata
*/
function cloneMetadata(source) {
var backupMetadata = initMetadata(source.__ui5version);
for (var index in source.__byIndex__) {
backupMetadata.__byIndex__[index] = source.__byIndex__[index];
}
for (var key in source.__byKey__) {
backupMetadata.__byKey__[key] = source.__byKey__[key];
}
for (var key in source.timestamps) {
backupMetadata.timestamps[key] = source.timestamps[key];
}
return backupMetadata;
}
function assignRUCounters(self) {
var lrumru = computeLRUMRU(self._metadata.__byIndex__);
self._mru = lrumru.mru;
self._lru = lrumru.lru;
Log.debug("Cache Manager LRUPersistentCache: LRU counters are assigned to the CM: " + JSON.stringify(lrumru));
}
function getItemCount(self) {
return Object.keys(self._metadata.__byKey__).length;
}
function getNextItemToDelete(self) {
var oKey = self._metadata.__byIndex__[self._lru];
if (oKey == undefined && !seekMetadataLRU(self)) {
return null;
} else {
//LRU is moved to an existing item by seekMetadataLRU
return self._metadata.__byIndex__[self._lru];
}
}
function computeLRUMRU(lruIndexes) {
var i = -1, mru = -1, lru = Number.MAX_VALUE,
aLruIndexKeys = Object.keys(lruIndexes),
iLength = aLruIndexKeys.length;
if (iLength === 0) {
return {mru: -1, lru: -1};
} else {
while (++i < iLength) {
var iIndex = parseInt(aLruIndexKeys[i]);
if (mru < iIndex) {
mru = iIndex;
}
if (lru > iIndex) {
lru = iIndex;
}
}
return {mru: mru, lru: lru};
}
}
/**
* Tries to free space until the given new item is successfully added.
* @param {sap.ui.core.cache.LRUPersistentCache} self the instance of the Cache Manager
* @param {string|number} key the key to associate the value with
* @param {any} value value to free space for
* @returns {Promise} a promise that will resolve if the given item is added, or reject - if not.
*/
function cleanAndStore(self, key, value) {
return new Promise(function (resolve, reject) {
var attempt = 0;
_cleanAndStore(self, key, value);
function _cleanAndStore(self, key, value) {
attempt++;
Log.debug("Cache Manager LRUPersistentCache: cleanAndStore: freeing space attempt [" + (attempt) + "]");
deleteItemAndUpdateMetadata(self).then(function (deletedKey) {
Log.debug("Cache Manager LRUPersistentCache: cleanAndStore: deleted item with key [" + deletedKey + "]. Going to put " + key);
return internalSet(self, key, value).then(resolve, function (event) {
if (isQuotaExceeded(event)) {
Log.debug("Cache Manager LRUPersistentCache: cleanAndStore: QuotaExceedError during freeing up space...");
if (getItemCount(self) > 0) {
_cleanAndStore(self, key, value);
} else {
reject("Cache Manager LRUPersistentCache: cleanAndStore: even when the cache is empty, the new item with key [" + key + "] cannot be added");
}
} else {
reject("Cache Manager LRUPersistentCache: cleanAndStore: cannot free space: " + collectErrorData(event));
}
});
}, reject);
}
});
}
function isQuotaExceeded(event) {
return (event && event.target && event.target.error && event.target.error.name === "QuotaExceededError");
}
/**
* Deletes all metadata for given key
* @param self the instance
* @param key the key for the entry
*/
function deleteMetadataForEntry(self, key) {
var iIndex = self._metadata.__byKey__[key];
delete self._metadata.__byKey__[key];
delete self._metadata.__byIndex__[iIndex];
if (self._metadata.timestamps[key]) {
delete self._metadata.timestamps[key];
}
seekMetadataLRU(self);
}
/**
* Moves the pointer of LRU to the next available (non-empty) item starting from the current position.
* @returns {boolean} true if the seek moved the pointer to an existing item, false - if no item is found
*/
function seekMetadataLRU(self) {
while (self._lru <= self._mru && self._metadata.__byIndex__[self._lru] == undefined) {
self._lru++;
}
// The lru should never skip item. So current value should always point to the 1st (numeric order) non-empty
// item in self._metadata.__byIndex map
return (self._lru <= self._mru);
}
function collectErrorData(event) {
if (!event) {
return "";
}
var sResult = event.message;
if (event.target && event.target.error && event.target.error.name) {
sResult += " Error name: " + event.target.error.name;
}
return sResult;
}
function isSerializationSupportOn() {
return BaseConfig.get({
name: "sapUiXxCacheSerialization",
type: BaseConfig.Type.Boolean,
external: true
});
}
function getExcludedKeys() {
return BaseConfig.get({
name: "sapUiXxCacheExcludedKeys",
type: BaseConfig.Type.StringArray,
external: true
});
}
/**
* Checks whether given key matches any item in the set of excluded keys.
* The matching utilizes "wildcard string comparison" and is case sensitive.
* @param key the key to check
* @returns {boolean} true if the key matches at least ont of the set of the excluded keys
* @private
*/
function keyMatchesExclusionStrings(key) {
return getExcludedKeys().some(function (excludedKey) {
return key.indexOf(excludedKey) > -1;
});
}
function startMeasurements(sOperation, key) {
iMsrCounter++;
var sMeasureAsync = "[async] " + sOperation + "[" + key + "]- #" + (iMsrCounter),
sMeasureSync = "[sync ] " + sOperation + "[" + key + "]- #" + (iMsrCounter);
Measurement.start(sMeasureAsync, "CM", ["LRUPersistentCache", sOperation]);
Measurement.start(sMeasureSync, "CM", ["LRUPersistentCache", sOperation]);
return {
sMeasureAsync: sMeasureAsync,
sMeasureSync: sMeasureSync,
endAsync: function () {
Measurement.end(this.sMeasureAsync);
},
endSync: function () {
Measurement.end(this.sMeasureSync);
}
};
}
/**
* Logs a debug message related to a certain {@link module:sap/ui/performance/Measurement measurement}
* if log level is debug or higher.
*
* @param {string} sMsg the message
* @param {string} sKey the key to log message for
* @param {string} sMsrId the measurementId to use for obtaining the measurement
*/
function debugMsr(sMsg, sKey, sMsrId) {
//avoid redundant string concatenation & getMeasurement call
if (Log.getLevel() >= Log.Level.DEBUG) {
Log.debug(sMsg + " for key [" + sKey + "] took: " + Measurement.getMeasurement(sMsrId).duration);
}
}
return LRUPersistentCache;
});