123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- // Copyright 2013 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- /**
- * Persistent cache storing images in an indexed database on the hard disk.
- * @constructor
- */
- export function ImageCache() {
- /**
- * IndexedDB database handle.
- * @type {IDBDatabase}
- * @private
- */
- this.db_ = null;
- }
- /**
- * Cache database name.
- * @type {string}
- * @const
- */
- ImageCache.DB_NAME = 'image-loader';
- /**
- * Cache database version.
- * @type {number}
- * @const
- */
- ImageCache.DB_VERSION = 15;
- /**
- * Memory limit for images data in bytes.
- *
- * @const
- * @type {number}
- */
- ImageCache.MEMORY_LIMIT = 250 * 1024 * 1024; // 250 MB.
- /**
- * Minimal amount of memory freed per eviction. Used to limit number of
- * evictions which are expensive.
- *
- * @const
- * @type {number}
- */
- ImageCache.EVICTION_CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB.
- /**
- * Initializes the cache database.
- * @param {function()} callback Completion callback.
- */
- ImageCache.prototype.initialize = function(callback) {
- // Establish a connection to the database or (re)create it if not available
- // or not up to date. After changing the database's schema, increment
- // ImageCache.DB_VERSION to force database recreating.
- const openRequest =
- window.indexedDB.open(ImageCache.DB_NAME, ImageCache.DB_VERSION);
- openRequest.onsuccess = function(e) {
- this.db_ = e.target.result;
- callback();
- }.bind(this);
- openRequest.onerror = callback;
- openRequest.onupgradeneeded = function(e) {
- console.info('Cache database creating or upgrading.');
- const db = e.target.result;
- if (db.objectStoreNames.contains('metadata')) {
- db.deleteObjectStore('metadata');
- }
- if (db.objectStoreNames.contains('data')) {
- db.deleteObjectStore('data');
- }
- if (db.objectStoreNames.contains('settings')) {
- db.deleteObjectStore('settings');
- }
- db.createObjectStore('metadata', {keyPath: 'key'});
- db.createObjectStore('data', {keyPath: 'key'});
- db.createObjectStore('settings', {keyPath: 'key'});
- };
- };
- /**
- * Sets size of the cache.
- *
- * @param {number} size Size in bytes.
- * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
- * provided, then a new one is created.
- * @private
- */
- ImageCache.prototype.setCacheSize_ = function(size, opt_transaction) {
- const transaction =
- opt_transaction || this.db_.transaction(['settings'], 'readwrite');
- const settingsStore = transaction.objectStore('settings');
- settingsStore.put({key: 'size', value: size}); // Update asynchronously.
- };
- /**
- * Fetches current size of the cache.
- *
- * @param {function(number)} onSuccess Callback to return the size.
- * @param {function()} onFailure Failure callback.
- * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
- * provided, then a new one is created.
- * @private
- */
- ImageCache.prototype.fetchCacheSize_ = function(
- onSuccess, onFailure, opt_transaction) {
- const transaction = opt_transaction ||
- this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
- const settingsStore = transaction.objectStore('settings');
- const sizeRequest = settingsStore.get('size');
- sizeRequest.onsuccess = function(e) {
- if (e.target.result) {
- onSuccess(e.target.result.value);
- } else {
- onSuccess(0);
- }
- };
- sizeRequest.onerror = function() {
- console.warn('Failed to fetch size from the database.');
- onFailure();
- };
- };
- /**
- * Evicts the least used elements in cache to make space for a new image and
- * updates size of the cache taking into account the upcoming item.
- *
- * @param {number} size Requested size.
- * @param {function()} onSuccess Success callback.
- * @param {function()} onFailure Failure callback.
- * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
- * provided, then a new one is created.
- * @private
- */
- ImageCache.prototype.evictCache_ = function(
- size, onSuccess, onFailure, opt_transaction) {
- const transaction = opt_transaction ||
- this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
- // Check if the requested size is smaller than the cache size.
- if (size > ImageCache.MEMORY_LIMIT) {
- onFailure();
- return;
- }
- const onCacheSize = function(cacheSize) {
- if (size < ImageCache.MEMORY_LIMIT - cacheSize) {
- // Enough space, no need to evict.
- this.setCacheSize_(cacheSize + size, transaction);
- onSuccess();
- return;
- }
- let bytesToEvict = Math.max(size, ImageCache.EVICTION_CHUNK_SIZE);
- // Fetch all metadata.
- const metadataEntries = [];
- const metadataStore = transaction.objectStore('metadata');
- const dataStore = transaction.objectStore('data');
- const onEntriesFetched = function() {
- metadataEntries.sort(function(a, b) {
- return b.lastLoadTimestamp - a.lastLoadTimestamp;
- });
- let totalEvicted = 0;
- while (bytesToEvict > 0) {
- const entry = metadataEntries.pop();
- totalEvicted += entry.size;
- bytesToEvict -= entry.size;
- metadataStore.delete(entry.key); // Remove asynchronously.
- dataStore.delete(entry.key); // Remove asynchronously.
- }
- this.setCacheSize_(cacheSize - totalEvicted + size, transaction);
- }.bind(this);
- metadataStore.openCursor().onsuccess = function(e) {
- const cursor = e.target.result;
- if (cursor) {
- metadataEntries.push(cursor.value);
- cursor.continue();
- } else {
- onEntriesFetched();
- }
- };
- }.bind(this);
- this.fetchCacheSize_(onCacheSize, onFailure, transaction);
- };
- /**
- * Saves an image in the cache.
- *
- * @param {string} key Cache key.
- * @param {number} timestamp Last modification timestamp. Used to detect
- * if the image cache entry is out of date.
- * @param {number} width Image width.
- * @param {number} height Image height.
- * @param {?string} ifd Image ifd, null if none.
- * @param {string} data Image data.
- */
- ImageCache.prototype.saveImage = function(
- key, timestamp, width, height, ifd, data) {
- if (!this.db_) {
- console.warn('Cache database not available.');
- return;
- }
- const onNotFoundInCache = function() {
- const metadataEntry = {
- key: key,
- timestamp: timestamp,
- width: width,
- height: height,
- ifd: ifd,
- size: data.length,
- lastLoadTimestamp: Date.now(),
- };
- const dataEntry = {key: key, data: data};
- const transaction =
- this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
- const metadataStore = transaction.objectStore('metadata');
- const dataStore = transaction.objectStore('data');
- const onCacheEvicted = function() {
- metadataStore.put(metadataEntry); // Add asynchronously.
- dataStore.put(dataEntry); // Add asynchronously.
- };
- // Make sure there is enough space in the cache.
- this.evictCache_(data.length, onCacheEvicted, function() {}, transaction);
- }.bind(this);
- // Check if the image is already in cache. If not, then save it to cache.
- this.loadImage(key, timestamp, function() {}, onNotFoundInCache);
- };
- /**
- * Loads an image from the cache.
- *
- * @param {string} key Cache key.
- * @param {number} timestamp Last modification timestamp. If different
- * than the one in cache, then the entry will be invalidated.
- * @param {function(number, number, ?string, string)} onSuccess Success
- * callback with the image width, height, ?ifd, and data.
- * @param {function()} onFailure Failure callback.
- */
- ImageCache.prototype.loadImage = function(
- key, timestamp, onSuccess, onFailure) {
- if (!this.db_) {
- console.warn('Cache database not available.');
- onFailure();
- return;
- }
- const transaction =
- this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
- const metadataStore = transaction.objectStore('metadata');
- const dataStore = transaction.objectStore('data');
- const metadataRequest = metadataStore.get(key);
- const dataRequest = dataStore.get(key);
- let metadataEntry = null;
- let metadataReceived = false;
- let dataEntry = null;
- let dataReceived = false;
- const onPartialSuccess = function() {
- // Check if all sub-requests have finished.
- if (!metadataReceived || !dataReceived) {
- return;
- }
- // Check if both entries are available or both unavailable.
- if (!!metadataEntry != !!dataEntry) {
- console.warn('Inconsistent cache database.');
- onFailure();
- return;
- }
- // Process the responses.
- if (!metadataEntry) {
- // The image not found.
- onFailure();
- } else if (metadataEntry.timestamp != timestamp) {
- // The image is not up to date, so remove it.
- this.removeImage(key, function() {}, function() {}, transaction);
- onFailure();
- } else {
- // The image is available. Update the last load time and return the
- // image data.
- metadataEntry.lastLoadTimestamp = Date.now();
- metadataStore.put(metadataEntry); // Added asynchronously.
- onSuccess(
- metadataEntry.width, metadataEntry.height, metadataEntry.ifd,
- dataEntry.data);
- }
- }.bind(this);
- metadataRequest.onsuccess = function(e) {
- if (e.target.result) {
- metadataEntry = e.target.result;
- }
- metadataReceived = true;
- onPartialSuccess();
- };
- dataRequest.onsuccess = function(e) {
- if (e.target.result) {
- dataEntry = e.target.result;
- }
- dataReceived = true;
- onPartialSuccess();
- };
- metadataRequest.onerror = function() {
- console.warn('Failed to fetch metadata from the database.');
- metadataReceived = true;
- onPartialSuccess();
- };
- dataRequest.onerror = function() {
- console.warn('Failed to fetch image data from the database.');
- dataReceived = true;
- onPartialSuccess();
- };
- };
- /**
- * Removes the image from the cache.
- *
- * @param {string} key Cache key.
- * @param {function()=} opt_onSuccess Success callback.
- * @param {function()=} opt_onFailure Failure callback.
- * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
- * provided, then a new one is created.
- */
- ImageCache.prototype.removeImage = function(
- key, opt_onSuccess, opt_onFailure, opt_transaction) {
- if (!this.db_) {
- console.warn('Cache database not available.');
- return;
- }
- const transaction = opt_transaction ||
- this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
- const metadataStore = transaction.objectStore('metadata');
- const dataStore = transaction.objectStore('data');
- let cacheSize = null;
- let cacheSizeReceived = false;
- let metadataEntry = null;
- let metadataReceived = false;
- const onPartialSuccess = function() {
- if (!cacheSizeReceived || !metadataReceived) {
- return;
- }
- // If either cache size or metadata entry is not available, then it is
- // an error.
- if (cacheSize === null || !metadataEntry) {
- if (opt_onFailure) {
- opt_onFailure();
- }
- return;
- }
- if (opt_onSuccess) {
- opt_onSuccess();
- }
- this.setCacheSize_(cacheSize - metadataEntry.size, transaction);
- metadataStore.delete(key); // Delete asynchronously.
- dataStore.delete(key); // Delete asynchronously.
- }.bind(this);
- const onCacheSizeFailure = function() {
- cacheSizeReceived = true;
- };
- const onCacheSizeSuccess = function(result) {
- cacheSize = result;
- cacheSizeReceived = true;
- onPartialSuccess();
- };
- // Fetch the current cache size.
- this.fetchCacheSize_(onCacheSizeSuccess, onCacheSizeFailure, transaction);
- // Receive image's metadata.
- const metadataRequest = metadataStore.get(key);
- metadataRequest.onsuccess = function(e) {
- if (e.target.result) {
- metadataEntry = e.target.result;
- }
- metadataReceived = true;
- onPartialSuccess();
- };
- metadataRequest.onerror = function() {
- console.warn('Failed to remove an image.');
- metadataReceived = true;
- onPartialSuccess();
- };
- };
|