Fix infinite recursion on hiding panel when created during fullscreen mode.
[chromium-blink-merge.git] / chrome / browser / resources / image_loader / cache.js
blobdde287c1a0a63285d0d647d40ffe6a5c8247f312
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.
5 'use strict';
7 /**
8 * Persistent cache storing images in an indexed database on the hard disk.
9 * @constructor
11 function Cache() {
12 /**
13 * IndexedDB database handle.
14 * @type {IDBDatabase}
15 * @private
17 this.db_ = null;
20 /**
21 * Cache database name.
22 * @type {string}
23 * @const
25 Cache.DB_NAME = 'image-loader';
27 /**
28 * Cache database version.
29 * @type {number}
30 * @const
32 Cache.DB_VERSION = 11;
34 /**
35 * Memory limit for images data in bytes.
37 * @const
38 * @type {number}
40 Cache.MEMORY_LIMIT = 250 * 1024 * 1024; // 250 MB.
42 /**
43 * Minimal amount of memory freed per eviction. Used to limit number of
44 * evictions which are expensive.
46 * @const
47 * @type {number}
49 Cache.EVICTION_CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB.
51 /**
52 * Creates a cache key.
54 * @param {Object} request Request options.
55 * @return {string} Cache key.
57 Cache.createKey = function(request) {
58 return JSON.stringify({url: request.url,
59 scale: request.scale,
60 width: request.width,
61 height: request.height,
62 maxWidth: request.maxWidth,
63 maxHeight: request.maxHeight});
66 /**
67 * Initializes the cache database.
68 * @param {function()} callback Completion callback.
70 Cache.prototype.initialize = function(callback) {
71 // Establish a connection to the database or (re)create it if not available
72 // or not up to date. After changing the database's schema, increment
73 // Cache.DB_VERSION to force database recreating.
74 var openRequest = window.webkitIndexedDB.open(Cache.DB_NAME,
75 Cache.DB_VERSION);
77 openRequest.onsuccess = function(e) {
78 this.db_ = e.target.result;
79 callback();
80 }.bind(this);
82 openRequest.onerror = callback;
84 openRequest.onupgradeneeded = function(e) {
85 console.info('Cache database creating or upgrading.');
86 var db = e.target.result;
87 if (db.objectStoreNames.contains('metadata'))
88 db.deleteObjectStore('metadata');
89 if (db.objectStoreNames.contains('data'))
90 db.deleteObjectStore('data');
91 if (db.objectStoreNames.contains('settings'))
92 db.deleteObjectStore('settings');
93 db.createObjectStore('metadata', {keyPath: 'key'});
94 db.createObjectStore('data', {keyPath: 'key'});
95 db.createObjectStore('settings', {keyPath: 'key'});
99 /**
100 * Sets size of the cache.
102 * @param {number} size Size in bytes.
103 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
104 * provided, then a new one is created.
105 * @private
107 Cache.prototype.setCacheSize_ = function(size, opt_transaction) {
108 var transaction = opt_transaction ||
109 this.db_.transaction(['settings'], 'readwrite');
110 var settingsStore = transaction.objectStore('settings');
112 settingsStore.put({key: 'size', value: size}); // Update asynchronously.
116 * Fetches current size of the cache.
118 * @param {function(number)} onSuccess Callback to return the size.
119 * @param {function()} onFailure Failure callback.
120 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
121 * provided, then a new one is created.
122 * @private
124 Cache.prototype.fetchCacheSize_ = function(
125 onSuccess, onFailure, opt_transaction) {
126 var transaction = opt_transaction ||
127 this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
128 var settingsStore = transaction.objectStore('settings');
129 var sizeRequest = settingsStore.get('size');
131 sizeRequest.onsuccess = function(e) {
132 if (e.target.result)
133 onSuccess(e.target.result.value);
134 else
135 onSuccess(0);
138 sizeRequest.onerror = function() {
139 console.error('Failed to fetch size from the database.');
140 onFailure();
145 * Evicts the least used elements in cache to make space for a new image and
146 * updates size of the cache taking into account the upcoming item.
148 * @param {number} size Requested size.
149 * @param {function()} onSuccess Success callback.
150 * @param {function()} onFailure Failure callback.
151 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
152 * provided, then a new one is created.
153 * @private
155 Cache.prototype.evictCache_ = function(
156 size, onSuccess, onFailure, opt_transaction) {
157 var transaction = opt_transaction ||
158 this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
160 // Check if the requested size is smaller than the cache size.
161 if (size > Cache.MEMORY_LIMIT) {
162 onFailure();
163 return;
166 var onCacheSize = function(cacheSize) {
167 if (size < Cache.MEMORY_LIMIT - cacheSize) {
168 // Enough space, no need to evict.
169 this.setCacheSize_(cacheSize + size, transaction);
170 onSuccess();
171 return;
174 var bytesToEvict = Math.max(size, Cache.EVICTION_CHUNK_SIZE);
176 // Fetch all metadata.
177 var metadataEntries = [];
178 var metadataStore = transaction.objectStore('metadata');
179 var dataStore = transaction.objectStore('data');
181 var onEntriesFetched = function() {
182 metadataEntries.sort(function(a, b) {
183 return b.lastLoadTimestamp - a.lastLoadTimestamp;
186 var totalEvicted = 0;
187 while (bytesToEvict > 0) {
188 var entry = metadataEntries.pop();
189 totalEvicted += entry.size;
190 bytesToEvict -= entry.size;
191 metadataStore.delete(entry.key); // Remove asynchronously.
192 dataStore.delete(entry.key); // Remove asynchronously.
195 this.setCacheSize_(cacheSize - totalEvicted + size, transaction);
196 }.bind(this);
198 metadataStore.openCursor().onsuccess = function(e) {
199 var cursor = event.target.result;
200 if (cursor) {
201 metadataEntries.push(cursor.value);
202 cursor.continue();
203 } else {
204 onEntriesFetched();
207 }.bind(this);
209 this.fetchCacheSize_(onCacheSize, onFailure, transaction);
213 * Saves an image in the cache.
215 * @param {string} key Cache key.
216 * @param {string} data Image data.
217 * @param {number} timestamp Last modification timestamp. Used to detect
218 * if the cache entry becomes out of date.
220 Cache.prototype.saveImage = function(key, data, timestamp) {
221 if (!this.db_) {
222 console.warn('Cache database not available.');
223 return;
226 var onNotFoundInCache = function() {
227 var metadataEntry = {key: key,
228 timestamp: timestamp,
229 size: data.length,
230 lastLoadTimestamp: Date.now()};
231 var dataEntry = {key: key,
232 data: data};
234 var transaction = this.db_.transaction(['settings', 'metadata', 'data'],
235 'readwrite');
236 var metadataStore = transaction.objectStore('metadata');
237 var dataStore = transaction.objectStore('data');
239 var onCacheEvicted = function() {
240 metadataStore.put(metadataEntry); // Add asynchronously.
241 dataStore.put(dataEntry); // Add asynchronously.
244 // Make sure there is enough space in the cache.
245 this.evictCache_(data.length, onCacheEvicted, function() {}, transaction);
246 }.bind(this);
248 // Check if the image is already in cache. If not, then save it to cache.
249 this.loadImage(key, timestamp, function() {}, onNotFoundInCache);
253 * Loads an image from the cache (if available) or returns null.
255 * @param {string} key Cache key.
256 * @param {number} timestamp Last modification timestamp. If different
257 * that the one in cache, then the entry will be invalidated.
258 * @param {function(<string>)} onSuccess Success callback with the image's data.
259 * @param {function()} onFailure Failure callback.
261 Cache.prototype.loadImage = function(key, timestamp, onSuccess, onFailure) {
262 if (!this.db_) {
263 console.warn('Cache database not available.');
264 onFailure();
265 return;
268 var transaction = this.db_.transaction(['settings', 'metadata', 'data'],
269 'readwrite');
270 var metadataStore = transaction.objectStore('metadata');
271 var dataStore = transaction.objectStore('data');
272 var metadataRequest = metadataStore.get(key);
273 var dataRequest = dataStore.get(key);
275 var metadataEntry = null;
276 var metadataReceived = false;
277 var dataEntry = null;
278 var dataReceived = false;
280 var onPartialSuccess = function() {
281 // Check if all sub-requests have finished.
282 if (!metadataReceived || !dataReceived)
283 return;
285 // Check if both entries are available or both unavailable.
286 if (!!metadataEntry != !!dataEntry) {
287 console.warn('Incosistent cache database.');
288 onFailure();
289 return;
292 // Process the responses.
293 if (!metadataEntry) {
294 // The image not found.
295 onFailure();
296 } else if (metadataEntry.timestamp != timestamp) {
297 // The image is not up to date, so remove it.
298 this.removeImage(key, function() {}, function() {}, transaction);
299 onFailure();
300 } else {
301 // The image is available. Update the last load time and return the
302 // image data.
303 metadataEntry.lastLoadTimestamp = Date.now();
304 metadataStore.put(metadataEntry); // Added asynchronously.
305 onSuccess(dataEntry.data);
307 }.bind(this);
309 metadataRequest.onsuccess = function(e) {
310 if (e.target.result)
311 metadataEntry = e.target.result;
312 metadataReceived = true;
313 onPartialSuccess();
316 dataRequest.onsuccess = function(e) {
317 if (e.target.result)
318 dataEntry = e.target.result;
319 dataReceived = true;
320 onPartialSuccess();
323 metadataRequest.onerror = function() {
324 console.error('Failed to fetch metadata from the database.');
325 metadataReceived = true;
326 onPartialSuccess();
329 dataRequest.onerror = function() {
330 console.error('Failed to fetch image data from the database.');
331 dataReceived = true;
332 onPartialSuccess();
337 * Removes the image from the cache.
339 * @param {string} key Cache key.
340 * @param {function()=} opt_onSuccess Success callback.
341 * @param {function()=} opt_onFailure Failure callback.
342 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
343 * provided, then a new one is created.
345 Cache.prototype.removeImage = function(
346 key, opt_onSuccess, opt_onFailure, opt_transaction) {
347 if (!this.db_) {
348 console.warn('Cache database not available.');
349 return;
352 var transaction = opt_transaction ||
353 this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
354 var metadataStore = transaction.objectStore('metadata');
355 var dataStore = transaction.objectStore('data');
357 var cacheSize = null;
358 var cacheSizeReceived = false;
359 var metadataEntry = null;
360 var metadataReceived = false;
362 var onPartialSuccess = function() {
363 if (!cacheSizeReceived || !metadataReceived)
364 return;
366 // If either cache size or metadata entry is not available, then it is
367 // an error.
368 if (cacheSize === null || !metadataEntry) {
369 if (opt_onFailure)
370 onFailure();
371 return;
374 if (opt_onSuccess)
375 opt_onSuccess();
377 this.setCacheSize_(cacheSize - metadataEntry.size, transaction);
378 metadataStore.delete(key); // Delete asynchronously.
379 dataStore.delete(key); // Delete asynchronously.
380 }.bind(this);
382 var onCacheSizeFailure = function() {
383 cacheSizeReceived = true;
386 var onCacheSizeSuccess = function(result) {
387 cacheSize = result;
388 cacheSizeReceived = true;
389 onPartialSuccess();
392 // Fetch the current cache size.
393 this.fetchCacheSize_(onCacheSizeSuccess, onCacheSizeFailure, transaction);
395 // Receive image's metadata.
396 var metadataRequest = metadataStore.get(key);
398 metadataRequest.onsuccess = function(e) {
399 if (e.target.result)
400 metadataEntry = e.target.result;
401 metadataReceived = true;
402 onPartialSuccess();
405 metadataRequest.onerror = function() {
406 console.error('Failed to remove an image.');
407 metadataReceived = true;
408 onPartialSuccess();