VulpesOS/gaia/apps/communications/contacts/js/cache.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

454 lines
12 KiB
JavaScript
Executable file

'use strict';
(function() {
// Ideally we should store this value in the configuration file.
// However, that means that we need to load two more script files and
// read directly from the config.json file or from the cookie storing
// the configuration data. This steals us ~150ms of startup time, so
// we chose to hard code this value instead.
const CACHE_ENABLED = true;
const FIRST_CHUNK = 'firstChunk';
const l10nStrings = {
'title': {
query: 'gaia-header h1#app-title[data-l10n-id="contacts"]',
container: 'textContent'
},
'search': {
query: 'input[data-l10n-id="search-contact"]',
container: 'placeholder'
}
};
var _observers = {};
function addObserver(aMessage, aCallback) {
if (typeof aCallback !== 'function') {
return;
}
if (!_observers[aMessage]) {
_observers[aMessage] = [];
}
_observers[aMessage].push(aCallback);
}
function notifyObservers(aMessage, aData) {
var observers = _observers[aMessage];
if (!observers) {
return;
}
for (var callback in observers) {
if (observers[callback]) {
observers[callback](aData);
}
}
}
// So far we only cache the first chunk of contacts, but we may want to
// extend the cache to a bigger or even the whole list in the future.
var _caches = new Map();
_caches.set(FIRST_CHUNK, {
containerId: 'groups-list',
active: false
});
var _cachedContacts;
var _cachedFavorites;
var _cachedHeaders;
var _cacheEvictionTimer;
function setCache(aCache) {
if (!CACHE_ENABLED || !aCache.id ||
!aCache.content || !aCache.lastOrderString ||
!_caches.has(aCache.id)) {
return;
}
var l10n = {};
Object.keys(l10nStrings).forEach(key => {
var string = l10nStrings[key];
var selector = document.querySelector(string.query);
if (!selector || !selector[string.container]) {
return;
}
l10n[key] = selector[string.container];
});
localStorage.setItem(aCache.id, JSON.stringify({
content: aCache.content,
languageDirection: navigator.mozL10n.language.direction,
languageCode: navigator.mozL10n.language.code,
lastOrderString: aCache.lastOrderString,
updated: Date.now(),
// Because we lazy load l10n.js quite late, we need to cache the
// localized strings to avoid showing the unlocalized text during
// the start up process.
l10n: l10n
}));
}
function deleteCache(aId) {
if (!CACHE_ENABLED) {
return;
}
localStorage.removeItem(aId);
}
function getCachedContacts(aNodeList) {
if (!CACHE_ENABLED || !aNodeList) {
return;
}
if (!_cachedContacts) {
_cachedContacts = new Map();
}
if (!_cachedFavorites) {
_cachedFavorites = new Map();
}
for (var i = 0; i < aNodeList.length; i++) {
var cacheEntry = aNodeList[i];
if (cacheEntry.parentNode.id == 'contact-list-ice') {
continue;
}
if (cacheEntry.parentNode.id == 'contacts-list-favorites') {
_cachedFavorites.set(cacheEntry.dataset.uuid,
cacheEntry.innerHTML);
continue;
}
_cachedContacts.set(cacheEntry.dataset.uuid,
cacheEntry.innerHTML);
}
}
function appendNodesToContainer(aContainer, aNodeList) {
if (!CACHE_ENABLED || !aNodeList.length) {
return Promise.resolve();
}
return new Promise((resolve) => {
if (!_cachedHeaders) {
_cachedHeaders = {};
}
var resolved = false;
var fragment = document.createDocumentFragment();
aNodeList.forEach((node) => {
if (!node.elementName) {
return;
}
var child = document.createElement(node.elementName);
node.attributes && node.attributes.forEach((attribute) => {
if (!attribute.name || !attribute.value) {
return;
}
child.setAttribute(attribute.name, attribute.value);
});
if (node.innerHTML) {
child.innerHTML = node.innerHTML;
}
fragment.appendChild(child);
var headerName = child.id.split('section-group-')[1];
_cachedHeaders[headerName] =
fragment.querySelector('ol#contacts-list-' + headerName);
if (!resolved) {
resolve();
}
});
getCachedContacts(fragment.querySelectorAll('li[data-cache=true]'));
aContainer.appendChild(fragment);
});
}
function removeCacheFromContainer(aContainer) {
if (!CACHE_ENABLED || !_cachedContacts) {
return;
}
for (var id of _cachedContacts.keys()) {
var items = aContainer.querySelectorAll('li[data-uuid=\"' + id + '\"]');
Array.prototype.forEach.call(items, (item) => {
var ol = item.parentNode;
ol.removeChild(item);
if (ol.children.length < 1) {
var header = _cachedHeaders[ol.dataset.group];
if (header) {
var groupTitle = header.parentNode.children[0];
groupTitle.classList.add('hide');
}
}
});
}
}
function undoCache(aCacheId) {
if (!CACHE_ENABLED || !_caches.has(aCacheId)) {
return;
}
var _cache = _caches.get(aCacheId);
if (!_cache.containerId) {
return;
}
var container = document.getElementById(_cache.containerId);
if (!container) {
console.warning('Could not remove cached content from ' +
_cache.containerId);
return;
}
removeCacheFromContainer(container);
}
var Cache = {
get active() {
return CACHE_ENABLED && _caches.get(FIRST_CHUNK).active;
},
get enabled() {
return CACHE_ENABLED;
},
get contacts() {
return (CACHE_ENABLED && _cachedContacts) ? _cachedContacts.keys()
: null;
},
get favorites() {
return (CACHE_ENABLED && _cachedFavorites) ? _cachedFavorites.keys()
: null;
},
get length() {
return (CACHE_ENABLED && _cachedContacts) ?
_cachedContacts.size + _cachedFavorites.size : 0;
},
get headers() {
return (CACHE_ENABLED && _cachedHeaders) ? _cachedHeaders
: {};
},
// We need to know the order string of the last contact in the cache
// so we can insert externaly added contacts in the appropriate order.
get lastOrderString() {
return (CACHE_ENABLED && _caches.get(FIRST_CHUNK).lastOrderString);
},
get updated() {
return (CACHE_ENABLED && _caches.get(FIRST_CHUNK).updated);
},
// For testing purposes only.
get _rawContent() {
return _caches.get(FIRST_CHUNK).content;
},
set firstChunk(aFirstChunk) {
if (!CACHE_ENABLED || !aFirstChunk ||
!aFirstChunk.cache || !aFirstChunk.lastOrderString) {
return;
}
setCache({
id: FIRST_CHUNK,
content: aFirstChunk.cache,
lastOrderString: aFirstChunk.lastOrderString
});
},
set oneviction(aCallback) {
if (!CACHE_ENABLED) {
return;
}
addObserver('oneviction', aCallback);
},
hasContact: function(aUuid) {
if (!CACHE_ENABLED) {
return;
}
return _cachedContacts && _cachedContacts.has(aUuid);
},
hasFavorite: function(aUuid) {
if (!CACHE_ENABLED) {
return;
}
return _cachedFavorites && _cachedFavorites.has(aUuid);
},
getContact: function(aUuid) {
if (!CACHE_ENABLED || !this.hasContact(aUuid)) {
return;
}
// We should get each contact once while rendering the contacts list
// to see if what we have in the cache is different to what we have in
// the contacts source (most likely mozContacts). Removing the contact
// entry from the map allow us to easily check if we have any contact in
// the cache that was deleted from the original source and so it needs
// to be removed from the view.
var contact = _cachedContacts.get(aUuid);
_cachedContacts.delete(aUuid);
return contact;
},
getFavorite: function(aUuid) {
if (!CACHE_ENABLED || !this.hasFavorite(aUuid)) {
return;
}
var contact = _cachedFavorites.get(aUuid);
_cachedFavorites.delete(aUuid);
return contact;
},
removeContact: function(aUuid) {
if (!CACHE_ENABLED ||
!this.hasContact(aUuid) ||
!this.hasFavorite(aUuid)) {
return;
}
if (_cachedContacts.has(aUuid)) {
_cachedContacts.delete(aUuid);
}
if (_cachedFavorites.has(aUuid)) {
_cachedFavorites.delete(aUuid);
}
},
_doEvict: function(aUndo) {
_cacheEvictionTimer = null;
if (aUndo) {
undoCache(FIRST_CHUNK);
}
deleteCache(FIRST_CHUNK);
this.cleanup();
notifyObservers('oneviction');
},
evict: function(aUndo, aInstant) {
if (!CACHE_ENABLED) {
return;
}
// There are cases where we need to evict the cache
// inmediately, like in web activities, where we can be closed
// before doing the eviction.
if (aInstant) {
this._doEvict(aUndo);
return;
}
// Evicting the cache means accessing localStorage.
// Use the following logic to evict the cache in a moderate pattern.
// 1. When a request to evict the cache is received, start a timer.
// 2. If another request is received while the timer is
// running, reset it.
// 3. Once the timer fires, evict the cache.
if (_cacheEvictionTimer) {
clearTimeout(_cacheEvictionTimer);
}
_cacheEvictionTimer = setTimeout(() => {
this._doEvict(aUndo);
}, 1000);
},
maybeEvict: function() {
if (!CACHE_ENABLED || !this.active) {
return;
}
if ((_caches.get(FIRST_CHUNK).languageDirection !=
navigator.mozL10n.language.direction) ||
(_caches.get(FIRST_CHUNK).languageCode !=
navigator.mozL10n.language.code)) {
// If the language direction or code changed, we need to evict and also
// undo the applied cache.
this.evict(true /* remove applied cache from DOM */);
}
},
cleanup: function() {
if (!CACHE_ENABLED) {
return;
}
_caches.get(FIRST_CHUNK).active = false;
_caches.get(FIRST_CHUNK).content = null;
_cachedContacts = null;
_cachedFavorites = null;
_cachedHeaders = null;
},
apply: function(aCacheId) {
if (!CACHE_ENABLED || !_caches.has(aCacheId)) {
return Promise.reject();
}
var _cache = _caches.get(aCacheId);
var cache = localStorage.getItem(aCacheId);
if (!cache) {
return Promise.reject();
}
try {
cache = JSON.parse(cache);
_cache.content = cache.content;
_cache.languageDirection = cache.languageDirection;
_cache.languageCode = cache.languageCode;
_cache.lastOrderString = cache.lastOrderString;
_cache.updated = new Date(cache.updated);
} catch(e) {
console.error(e);
return Promise.reject();
}
if (!_cache.containerId) {
return Promise.reject();
}
if (cache.l10n) {
Object.keys(cache.l10n).forEach(key => {
if (!l10nStrings[key] || cache.l10n[key] === 'undefined') {
return;
}
var string = l10nStrings[key];
var selector = document.querySelector(string.query);
if (!selector) {
return;
}
selector[string.container] = cache.l10n[key];
});
}
if (cache.languageDirection) {
var html = document.querySelector('html');
html.setAttribute('dir', cache.languageDirection);
}
var container = document.getElementById(_cache.containerId);
if (!container) {
console.warning('Could not apply cached content to ' +
_cache.containerId);
return Promise.reject();
}
return appendNodesToContainer(container, _cache.content).then(() => {
_cache.content = null;
_cache.active = true;
_caches.set(aCacheId, _cache);
return Promise.resolve();
});
}
};
window.Cache = Cache;
})();