Chromium Code Reviews| Index: chrome/browser/resources/image_loader/client.js |
| diff --git a/chrome/browser/resources/image_loader/client.js b/chrome/browser/resources/image_loader/client.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..a5f455e14ff3a67b751e2a85d76c152a72670466 |
| --- /dev/null |
| +++ b/chrome/browser/resources/image_loader/client.js |
| @@ -0,0 +1,313 @@ |
| +// Copyright 2012 The Chromium Authors. All rights reserved. |
|
James Hawkins
2013/02/20 04:31:06
2013
mtomasz
2013/02/21 05:19:24
Done.
|
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +var ImageLoader = ImageLoader || {}; |
| + |
| +/** |
| + * Image loader's extension id. |
| + * @const |
| + * @type {string} |
| + */ |
| +ImageLoader.EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp'; |
| + |
| +/** |
| + * Client used to connect to the remote ImageLoader class working in a separate |
|
James Hawkins
2013/02/20 04:31:06
What is the 'separate extension'?
mtomasz
2013/02/21 05:19:24
Clarified the comment. Client runs in Files.app ex
|
| + * extension. This class runs in the extension, when the client.js is included. |
| + * It sends remote requests using IPC to the ImageLoader class and forwards |
| + * its responses. |
| + * |
| + * Implements cache, which is stored in the calling extension. |
| + * |
| + * @constructor |
| + */ |
| +ImageLoader.Client = function() { |
| + this.port_ = chrome.extension.connect(ImageLoader.EXTENSION_ID); |
| + this.port_.onMessage.addListener(this.handleMessage_.bind(this)); |
| + |
| + this.tasks_ = {}; |
|
James Hawkins
2013/02/20 04:31:06
nit: Document member variables.
mtomasz
2013/02/21 05:19:24
Done.
|
| + this.lastTaskId_ = 0; |
| + this.cache_ = new ImageLoader.Client.Cache(); |
| +}; |
| + |
| +/** |
| + * Returns a singleton instance. Static. |
|
James Hawkins
2013/02/20 04:31:06
nit: No need to specify 'static'. Here and elsewh
mtomasz
2013/02/21 05:19:24
I wanted to use @static, however it is not accepte
|
| + * @return {ImageLoader.Client} ImageLoader.Client instance. |
| + */ |
| +ImageLoader.Client.getInstance = function() { |
| + if (!ImageLoader.Client.instance_) |
| + ImageLoader.Client.instance_ = new ImageLoader.Client(); |
| + return ImageLoader.Client.instance_; |
| +}; |
| + |
| +/** |
| + * Loads and resizes and image. Use opt_isValid to easily cancel requests |
|
James Hawkins
2013/02/20 04:31:06
s/and/an/?
mtomasz
2013/02/21 05:19:24
Done.
|
| + * which are not valid anymore, which will reduce cpu consumption. Static. |
| + * |
| + * @param {string} url Url of the requested image. |
| + * @param {function} callback Callback used to return response. |
| + * @param {Object=} opt_options Loader options, such as: scale, maxHeight, |
| + * width, height and/or cache. |
| + * @param {function=} opt_isValid Function returning false in case |
| + * a request is not valid anymore, eg. parent node has been detached. |
| + * @return {?number} Remote task id or null if loaded from cache. |
| + */ |
| +ImageLoader.Client.load = function(url, callback, opt_options, opt_isValid) { |
|
James Hawkins
2013/02/20 04:31:06
Please remove all of these 'singleton' methods and
mtomasz
2013/02/21 05:19:24
Done.
|
| + return ImageLoader.Client.getInstance().load( |
| + url, callback, opt_options, opt_isValid); |
| +}; |
| + |
| +/** |
| + * Cancels the request. Static. |
| + * @param {number} taskId Task id returned by ImageLoader.Client.load(). |
| + */ |
| +ImageLoader.Client.cancel = function(taskId) { |
| + ImageLoader.Client.getInstance().cancel(taskId); |
| +}; |
| + |
| +/** |
| + * Prints the cache usage statistics. Static. |
| + */ |
| +ImageLoader.Client.stat = function() { |
| + ImageLoader.Client.getInstance().stat(); |
| +}; |
| + |
| +/** |
| + * Handles a message from the remote image loader and calls the registered |
| + * callback to pass the response back to the requester. |
| + * |
| + * @param {Object} message Response message as a hash array. |
| + * @private |
| + */ |
| +ImageLoader.Client.prototype.handleMessage_ = function(message) { |
| + if (!(message.taskId in this.tasks_)) { |
| + // This task has been canceled, but was already fetched, so it's result |
| + // should be discarded anyway. |
| + return; |
| + } |
| + |
| + var task = this.tasks_[message.taskId]; |
| + |
| + // Check if the task is still valid. |
| + if (task.isValid()) |
| + task.accept(message); |
| + |
| + delete this.tasks_[message.taskId]; |
| +}; |
| + |
| +/** |
| + * Loads and resizes and image. Use opt_isValid to easily cancel requests |
| + * which are not valid anymore, which will reduce cpu consumption. |
| + * |
| + * @param {string} url Url of the requested image. |
| + * @param {function} callback Callback used to return response. |
| + * @param {Object=} opt_options Loader options, such as: scale, maxHeight, |
| + * width, height and/or cache. |
| + * @param {function=} opt_isValid Function returning false in case |
| + * a request is not valid anymore, eg. parent node has been detached. |
| + * @return {?number} Remote task id or null if loaded from cache. |
| + */ |
| +ImageLoader.Client.prototype.load = function( |
| + url, callback, opt_options, opt_isValid) { |
| + opt_options = opt_options || {}; |
| + opt_isValid = opt_isValid || function() { return true; }; |
| + |
| + // Cancel old, invalid tasks. |
| + var taskKeys = Object.keys(this.tasks_); |
| + for (var index = 0; index < taskKeys.length; index++) { |
| + var taskKey = taskKeys[index]; |
| + var task = this.tasks_[taskKey]; |
| + if (!task.isValid()) { |
| + // Cancel this task since it is not valid anymore. |
| + this.cancel(taskKey); |
| + delete this.tasks_[taskKey]; |
| + } |
| + } |
| + |
| + // Replace exchange id. |
|
James Hawkins
2013/02/20 04:31:06
What does 'exchange id' mean?
James Hawkins
2013/02/20 04:31:06
nit: s/id/ID/
mtomasz
2013/02/21 05:19:24
Should be extension id. Fixed.
mtomasz
2013/02/21 05:19:24
Done.
mtomasz
2013/02/21 05:19:24
git grep 'extension id' | wc -l
152
git grep 'ext
James Hawkins
2013/02/21 23:20:36
I'm not going to block the CL on this, but for ref
|
| + var sourceId = chrome.i18n.getMessage('@@extension_id'); |
| + var targetId = ImageLoader.EXTENSION_ID; |
| + |
| + url = url.replace('filesystem:chrome-extension://' + sourceId, |
| + 'filesystem:chrome-extension://' + targetId); |
| + |
| + // Try to load from cache, if available. |
| + var cacheKey = ImageLoader.Client.Cache.createKey(url, opt_options); |
| + if (opt_options.cache) { |
| + // Load from cache. |
| + // TODO(mtomasz): Add cache invalidating if the file has changed. |
| + var cachedData = this.cache_.loadImage(cacheKey); |
| + if (cachedData) { |
| + callback({ status: 'success', data: cachedData }); |
| + return null; |
| + } |
| + } else { |
| + // Remove from cache. |
| + this.cache_.removeImage(cacheKey); |
| + } |
| + |
| + // Not available in cache, performing a request to a remote extension. |
| + request = opt_options; |
| + this.lastTaskId_++; |
| + var task = { isValid: opt_isValid, accept: function(result) { |
| + // Save to cache. |
| + if (result.status == 'success' && opt_options.cache) |
| + this.cache_.saveImage(cacheKey, result.data); |
| + callback(result); |
| + }.bind(this) }; |
| + this.tasks_[this.lastTaskId_] = task; |
| + |
| + request.url = url; |
| + request.taskId = this.lastTaskId_; |
| + |
| + this.port_.postMessage(request); |
| + return request.taskId; |
| +}; |
| + |
| +/** |
| + * Cancels the request. |
| + * @param {number} taskId Task id returned by ImageLoader.Client.load(). |
| + */ |
| +ImageLoader.Client.prototype.cancel = function(taskId) { |
| + this.port_.postMessage({ taskId: taskId, cancel: true }); |
| +}; |
| + |
| +/** |
| + * Prints the cache usage statistics. |
| + */ |
| +ImageLoader.Client.prototype.stat = function() { |
| + this.cache_.stat(); |
| +}; |
| + |
| +/** |
| + * Least Recently Used (LRU) cache implementation to be used by |
| + * ImageLoader.Client class. It has memory constraints, so it will never |
| + * exceed specified memory limit defined in MEMORY_LIMIT. |
| + * |
| + * @constructor |
| + */ |
| +ImageLoader.Client.Cache = function() { |
| + this.images_ = []; |
| + this.size_ = 0; |
| +}; |
| + |
| +/** |
| + * Memory limit for images data in bytes. |
| + * |
| + * @const |
| + * @type {number} |
| + */ |
| +ImageLoader.Client.Cache.MEMORY_LIMIT = 100 * 1024 * 1024; // 100 MB. |
| + |
| +/** |
| + * Creates a cache key. Static. |
| + * |
| + * @param {string} url Image url. |
| + * @param {Object=} opt_options Loader options as a hash array. |
| + * @return {string} Cache key. |
| + */ |
| +ImageLoader.Client.Cache.createKey = function(url, opt_options) { |
| + var array = opt_options || {}; |
| + array.url = url; |
| + return JSON.stringify(array); |
| +}; |
| + |
| +/** |
| + * Evicts the least used elements in cache to make space for a new image. |
| + * |
| + * @param {number} size Requested size. |
| + * @private |
| + */ |
| +ImageLoader.Client.Cache.prototype.evictCache_ = function(size) { |
| + // Sort from the most recent to the oldest. |
| + this.images_.sort(function(a, b) { |
| + return b.timestamp - a.timestamp; |
| + }); |
| + |
| + while (this.images_.length > 0 && |
| + (ImageLoader.Client.Cache.MEMORY_LIMIT - this.size_ < size)) { |
| + var entry = this.images_.pop(); |
| + this.size_ -= entry.data.length; |
| + } |
| +}; |
| + |
| +/** |
| + * Saves an image in the cache. |
| + * |
| + * @param {string} key Cache key. |
| + * @param {string} data Image data. |
| + */ |
| +ImageLoader.Client.Cache.prototype.saveImage = function(key, data) { |
| + this.evictCache_(data.length); |
| + if (ImageLoader.Client.Cache.MEMORY_LIMIT - this.size_ >= data.length) { |
| + this.images_[key] = { timestamp: Date.now(), data: data }; |
| + this.size_ += data.length; |
| + } |
| +}; |
| + |
| +/** |
| + * Loads an image from the cache (if available) or returns null. |
| + * |
| + * @param {string} key Cache key. |
| + * @return {?string} Data of the loaded image or null. |
| + */ |
| +ImageLoader.Client.Cache.prototype.loadImage = function(key) { |
| + if (!(key in this.images_)) |
| + return null; |
| + |
| + var entry = this.images_[key]; |
| + entry.timestamp = Date.now(); |
| + return entry.data; |
| +}; |
| + |
| +/** |
| + * Prints the cache usage stats. |
| + */ |
| +ImageLoader.Client.Cache.prototype.stat = function() { |
| + console.log('Cache entries: ' + Object.keys(this.images_).length); |
| + console.log('Usage: ' + Math.round(this.size_ / |
| + ImageLoader.Client.Cache.MEMORY_LIMIT * 100.0) + '%'); |
| +}; |
| + |
| +/** |
| + * Removes the image from the cache. |
| + * @param {string} key Cache key. |
| + */ |
| +ImageLoader.Client.Cache.prototype.removeImage = function(key) { |
| + if (!(key in this.images_)) |
| + return; |
| + |
| + var entry = this.images_[key]; |
| + this.size_ -= entry.data.length; |
| + delete this.images_[key]; |
| +}; |
| + |
| +// Helper functions. |
| + |
| +/** |
| + * Loads and resizes and image. Use opt_isValid to easily cancel requests |
| + * which are not valid anymore, which will reduce cpu consumption. |
| + * |
| + * @param {string} url Url of the requested image. |
| + * @param {Image} image Image node to load the requested picture into. |
| + * @param {Object} options Loader options, such as: scale, maxHeight, width, |
| + * height and/or cache. |
| + * @param {function=} onSuccess Callback for success. |
| + * @param {function=} onError Callback for failure. |
| + * @param {function=} opt_isValid Function returning false in case |
| + * a request is not valid anymore, eg. parent node has been detached. |
| + * @return {?number} Remote task id or null if loaded from cache. |
| + */ |
| +ImageLoader.Client.loadToImage = function(url, image, options, onSuccess, |
| + onError, opt_isValid) { |
| + var callback = function(result) { |
| + if (result.status == 'error') { |
| + onError(); |
| + return; |
| + } |
| + image.src = result.data; |
| + onSuccess(); |
| + }; |
| + |
| + return ImageLoader.Client.load(url, callback, options, opt_isValid); |
| +}; |