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.
8 * Persistent cache storing images in an indexed database on the hard disk.
13 * IndexedDB database handle.
21 * Cache database name.
25 Cache
.DB_NAME
= 'image-loader';
28 * Cache database version.
32 Cache
.DB_VERSION
= 11;
35 * Memory limit for images data in bytes.
40 Cache
.MEMORY_LIMIT
= 250 * 1024 * 1024; // 250 MB.
43 * Minimal amount of memory freed per eviction. Used to limit number of
44 * evictions which are expensive.
49 Cache
.EVICTION_CHUNK_SIZE
= 50 * 1024 * 1024; // 50 MB.
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
,
61 height
: request
.height
,
62 maxWidth
: request
.maxWidth
,
63 maxHeight
: request
.maxHeight
});
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
,
77 openRequest
.onsuccess = function(e
) {
78 this.db_
= e
.target
.result
;
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'});
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.
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.
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
) {
133 onSuccess(e
.target
.result
.value
);
138 sizeRequest
.onerror = function() {
139 console
.error('Failed to fetch size from the database.');
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.
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
) {
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
);
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
);
198 metadataStore
.openCursor().onsuccess = function(e
) {
199 var cursor
= event
.target
.result
;
201 metadataEntries
.push(cursor
.value
);
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
) {
222 console
.warn('Cache database not available.');
226 var onNotFoundInCache = function() {
227 var metadataEntry
= {key
: key
,
228 timestamp
: timestamp
,
230 lastLoadTimestamp
: Date
.now()};
231 var dataEntry
= {key
: key
,
234 var transaction
= this.db_
.transaction(['settings', 'metadata', 'data'],
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
);
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
) {
263 console
.warn('Cache database not available.');
268 var transaction
= this.db_
.transaction(['settings', 'metadata', 'data'],
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
)
285 // Check if both entries are available or both unavailable.
286 if (!!metadataEntry
!= !!dataEntry
) {
287 console
.warn('Incosistent cache database.');
292 // Process the responses.
293 if (!metadataEntry
) {
294 // The image not found.
296 } else if (metadataEntry
.timestamp
!= timestamp
) {
297 // The image is not up to date, so remove it.
298 this.removeImage(key
, function() {}, function() {}, transaction
);
301 // The image is available. Update the last load time and return the
303 metadataEntry
.lastLoadTimestamp
= Date
.now();
304 metadataStore
.put(metadataEntry
); // Added asynchronously.
305 onSuccess(dataEntry
.data
);
309 metadataRequest
.onsuccess = function(e
) {
311 metadataEntry
= e
.target
.result
;
312 metadataReceived
= true;
316 dataRequest
.onsuccess = function(e
) {
318 dataEntry
= e
.target
.result
;
323 metadataRequest
.onerror = function() {
324 console
.error('Failed to fetch metadata from the database.');
325 metadataReceived
= true;
329 dataRequest
.onerror = function() {
330 console
.error('Failed to fetch image data from the database.');
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
) {
348 console
.warn('Cache database not available.');
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
)
366 // If either cache size or metadata entry is not available, then it is
368 if (cacheSize
=== null || !metadataEntry
) {
377 this.setCacheSize_(cacheSize
- metadataEntry
.size
, transaction
);
378 metadataStore
.delete(key
); // Delete asynchronously.
379 dataStore
.delete(key
); // Delete asynchronously.
382 var onCacheSizeFailure = function() {
383 cacheSizeReceived
= true;
386 var onCacheSizeSuccess = function(result
) {
388 cacheSizeReceived
= true;
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
) {
400 metadataEntry
= e
.target
.result
;
401 metadataReceived
= true;
405 metadataRequest
.onerror = function() {
406 console
.error('Failed to remove an image.');
407 metadataReceived
= true;