diff options
Diffstat (limited to 'js/assets.js')
-rw-r--r-- | js/assets.js | 911 |
1 files changed, 911 insertions, 0 deletions
diff --git a/js/assets.js b/js/assets.js new file mode 100644 index 0000000..c3fb85f --- /dev/null +++ b/js/assets.js @@ -0,0 +1,911 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2013-2019 Raymond Hill + Copyright (C) 2019 Alessio Vanni + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +µMatrix.assets = (function() { + +/******************************************************************************/ + +var reIsExternalPath = /^(?:[a-z-]+):\/\//, + errorCantConnectTo = vAPI.i18n('errorCantConnectTo'), + noopfunc = function(){}; + +var api = { +}; + +/******************************************************************************/ + +var observers = []; + +api.addObserver = function(observer) { + if ( observers.indexOf(observer) === -1 ) { + observers.push(observer); + } +}; + +api.removeObserver = function(observer) { + var pos; + while ( (pos = observers.indexOf(observer)) !== -1 ) { + observers.splice(pos, 1); + } +}; + +var fireNotification = function(topic, details) { + var result; + for ( var i = 0; i < observers.length; i++ ) { + if ( observers[i](topic, details) === false ) { + result = false; + } + } + return result; +}; + +/******************************************************************************/ + +api.fetchText = function(url, onLoad, onError) { + var actualUrl = reIsExternalPath.test(url) ? url : vAPI.getURL(url); + + if ( typeof onError !== 'function' ) { + onError = onLoad; + } + + // https://github.com/gorhill/uMatrix/issues/15 + var onResponseReceived = function() { + this.onload = this.onerror = this.ontimeout = null; + // xhr for local files gives status 0, but actually succeeds + var details = { + url: url, + content: '', + statusCode: this.status || 200, + statusText: this.statusText || '' + }; + if ( details.statusCode < 200 || details.statusCode >= 300 ) { + return onError.call(null, details); + } + // consider an empty result to be an error + if ( stringIsNotEmpty(this.responseText) === false ) { + return onError.call(null, details); + } + // we never download anything else than plain text: discard if response + // appears to be a HTML document: could happen when server serves + // some kind of error page I suppose + var text = this.responseText.trim(); + if ( text.startsWith('<') && text.endsWith('>') ) { + return onError.call(null, details); + } + details.content = this.responseText; + return onLoad.call(null, details); + }; + + var onErrorReceived = function() { + this.onload = this.onerror = this.ontimeout = null; + µMatrix.logger.writeOne('', 'error', errorCantConnectTo.replace('{{msg}}', actualUrl)); + onError.call(null, { url: url, content: '' }); + }; + + // Be ready for thrown exceptions: + // I am pretty sure it used to work, but now using a URL such as + // `file:///` on Chromium 40 results in an exception being thrown. + var xhr = new XMLHttpRequest(); + try { + xhr.open('get', actualUrl, true); + xhr.timeout = 30000; + xhr.onload = onResponseReceived; + xhr.onerror = onErrorReceived; + xhr.ontimeout = onErrorReceived; + xhr.responseType = 'text'; + xhr.send(); + } catch (e) { + onErrorReceived.call(xhr); + } +}; + +/******************************************************************************* + + TODO(seamless migration): + This block of code will be removed when I am confident all users have + moved to a version of uBO which does not require the old way of caching + assets. + + api.listKeyAliases: a map of old asset keys to new asset keys. + + migrate(): to seamlessly migrate the old cache manager to the new one: + - attempt to preserve and move content of cached assets to new locations; + - removes all traces of now obsolete cache manager entries in cacheStorage. + + This code will typically execute only once, when the newer version of uBO + is first installed and executed. + +**/ + +api.listKeyAliases = { + "assets/thirdparties/publicsuffix.org/list/effective_tld_names.dat": "public_suffix_list.dat", + "assets/thirdparties/hosts-file.net/ad-servers": "hphosts", + "assets/thirdparties/www.malwaredomainlist.com/hostslist/hosts.txt": "malware-0", + "assets/thirdparties/mirror1.malwaredomains.com/files/justdomains": "malware-1", + "assets/thirdparties/pgl.yoyo.org/as/serverlist": "plowe-0", + "assets/thirdparties/someonewhocares.org/hosts/hosts": "dpollock-0", + "assets/thirdparties/winhelp2002.mvps.org/hosts.txt": "mvps-0" +}; + +var migrate = function(callback) { + var entries, + moveCount = 0, + toRemove = []; + + var countdown = function(change) { + moveCount -= (change || 0); + if ( moveCount !== 0 ) { return; } + vAPI.cacheStorage.remove(toRemove); + saveAssetCacheRegistry(); + callback(); + }; + + var onContentRead = function(oldKey, newKey, bin) { + var content = bin && bin['cached_asset_content://' + oldKey] || undefined; + if ( content ) { + assetCacheRegistry[newKey] = { + readTime: Date.now(), + writeTime: entries[oldKey] + }; + if ( reIsExternalPath.test(oldKey) ) { + assetCacheRegistry[newKey].remoteURL = oldKey; + } + bin = {}; + bin['cache/' + newKey] = content; + vAPI.cacheStorage.set(bin); + } + countdown(1); + }; + + var onEntries = function(bin) { + entries = bin && bin.cached_asset_entries; + if ( !entries ) { return callback(); } + if ( bin && bin.assetCacheRegistry ) { + assetCacheRegistry = bin.assetCacheRegistry; + } + var aliases = api.listKeyAliases; + for ( var oldKey in entries ) { + var newKey = aliases[oldKey]; + if ( !newKey && /^https?:\/\//.test(oldKey) ) { + newKey = oldKey; + } + if ( newKey ) { + vAPI.cacheStorage.get( + 'cached_asset_content://' + oldKey, + onContentRead.bind(null, oldKey, newKey) + ); + moveCount += 1; + } + toRemove.push('cached_asset_content://' + oldKey); + } + toRemove.push('cached_asset_entries', 'extensionLastVersion'); + countdown(); + }; + + vAPI.cacheStorage.get( + [ 'cached_asset_entries', 'assetCacheRegistry' ], + onEntries + ); +}; + +/******************************************************************************* + + The purpose of the asset source registry is to keep key detail information + about an asset: + - Where to load it from: this may consist of one or more URLs, either local + or remote. + - After how many days an asset should be deemed obsolete -- i.e. in need of + an update. + - The origin and type of an asset. + - The last time an asset was registered. + +**/ + +var assetSourceRegistryStatus, + assetSourceRegistry = Object.create(null); + +var registerAssetSource = function(assetKey, dict) { + var entry = assetSourceRegistry[assetKey] || {}; + for ( var prop in dict ) { + if ( dict.hasOwnProperty(prop) === false ) { continue; } + if ( dict[prop] === undefined ) { + delete entry[prop]; + } else { + entry[prop] = dict[prop]; + } + } + var contentURL = dict.contentURL; + if ( contentURL !== undefined ) { + if ( typeof contentURL === 'string' ) { + contentURL = entry.contentURL = [ contentURL ]; + } else if ( Array.isArray(contentURL) === false ) { + contentURL = entry.contentURL = []; + } + var remoteURLCount = 0; + for ( var i = 0; i < contentURL.length; i++ ) { + if ( reIsExternalPath.test(contentURL[i]) ) { + remoteURLCount += 1; + } + } + entry.hasLocalURL = remoteURLCount !== contentURL.length; + entry.hasRemoteURL = remoteURLCount !== 0; + } else if ( entry.contentURL === undefined ) { + entry.contentURL = []; + } + if ( typeof entry.updateAfter !== 'number' ) { + entry.updateAfter = 13; + } + if ( entry.submitter ) { + entry.submitTime = Date.now(); // To detect stale entries + } + assetSourceRegistry[assetKey] = entry; +}; + +var unregisterAssetSource = function(assetKey) { + assetCacheRemove(assetKey); + delete assetSourceRegistry[assetKey]; +}; + +var saveAssetSourceRegistry = (function() { + var timer; + var save = function() { + timer = undefined; + vAPI.cacheStorage.set({ assetSourceRegistry: assetSourceRegistry }); + }; + return function(lazily) { + if ( timer !== undefined ) { + clearTimeout(timer); + } + if ( lazily ) { + timer = vAPI.setTimeout(save, 500); + } else { + save(); + } + }; +})(); + +var updateAssetSourceRegistry = function(json, silent) { + var newDict; + try { + newDict = JSON.parse(json); + } catch (ex) { + } + if ( newDict instanceof Object === false ) { return; } + + var oldDict = assetSourceRegistry, + assetKey; + + // Remove obsolete entries (only those which were built-in). + for ( assetKey in oldDict ) { + if ( + newDict[assetKey] === undefined && + oldDict[assetKey].submitter === undefined + ) { + unregisterAssetSource(assetKey); + } + } + // Add/update existing entries. Notify of new asset sources. + for ( assetKey in newDict ) { + if ( oldDict[assetKey] === undefined && !silent ) { + fireNotification( + 'builtin-asset-source-added', + { assetKey: assetKey, entry: newDict[assetKey] } + ); + } + registerAssetSource(assetKey, newDict[assetKey]); + } + saveAssetSourceRegistry(); +}; + +var getAssetSourceRegistry = function(callback) { + // Already loaded. + if ( assetSourceRegistryStatus === 'ready' ) { + callback(assetSourceRegistry); + return; + } + + // Being loaded. + if ( Array.isArray(assetSourceRegistryStatus) ) { + assetSourceRegistryStatus.push(callback); + return; + } + + // Not loaded: load it. + assetSourceRegistryStatus = [ callback ]; + + var registryReady = function() { + var callers = assetSourceRegistryStatus; + assetSourceRegistryStatus = 'ready'; + var fn; + while ( (fn = callers.shift()) ) { + fn(assetSourceRegistry); + } + }; + + // First-install case. + var createRegistry = function() { + api.fetchText( + µMatrix.assetsBootstrapLocation || 'assets/assets.json', + function(details) { + updateAssetSourceRegistry(details.content, true); + registryReady(); + } + ); + }; + + vAPI.cacheStorage.get('assetSourceRegistry', function(bin) { + if ( !bin || !bin.assetSourceRegistry ) { + createRegistry(); + return; + } + assetSourceRegistry = bin.assetSourceRegistry; + registryReady(); + }); +}; + +api.registerAssetSource = function(assetKey, details) { + getAssetSourceRegistry(function() { + registerAssetSource(assetKey, details); + saveAssetSourceRegistry(true); + }); +}; + +api.unregisterAssetSource = function(assetKey) { + getAssetSourceRegistry(function() { + unregisterAssetSource(assetKey); + saveAssetSourceRegistry(true); + }); +}; + +/******************************************************************************* + + The purpose of the asset cache registry is to keep track of all assets + which have been persisted into the local cache. + +**/ + +var assetCacheRegistryStatus, + assetCacheRegistryStartTime = Date.now(), + assetCacheRegistry = {}; + +var getAssetCacheRegistry = function(callback) { + // Already loaded. + if ( assetCacheRegistryStatus === 'ready' ) { + callback(assetCacheRegistry); + return; + } + + // Being loaded. + if ( Array.isArray(assetCacheRegistryStatus) ) { + assetCacheRegistryStatus.push(callback); + return; + } + + // Not loaded: load it. + assetCacheRegistryStatus = [ callback ]; + + var registryReady = function() { + var callers = assetCacheRegistryStatus; + assetCacheRegistryStatus = 'ready'; + var fn; + while ( (fn = callers.shift()) ) { + fn(assetCacheRegistry); + } + }; + + var migrationDone = function() { + vAPI.cacheStorage.get('assetCacheRegistry', function(bin) { + if ( bin && bin.assetCacheRegistry ) { + assetCacheRegistry = bin.assetCacheRegistry; + } + registryReady(); + }); + }; + + migrate(migrationDone); +}; + +var saveAssetCacheRegistry = (function() { + var timer; + var save = function() { + timer = undefined; + vAPI.cacheStorage.set({ assetCacheRegistry: assetCacheRegistry }); + }; + return function(lazily) { + if ( timer !== undefined ) { clearTimeout(timer); } + if ( lazily ) { + timer = vAPI.setTimeout(save, 500); + } else { + save(); + } + }; +})(); + +var assetCacheRead = function(assetKey, callback) { + var internalKey = 'cache/' + assetKey; + + var reportBack = function(content, err) { + var details = { assetKey: assetKey, content: content }; + if ( err ) { details.error = err; } + callback(details); + }; + + var onAssetRead = function(bin) { + if ( !bin || !bin[internalKey] ) { + return reportBack('', 'E_NOTFOUND'); + } + var entry = assetCacheRegistry[assetKey]; + if ( entry === undefined ) { + return reportBack('', 'E_NOTFOUND'); + } + entry.readTime = Date.now(); + saveAssetCacheRegistry(true); + reportBack(bin[internalKey]); + }; + + var onReady = function() { + vAPI.cacheStorage.get(internalKey, onAssetRead); + }; + + getAssetCacheRegistry(onReady); +}; + +var assetCacheWrite = function(assetKey, details, callback) { + var internalKey = 'cache/' + assetKey; + var content = ''; + if ( typeof details === 'string' ) { + content = details; + } else if ( details instanceof Object ) { + content = details.content || ''; + } + + if ( content === '' ) { + return assetCacheRemove(assetKey, callback); + } + + var reportBack = function(content) { + var details = { assetKey: assetKey, content: content }; + if ( typeof callback === 'function' ) { + callback(details); + } + fireNotification('after-asset-updated', details); + }; + + var onReady = function() { + var entry = assetCacheRegistry[assetKey]; + if ( entry === undefined ) { + entry = assetCacheRegistry[assetKey] = {}; + } + entry.writeTime = entry.readTime = Date.now(); + if ( details instanceof Object && typeof details.url === 'string' ) { + entry.remoteURL = details.url; + } + var bin = { assetCacheRegistry: assetCacheRegistry }; + bin[internalKey] = content; + vAPI.cacheStorage.set(bin); + reportBack(content); + }; + getAssetCacheRegistry(onReady); +}; + +var assetCacheRemove = function(pattern, callback) { + var onReady = function() { + var cacheDict = assetCacheRegistry, + removedEntries = [], + removedContent = []; + for ( var assetKey in cacheDict ) { + if ( pattern instanceof RegExp && !pattern.test(assetKey) ) { + continue; + } + if ( typeof pattern === 'string' && assetKey !== pattern ) { + continue; + } + removedEntries.push(assetKey); + removedContent.push('cache/' + assetKey); + delete cacheDict[assetKey]; + } + if ( removedContent.length !== 0 ) { + vAPI.cacheStorage.remove(removedContent); + var bin = { assetCacheRegistry: assetCacheRegistry }; + vAPI.cacheStorage.set(bin); + } + if ( typeof callback === 'function' ) { + callback(); + } + for ( var i = 0; i < removedEntries.length; i++ ) { + fireNotification('after-asset-updated', { assetKey: removedEntries[i] }); + } + }; + + getAssetCacheRegistry(onReady); +}; + +var assetCacheMarkAsDirty = function(pattern, exclude, callback) { + var onReady = function() { + var cacheDict = assetCacheRegistry, + cacheEntry, + mustSave = false; + for ( var assetKey in cacheDict ) { + if ( pattern instanceof RegExp ) { + if ( pattern.test(assetKey) === false ) { continue; } + } else if ( typeof pattern === 'string' ) { + if ( assetKey !== pattern ) { continue; } + } else if ( Array.isArray(pattern) ) { + if ( pattern.indexOf(assetKey) === -1 ) { continue; } + } + if ( exclude instanceof RegExp ) { + if ( exclude.test(assetKey) ) { continue; } + } else if ( typeof exclude === 'string' ) { + if ( assetKey === exclude ) { continue; } + } else if ( Array.isArray(exclude) ) { + if ( exclude.indexOf(assetKey) !== -1 ) { continue; } + } + cacheEntry = cacheDict[assetKey]; + if ( !cacheEntry.writeTime ) { continue; } + cacheDict[assetKey].writeTime = 0; + mustSave = true; + } + if ( mustSave ) { + var bin = { assetCacheRegistry: assetCacheRegistry }; + vAPI.cacheStorage.set(bin); + } + if ( typeof callback === 'function' ) { + callback(); + } + }; + if ( typeof exclude === 'function' ) { + callback = exclude; + exclude = undefined; + } + getAssetCacheRegistry(onReady); +}; + +/******************************************************************************/ + +var stringIsNotEmpty = function(s) { + return typeof s === 'string' && s !== ''; +}; + +/******************************************************************************/ + +api.get = function(assetKey, options, callback) { + if ( typeof options === 'function' ) { + callback = options; + options = {}; + } else if ( typeof callback !== 'function' ) { + callback = noopfunc; + } + + var assetDetails = {}, + contentURLs, + contentURL; + + var reportBack = function(content, err) { + var details = { assetKey: assetKey, content: content }; + if ( err ) { + details.error = assetDetails.lastError = err; + } else { + assetDetails.lastError = undefined; + } + callback(details); + }; + + var onContentNotLoaded = function() { + var isExternal; + while ( (contentURL = contentURLs.shift()) ) { + isExternal = reIsExternalPath.test(contentURL); + if ( isExternal === false || assetDetails.hasLocalURL !== true ) { + break; + } + } + if ( !contentURL ) { + return reportBack('', 'E_NOTFOUND'); + } + api.fetchText(contentURL, onContentLoaded, onContentNotLoaded); + }; + + var onContentLoaded = function(details) { + if ( stringIsNotEmpty(details.content) === false ) { + onContentNotLoaded(); + return; + } + if ( reIsExternalPath.test(contentURL) && options.dontCache !== true ) { + assetCacheWrite(assetKey, { + content: details.content, + url: contentURL + }); + } + reportBack(details.content); + }; + + var onCachedContentLoaded = function(details) { + if ( details.content !== '' ) { + return reportBack(details.content); + } + getAssetSourceRegistry(function(registry) { + assetDetails = registry[assetKey] || {}; + if ( typeof assetDetails.contentURL === 'string' ) { + contentURLs = [ assetDetails.contentURL ]; + } else if ( Array.isArray(assetDetails.contentURL) ) { + contentURLs = assetDetails.contentURL.slice(0); + } else { + contentURLs = []; + } + onContentNotLoaded(); + }); + }; + + assetCacheRead(assetKey, onCachedContentLoaded); +}; + +/******************************************************************************/ + +var getRemote = function(assetKey, callback) { + var assetDetails = {}, + contentURLs, + contentURL; + + var reportBack = function(content, err) { + var details = { assetKey: assetKey, content: content }; + if ( err ) { + details.error = assetDetails.lastError = err; + } else { + assetDetails.lastError = undefined; + } + callback(details); + }; + + var onRemoteContentLoaded = function(details) { + if ( stringIsNotEmpty(details.content) === false ) { + registerAssetSource(assetKey, { error: { time: Date.now(), error: 'No content' } }); + tryLoading(); + return; + } + assetCacheWrite(assetKey, { + content: details.content, + url: contentURL + }); + registerAssetSource(assetKey, { error: undefined }); + reportBack(details.content); + }; + + var onRemoteContentError = function(details) { + var text = details.statusText; + if ( details.statusCode === 0 ) { + text = 'network error'; + } + registerAssetSource(assetKey, { error: { time: Date.now(), error: text } }); + tryLoading(); + }; + + var tryLoading = function() { + while ( (contentURL = contentURLs.shift()) ) { + if ( reIsExternalPath.test(contentURL) ) { break; } + } + if ( !contentURL ) { + return reportBack('', 'E_NOTFOUND'); + } + api.fetchText(contentURL, onRemoteContentLoaded, onRemoteContentError); + }; + + getAssetSourceRegistry(function(registry) { + assetDetails = registry[assetKey] || {}; + if ( typeof assetDetails.contentURL === 'string' ) { + contentURLs = [ assetDetails.contentURL ]; + } else if ( Array.isArray(assetDetails.contentURL) ) { + contentURLs = assetDetails.contentURL.slice(0); + } else { + contentURLs = []; + } + tryLoading(); + }); +}; + +/******************************************************************************/ + +api.put = function(assetKey, content, callback) { + assetCacheWrite(assetKey, content, callback); +}; + +/******************************************************************************/ + +api.metadata = function(callback) { + var assetRegistryReady = false, + cacheRegistryReady = false; + + var onReady = function() { + var assetDict = JSON.parse(JSON.stringify(assetSourceRegistry)), + cacheDict = assetCacheRegistry, + assetEntry, cacheEntry, + now = Date.now(), obsoleteAfter; + for ( var assetKey in assetDict ) { + assetEntry = assetDict[assetKey]; + cacheEntry = cacheDict[assetKey]; + if ( cacheEntry ) { + assetEntry.cached = true; + assetEntry.writeTime = cacheEntry.writeTime; + obsoleteAfter = cacheEntry.writeTime + assetEntry.updateAfter * 86400000; + assetEntry.obsolete = obsoleteAfter < now; + assetEntry.remoteURL = cacheEntry.remoteURL; + } else { + assetEntry.writeTime = 0; + obsoleteAfter = 0; + assetEntry.obsolete = true; + } + } + callback(assetDict); + }; + + getAssetSourceRegistry(function() { + assetRegistryReady = true; + if ( cacheRegistryReady ) { onReady(); } + }); + + getAssetCacheRegistry(function() { + cacheRegistryReady = true; + if ( assetRegistryReady ) { onReady(); } + }); +}; + +/******************************************************************************/ + +api.purge = assetCacheMarkAsDirty; + +api.remove = function(pattern, callback) { + assetCacheRemove(pattern, callback); +}; + +api.rmrf = function() { + assetCacheRemove(/./); +}; + +/******************************************************************************/ + +// Asset updater area. +var updaterStatus, + updaterTimer, + updaterAssetDelayDefault = 120000, + updaterAssetDelay = updaterAssetDelayDefault, + updaterUpdated = [], + updaterFetched = new Set(); + +var updateFirst = function() { + updaterStatus = 'updating'; + updaterFetched.clear(); + updaterUpdated = []; + fireNotification('before-assets-updated'); + updateNext(); +}; + +var updateNext = function() { + var assetDict, cacheDict; + + // This will remove a cached asset when it's no longer in use. + var garbageCollectOne = function(assetKey) { + var cacheEntry = cacheDict[assetKey]; + if ( cacheEntry && cacheEntry.readTime < assetCacheRegistryStartTime ) { + assetCacheRemove(assetKey); + } + }; + + var findOne = function() { + var now = Date.now(), + assetEntry, cacheEntry; + for ( var assetKey in assetDict ) { + assetEntry = assetDict[assetKey]; + if ( assetEntry.hasRemoteURL !== true ) { continue; } + if ( updaterFetched.has(assetKey) ) { continue; } + cacheEntry = cacheDict[assetKey]; + if ( cacheEntry && (cacheEntry.writeTime + assetEntry.updateAfter * 86400000) > now ) { + continue; + } + if ( fireNotification('before-asset-updated', { assetKey: assetKey }) !== false ) { + return assetKey; + } + garbageCollectOne(assetKey); + } + }; + + var updatedOne = function(details) { + if ( details.content !== '' ) { + updaterUpdated.push(details.assetKey); + if ( details.assetKey === 'assets.json' ) { + updateAssetSourceRegistry(details.content); + } + } else { + fireNotification('asset-update-failed', { assetKey: details.assetKey }); + } + if ( findOne() !== undefined ) { + vAPI.setTimeout(updateNext, updaterAssetDelay); + } else { + updateDone(); + } + }; + + var updateOne = function() { + var assetKey = findOne(); + if ( assetKey === undefined ) { + return updateDone(); + } + updaterFetched.add(assetKey); + getRemote(assetKey, updatedOne); + }; + + getAssetSourceRegistry(function(dict) { + assetDict = dict; + if ( !cacheDict ) { return; } + updateOne(); + }); + + getAssetCacheRegistry(function(dict) { + cacheDict = dict; + if ( !assetDict ) { return; } + updateOne(); + }); +}; + +var updateDone = function() { + var assetKeys = updaterUpdated.slice(0); + updaterFetched.clear(); + updaterUpdated = []; + updaterStatus = undefined; + updaterAssetDelay = updaterAssetDelayDefault; + fireNotification('after-assets-updated', { assetKeys: assetKeys }); +}; + +api.updateStart = function(details) { + var oldUpdateDelay = updaterAssetDelay, + newUpdateDelay = details.delay || updaterAssetDelayDefault; + updaterAssetDelay = Math.min(oldUpdateDelay, newUpdateDelay); + if ( updaterStatus !== undefined ) { + if ( newUpdateDelay < oldUpdateDelay ) { + clearTimeout(updaterTimer); + updaterTimer = vAPI.setTimeout(updateNext, updaterAssetDelay); + } + return; + } + updateFirst(); +}; + +api.updateStop = function() { + if ( updaterTimer ) { + clearTimeout(updaterTimer); + updaterTimer = undefined; + } + if ( updaterStatus !== undefined ) { + updateDone(); + } +}; + +/******************************************************************************/ + +return api; + +/******************************************************************************/ + +})(); + +/******************************************************************************/ |