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.
9 function ImageCache() {
11 * IndexedDB database handle.
19 * Cache database name.
23 ImageCache
.DB_NAME
= 'image-loader';
26 * Cache database version.
30 ImageCache
.DB_VERSION
= 12;
33 * Memory limit for images data in bytes.
38 ImageCache
.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 ImageCache
.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 ImageCache
.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 ImageCache
.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 // ImageCache.DB_VERSION to force database recreating.
76 var openRequest
= window
.indexedDB
.open(
77 ImageCache
.DB_NAME
, ImageCache
.DB_VERSION
);
79 openRequest
.onsuccess = function(e
) {
80 this.db_
= e
.target
.result
;
84 openRequest
.onerror
= callback
;
86 openRequest
.onupgradeneeded = function(e
) {
87 console
.info('Cache database creating or upgrading.');
88 var db
= e
.target
.result
;
89 if (db
.objectStoreNames
.contains('metadata'))
90 db
.deleteObjectStore('metadata');
91 if (db
.objectStoreNames
.contains('data'))
92 db
.deleteObjectStore('data');
93 if (db
.objectStoreNames
.contains('settings'))
94 db
.deleteObjectStore('settings');
95 db
.createObjectStore('metadata', {keyPath
: 'key'});
96 db
.createObjectStore('data', {keyPath
: 'key'});
97 db
.createObjectStore('settings', {keyPath
: 'key'});
102 * Sets size of the cache.
104 * @param {number} size Size in bytes.
105 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
106 * provided, then a new one is created.
109 ImageCache
.prototype.setCacheSize_ = function(size
, opt_transaction
) {
110 var transaction
= opt_transaction
||
111 this.db_
.transaction(['settings'], 'readwrite');
112 var settingsStore
= transaction
.objectStore('settings');
114 settingsStore
.put({key
: 'size', value
: size
}); // Update asynchronously.
118 * Fetches current size of the cache.
120 * @param {function(number)} onSuccess Callback to return the size.
121 * @param {function()} onFailure Failure callback.
122 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
123 * provided, then a new one is created.
126 ImageCache
.prototype.fetchCacheSize_ = function(
127 onSuccess
, onFailure
, opt_transaction
) {
128 var transaction
= opt_transaction
||
129 this.db_
.transaction(['settings', 'metadata', 'data'], 'readwrite');
130 var settingsStore
= transaction
.objectStore('settings');
131 var sizeRequest
= settingsStore
.get('size');
133 sizeRequest
.onsuccess = function(e
) {
135 onSuccess(e
.target
.result
.value
);
140 sizeRequest
.onerror = function() {
141 console
.error('Failed to fetch size from the database.');
147 * Evicts the least used elements in cache to make space for a new image and
148 * updates size of the cache taking into account the upcoming item.
150 * @param {number} size Requested size.
151 * @param {function()} onSuccess Success callback.
152 * @param {function()} onFailure Failure callback.
153 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
154 * provided, then a new one is created.
157 ImageCache
.prototype.evictCache_ = function(
158 size
, onSuccess
, onFailure
, opt_transaction
) {
159 var transaction
= opt_transaction
||
160 this.db_
.transaction(['settings', 'metadata', 'data'], 'readwrite');
162 // Check if the requested size is smaller than the cache size.
163 if (size
> ImageCache
.MEMORY_LIMIT
) {
168 var onCacheSize = function(cacheSize
) {
169 if (size
< ImageCache
.MEMORY_LIMIT
- cacheSize
) {
170 // Enough space, no need to evict.
171 this.setCacheSize_(cacheSize
+ size
, transaction
);
176 var bytesToEvict
= Math
.max(size
, ImageCache
.EVICTION_CHUNK_SIZE
);
178 // Fetch all metadata.
179 var metadataEntries
= [];
180 var metadataStore
= transaction
.objectStore('metadata');
181 var dataStore
= transaction
.objectStore('data');
183 var onEntriesFetched = function() {
184 metadataEntries
.sort(function(a
, b
) {
185 return b
.lastLoadTimestamp
- a
.lastLoadTimestamp
;
188 var totalEvicted
= 0;
189 while (bytesToEvict
> 0) {
190 var entry
= metadataEntries
.pop();
191 totalEvicted
+= entry
.size
;
192 bytesToEvict
-= entry
.size
;
193 metadataStore
.delete(entry
.key
); // Remove asynchronously.
194 dataStore
.delete(entry
.key
); // Remove asynchronously.
197 this.setCacheSize_(cacheSize
- totalEvicted
+ size
, transaction
);
200 metadataStore
.openCursor().onsuccess = function(e
) {
201 var cursor
= e
.target
.result
;
203 metadataEntries
.push(cursor
.value
);
211 this.fetchCacheSize_(onCacheSize
, onFailure
, transaction
);
215 * Saves an image in the cache.
217 * @param {string} key Cache key.
218 * @param {string} data Image data.
219 * @param {number} width Image width.
220 * @param {number} height Image height.
221 * @param {number} timestamp Last modification timestamp. Used to detect
222 * if the cache entry becomes out of date.
224 ImageCache
.prototype.saveImage = function(key
, data
, width
, height
, timestamp
) {
226 console
.warn('Cache database not available.');
230 var onNotFoundInCache = function() {
231 var metadataEntry
= {
233 timestamp
: timestamp
,
237 lastLoadTimestamp
: Date
.now()};
238 var dataEntry
= {key
: key
, data
: data
};
240 var transaction
= this.db_
.transaction(['settings', 'metadata', 'data'],
242 var metadataStore
= transaction
.objectStore('metadata');
243 var dataStore
= transaction
.objectStore('data');
245 var onCacheEvicted = function() {
246 metadataStore
.put(metadataEntry
); // Add asynchronously.
247 dataStore
.put(dataEntry
); // Add asynchronously.
250 // Make sure there is enough space in the cache.
251 this.evictCache_(data
.length
, onCacheEvicted
, function() {}, transaction
);
254 // Check if the image is already in cache. If not, then save it to cache.
255 this.loadImage(key
, timestamp
, function() {}, onNotFoundInCache
);
259 * Loads an image from the cache (if available) or returns null.
261 * @param {string} key Cache key.
262 * @param {number} timestamp Last modification timestamp. If different
263 * that the one in cache, then the entry will be invalidated.
264 * @param {function(string, number, number)} onSuccess Success callback with
265 * the image's data, width, height.
266 * @param {function()} onFailure Failure callback.
268 ImageCache
.prototype.loadImage = function(
269 key
, timestamp
, onSuccess
, onFailure
) {
271 console
.warn('Cache database not available.');
276 var transaction
= this.db_
.transaction(['settings', 'metadata', 'data'],
278 var metadataStore
= transaction
.objectStore('metadata');
279 var dataStore
= transaction
.objectStore('data');
280 var metadataRequest
= metadataStore
.get(key
);
281 var dataRequest
= dataStore
.get(key
);
283 var metadataEntry
= null;
284 var metadataReceived
= false;
285 var dataEntry
= null;
286 var dataReceived
= false;
288 var onPartialSuccess = function() {
289 // Check if all sub-requests have finished.
290 if (!metadataReceived
|| !dataReceived
)
293 // Check if both entries are available or both unavailable.
294 if (!!metadataEntry
!= !!dataEntry
) {
295 console
.warn('Inconsistent cache database.');
300 // Process the responses.
301 if (!metadataEntry
) {
302 // The image not found.
304 } else if (metadataEntry
.timestamp
!= timestamp
) {
305 // The image is not up to date, so remove it.
306 this.removeImage(key
, function() {}, function() {}, transaction
);
309 // The image is available. Update the last load time and return the
311 metadataEntry
.lastLoadTimestamp
= Date
.now();
312 metadataStore
.put(metadataEntry
); // Added asynchronously.
313 onSuccess(dataEntry
.data
, metadataEntry
.width
, metadataEntry
.height
);
317 metadataRequest
.onsuccess = function(e
) {
319 metadataEntry
= e
.target
.result
;
320 metadataReceived
= true;
324 dataRequest
.onsuccess = function(e
) {
326 dataEntry
= e
.target
.result
;
331 metadataRequest
.onerror = function() {
332 console
.error('Failed to fetch metadata from the database.');
333 metadataReceived
= true;
337 dataRequest
.onerror = function() {
338 console
.error('Failed to fetch image data from the database.');
345 * Removes the image from the cache.
347 * @param {string} key Cache key.
348 * @param {function()=} opt_onSuccess Success callback.
349 * @param {function()=} opt_onFailure Failure callback.
350 * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
351 * provided, then a new one is created.
353 ImageCache
.prototype.removeImage = function(
354 key
, opt_onSuccess
, opt_onFailure
, opt_transaction
) {
356 console
.warn('Cache database not available.');
360 var transaction
= opt_transaction
||
361 this.db_
.transaction(['settings', 'metadata', 'data'], 'readwrite');
362 var metadataStore
= transaction
.objectStore('metadata');
363 var dataStore
= transaction
.objectStore('data');
365 var cacheSize
= null;
366 var cacheSizeReceived
= false;
367 var metadataEntry
= null;
368 var metadataReceived
= false;
370 var onPartialSuccess = function() {
371 if (!cacheSizeReceived
|| !metadataReceived
)
374 // If either cache size or metadata entry is not available, then it is
376 if (cacheSize
=== null || !metadataEntry
) {
385 this.setCacheSize_(cacheSize
- metadataEntry
.size
, transaction
);
386 metadataStore
.delete(key
); // Delete asynchronously.
387 dataStore
.delete(key
); // Delete asynchronously.
390 var onCacheSizeFailure = function() {
391 cacheSizeReceived
= true;
394 var onCacheSizeSuccess = function(result
) {
396 cacheSizeReceived
= true;
400 // Fetch the current cache size.
401 this.fetchCacheSize_(onCacheSizeSuccess
, onCacheSizeFailure
, transaction
);
403 // Receive image's metadata.
404 var metadataRequest
= metadataStore
.get(key
);
406 metadataRequest
.onsuccess = function(e
) {
408 metadataEntry
= e
.target
.result
;
409 metadataReceived
= true;
413 metadataRequest
.onerror = function() {
414 console
.error('Failed to remove an image.');
415 metadataReceived
= true;