1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * Persistent cache storing images in an indexed database on the hard disk.
11 * IndexedDB database handle.
19 * Cache database name.
23 Cache.DB_NAME = 'image-loader';
26 * Cache database version.
30 Cache.DB_VERSION = 11;
33 * Memory limit for images data in bytes.
38 Cache.MEMORY_LIMIT = 250 * 1024 * 1024; // 250 MB.
41 * Minimal amount of memory freed per eviction. Used to limit number of
42 * evictions which are expensive.
47 Cache.EVICTION_CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB.
50 * Creates a cache key.
52 * @param {Object} request Request options.
53 * @return {?string} Cache key. It may be null if the cache does not support
54 * |request|. e.g. Data URI.
56 Cache.createKey = function(request) {
57 if (/^data:/i.test(request.url))
59 return JSON.stringify({
63 height: request.height,
64 maxWidth: request.maxWidth,
65 maxHeight: request.maxHeight});
69 * Initializes the cache database.
70 * @param {function()} callback Completion callback.
72 Cache.prototype.initialize = function(callback) {
73 // Establish a connection to the database or (re)create it if not available
74 // or not up to date. After changing the database's schema, increment
75 // Cache.DB_VERSION to force database recreating.
76 var openRequest = window.indexedDB.open(Cache.DB_NAME, Cache.DB_VERSION);
78 openRequest.onsuccess = function(e) {
79 this.db_ = e.target.result;
83 openRequest.onerror = callback;
85 openRequest.onupgradeneeded = function(e) {
86 console.info('Cache database creating or upgrading.');
87 var db = e.target.result;
88 if (db.objectStoreNames.contains('metadata'))
89 db.deleteObjectStore('metadata');
90 if (db.objectStoreNames.contains('data'))
91 db.deleteObjectStore('data');
92 if (db.objectStoreNames.contains('settings'))
93 db.deleteObjectStore('settings');
94 db.createObjectStore('metadata', {keyPath: 'key'});
95 db.createObjectStore('data', {keyPath: 'key'});
96 db.createObjectStore('settings', {keyPath: 'key'});
101 * Sets size of the cache.
103 * @param {number} size Size in bytes.
104 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
105 * provided, then a new one is created.
108 Cache.prototype.setCacheSize_ = function(size, opt_transaction) {
109 var transaction = opt_transaction ||
110 this.db_.transaction(['settings'], 'readwrite');
111 var settingsStore = transaction.objectStore('settings');
113 settingsStore.put({key: 'size', value: size}); // Update asynchronously.
117 * Fetches current size of the cache.
119 * @param {function(number)} onSuccess Callback to return the size.
120 * @param {function()} onFailure Failure callback.
121 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
122 * provided, then a new one is created.
125 Cache.prototype.fetchCacheSize_ = function(
126 onSuccess, onFailure, opt_transaction) {
127 var transaction = opt_transaction ||
128 this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
129 var settingsStore = transaction.objectStore('settings');
130 var sizeRequest = settingsStore.get('size');
132 sizeRequest.onsuccess = function(e) {
134 onSuccess(e.target.result.value);
139 sizeRequest.onerror = function() {
140 console.error('Failed to fetch size from the database.');
146 * Evicts the least used elements in cache to make space for a new image and
147 * updates size of the cache taking into account the upcoming item.
149 * @param {number} size Requested size.
150 * @param {function()} onSuccess Success callback.
151 * @param {function()} onFailure Failure callback.
152 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
153 * provided, then a new one is created.
156 Cache.prototype.evictCache_ = function(
157 size, onSuccess, onFailure, opt_transaction) {
158 var transaction = opt_transaction ||
159 this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
161 // Check if the requested size is smaller than the cache size.
162 if (size > Cache.MEMORY_LIMIT) {
167 var onCacheSize = function(cacheSize) {
168 if (size < Cache.MEMORY_LIMIT - cacheSize) {
169 // Enough space, no need to evict.
170 this.setCacheSize_(cacheSize + size, transaction);
175 var bytesToEvict = Math.max(size, Cache.EVICTION_CHUNK_SIZE);
177 // Fetch all metadata.
178 var metadataEntries = [];
179 var metadataStore = transaction.objectStore('metadata');
180 var dataStore = transaction.objectStore('data');
182 var onEntriesFetched = function() {
183 metadataEntries.sort(function(a, b) {
184 return b.lastLoadTimestamp - a.lastLoadTimestamp;
187 var totalEvicted = 0;
188 while (bytesToEvict > 0) {
189 var entry = metadataEntries.pop();
190 totalEvicted += entry.size;
191 bytesToEvict -= entry.size;
192 metadataStore.delete(entry.key); // Remove asynchronously.
193 dataStore.delete(entry.key); // Remove asynchronously.
196 this.setCacheSize_(cacheSize - totalEvicted + size, transaction);
199 metadataStore.openCursor().onsuccess = function(e) {
200 var cursor = e.target.result;
202 metadataEntries.push(cursor.value);
210 this.fetchCacheSize_(onCacheSize, onFailure, transaction);
214 * Saves an image in the cache.
216 * @param {string} key Cache key.
217 * @param {string} data Image data.
218 * @param {number} timestamp Last modification timestamp. Used to detect
219 * if the cache entry becomes out of date.
221 Cache.prototype.saveImage = function(key, data, timestamp) {
223 console.warn('Cache database not available.');
227 var onNotFoundInCache = function() {
228 var metadataEntry = {
230 timestamp: timestamp,
232 lastLoadTimestamp: Date.now()};
233 var dataEntry = {key: key, data: data};
235 var transaction = this.db_.transaction(['settings', 'metadata', 'data'],
237 var metadataStore = transaction.objectStore('metadata');
238 var dataStore = transaction.objectStore('data');
240 var onCacheEvicted = function() {
241 metadataStore.put(metadataEntry); // Add asynchronously.
242 dataStore.put(dataEntry); // Add asynchronously.
245 // Make sure there is enough space in the cache.
246 this.evictCache_(data.length, onCacheEvicted, function() {}, transaction);
249 // Check if the image is already in cache. If not, then save it to cache.
250 this.loadImage(key, timestamp, function() {}, onNotFoundInCache);
254 * Loads an image from the cache (if available) or returns null.
256 * @param {string} key Cache key.
257 * @param {number} timestamp Last modification timestamp. If different
258 * that the one in cache, then the entry will be invalidated.
259 * @param {function(string)} onSuccess Success callback with the image's data.
260 * @param {function()} onFailure Failure callback.
262 Cache.prototype.loadImage = function(key, timestamp, onSuccess, onFailure) {
264 console.warn('Cache database not available.');
269 var transaction = this.db_.transaction(['settings', 'metadata', 'data'],
271 var metadataStore = transaction.objectStore('metadata');
272 var dataStore = transaction.objectStore('data');
273 var metadataRequest = metadataStore.get(key);
274 var dataRequest = dataStore.get(key);
276 var metadataEntry = null;
277 var metadataReceived = false;
278 var dataEntry = null;
279 var dataReceived = false;
281 var onPartialSuccess = function() {
282 // Check if all sub-requests have finished.
283 if (!metadataReceived || !dataReceived)
286 // Check if both entries are available or both unavailable.
287 if (!!metadataEntry != !!dataEntry) {
288 console.warn('Inconsistent cache database.');
293 // Process the responses.
294 if (!metadataEntry) {
295 // The image not found.
297 } else if (metadataEntry.timestamp != timestamp) {
298 // The image is not up to date, so remove it.
299 this.removeImage(key, function() {}, function() {}, transaction);
302 // The image is available. Update the last load time and return the
304 metadataEntry.lastLoadTimestamp = Date.now();
305 metadataStore.put(metadataEntry); // Added asynchronously.
306 onSuccess(dataEntry.data);
310 metadataRequest.onsuccess = function(e) {
312 metadataEntry = e.target.result;
313 metadataReceived = true;
317 dataRequest.onsuccess = function(e) {
319 dataEntry = e.target.result;
324 metadataRequest.onerror = function() {
325 console.error('Failed to fetch metadata from the database.');
326 metadataReceived = true;
330 dataRequest.onerror = function() {
331 console.error('Failed to fetch image data from the database.');
338 * Removes the image from the cache.
340 * @param {string} key Cache key.
341 * @param {function()=} opt_onSuccess Success callback.
342 * @param {function()=} opt_onFailure Failure callback.
343 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
344 * provided, then a new one is created.
346 Cache.prototype.removeImage = function(
347 key, opt_onSuccess, opt_onFailure, opt_transaction) {
349 console.warn('Cache database not available.');
353 var transaction = opt_transaction ||
354 this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
355 var metadataStore = transaction.objectStore('metadata');
356 var dataStore = transaction.objectStore('data');
358 var cacheSize = null;
359 var cacheSizeReceived = false;
360 var metadataEntry = null;
361 var metadataReceived = false;
363 var onPartialSuccess = function() {
364 if (!cacheSizeReceived || !metadataReceived)
367 // If either cache size or metadata entry is not available, then it is
369 if (cacheSize === null || !metadataEntry) {
378 this.setCacheSize_(cacheSize - metadataEntry.size, transaction);
379 metadataStore.delete(key); // Delete asynchronously.
380 dataStore.delete(key); // Delete asynchronously.
383 var onCacheSizeFailure = function() {
384 cacheSizeReceived = true;
387 var onCacheSizeSuccess = function(result) {
389 cacheSizeReceived = true;
393 // Fetch the current cache size.
394 this.fetchCacheSize_(onCacheSizeSuccess, onCacheSizeFailure, transaction);
396 // Receive image's metadata.
397 var metadataRequest = metadataStore.get(key);
399 metadataRequest.onsuccess = function(e) {
401 metadataEntry = e.target.result;
402 metadataReceived = true;
406 metadataRequest.onerror = function() {
407 console.error('Failed to remove an image.');
408 metadataReceived = true;