blob: 323531083734ced0bba06e1a999d608271dfefad [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @typedef {{
* fulfill: function(PiexLoaderResponse):undefined,
* reject: function(string):undefined}
* }}
*/
var PiexRequestCallbacks;
/**
* @param {{id:number, thumbnail:!ArrayBuffer, orientation:number,
* colorSpace: ColorSpace}}
* data Data directly returned from NaCl module.
* @constructor
* @struct
*/
function PiexLoaderResponse(data) {
/**
* @public {number}
* @const
*/
this.id = data.id;
/**
* @public {!ArrayBuffer}
* @const
*/
this.thumbnail = data.thumbnail;
/**
* @public {!ImageOrientation}
* @const
*/
this.orientation =
ImageOrientation.fromExifOrientation(data.orientation);
/**
* @public {ColorSpace}
* @const
*/
this.colorSpace = data.colorSpace;
}
/**
* Creates a PiexLoader for loading RAW files using a Piex NaCl module.
*
* All of the arguments are optional and used for tests only. If not passed,
* then default implementations and values will be used.
*
* @param {function()=} opt_createModule Creates a NaCl module.
* @param {function(!Element)=} opt_destroyModule Destroys a NaCl module.
* @param {number=} opt_idleTimeout Idle timeout to destroy NaCl module.
* @constructor
* @struct
*/
function PiexLoader(opt_createModule, opt_destroyModule, opt_idleTimeout) {
/**
* @private {function():!HTMLEmbedElement}
*/
this.createModule_ = opt_createModule || this.defaultCreateModule_.bind(this);
/**
* @private {function():!Element}
*/
this.destroyModule_ =
opt_destroyModule || this.defaultDestroyModule_.bind(this);
this.idleTimeoutMs_ = opt_idleTimeout !== undefined ?
opt_idleTimeout :
PiexLoader.DEFAULT_IDLE_TIMEOUT_MS;
/**
* @private {HTMLEmbedElement}
*/
this.naclModule_ = null;
/**
* @private {Element}
*/
this.containerElement_ = null;
/**
* @private {number}
*/
this.unloadTimer_ = 0;
/**
* @private {Promise<boolean>}
*/
this.naclPromise_ = null;
/**
* @private {?function(boolean)}
*/
this.naclPromiseFulfill_ = null;
/**
* @private {?function(string=)}
*/
this.naclPromiseReject_ = null;
/**
* @private {!Object<number, ?PiexRequestCallbacks>}
* @const
*/
this.requests_ = {};
/**
* @private {number}
*/
this.requestIdCount_ = 0;
// Bound function so the listeners can be unregistered.
this.onNaclLoadBound_ = this.onNaclLoad_.bind(this);
this.onNaclMessageBound_ = this.onNaclMessage_.bind(this);
this.onNaclErrorBound_ = this.onNaclError_.bind(this);
this.onNaclCrashBound_ = this.onNaclCrash_.bind(this);
}
/**
* Idling time before the NaCl module is unloaded. This lets the image loader
* extension close when inactive.
*
* @const {number}
*/
PiexLoader.DEFAULT_IDLE_TIMEOUT_MS = 3000; // 3 seconds.
/**
* Creates a NaCl module element.
*
* Do not call directly. Use this.loadModule_ instead to support
* tests.
*
* @return {!HTMLEmbedElement}
* @private
*/
PiexLoader.prototype.defaultCreateModule_ = function() {
var embed =
assertInstanceof(document.createElement('embed'), HTMLEmbedElement);
embed.setAttribute('type', 'application/x-pnacl');
// The extension nmf is not allowed to load. We uses .nmf.js instead.
embed.setAttribute('src', '/piex/piex.nmf.txt');
embed.width = '0';
embed.height = '0';
return embed;
};
PiexLoader.prototype.defaultDestroyModule_ = function(module) {
// The module is destroyed by removing it from DOM in loadNaclModule_().
};
/**
* @return {!Promise<boolean>}
* @private
*/
PiexLoader.prototype.loadNaclModule_ = function() {
if (this.naclPromise_) {
return this.naclPromise_;
}
this.naclPromise_ =
new Promise(function(fulfill) {
chrome.fileManagerPrivate.isPiexLoaderEnabled(fulfill);
})
.then(function(enabled) {
if (!enabled) {
return false;
}
return new Promise(function(fulfill, reject) {
this.naclPromiseFulfill_ = fulfill;
this.naclPromiseReject_ = reject;
this.naclModule_ = this.createModule_();
// The <EMBED> element is wrapped inside a <DIV>, which has both a
// 'load' and a 'message' event listener attached. This wrapping
// method is used instead of attaching the event listeners
// directly to the <EMBED> element to ensure that the listeners
// are active before the NaCl module 'load' event fires.
var listenerContainer = assertInstanceof(
document.createElement('div'), HTMLDivElement);
listenerContainer.appendChild(this.naclModule_);
listenerContainer.addEventListener(
'load', this.onNaclLoadBound_, true);
listenerContainer.addEventListener(
'message', this.onNaclMessageBound_, true);
listenerContainer.addEventListener(
'error', this.onNaclErrorBound_, true);
listenerContainer.addEventListener(
'crash', this.onNaclCrashBound_, true);
listenerContainer.style.height = '0px';
this.containerElement_ = listenerContainer;
document.body.appendChild(listenerContainer);
// Force a relayout. Workaround for load event not being called on
// <embed> for a NaCl module. crbug.com/699930
/** @suppress {suspiciousCode} */ this.naclModule_.offsetTop;
}.bind(this));
}.bind(this))
.catch(function(error) {
console.error(error);
return false;
});
return this.naclPromise_;
};
/**
* @private
*/
PiexLoader.prototype.unloadNaclModule_ = function() {
this.containerElement_.removeEventListener('load', this.onNaclLoadBound_);
this.containerElement_.removeEventListener(
'message', this.onNaclMessageBound_);
this.containerElement_.removeEventListener('error', this.onNaclErrorBound_);
this.containerElement_.removeEventListener('crash', this.onNaclCrashBound_);
this.containerElement_.parentNode.removeChild(this.containerElement_);
this.containerElement_ = null;
this.destroyModule_();
this.naclModule_ = null;
this.naclPromise_ = null;
this.naclPromiseFulfill_ = null;
this.naclPromiseReject_ = null;
};
/**
* @param {Event} event
* @private
*/
PiexLoader.prototype.onNaclLoad_ = function(event) {
console.assert(this.naclPromiseFulfill_);
this.naclPromiseFulfill_(true);
};
/**
* @param {Event} listener_event
* @private
*/
PiexLoader.prototype.onNaclMessage_ = function(listener_event) {
let event = /** @type{MessageEvent} */ (listener_event);
var id = event.data.id;
if (!event.data.error) {
var response = new PiexLoaderResponse(event.data);
console.assert(this.requests_[id]);
this.requests_[id].fulfill(response);
} else {
console.assert(this.requests_[id]);
this.requests_[id].reject(event.data.error);
}
delete this.requests_[id];
if (Object.keys(this.requests_).length === 0) {
this.scheduleUnloadOnIdle_();
}
};
/**
* @param {Event} event
* @private
*/
PiexLoader.prototype.onNaclError_ = function(event) {
console.assert(this.naclPromiseReject_);
this.naclPromiseReject_(this.naclModule_['lastError']);
};
/**
* @param {Event} event
* @private
*/
PiexLoader.prototype.onNaclCrash_ = function(event) {
console.assert(this.naclPromiseReject_);
this.naclPromiseReject_('PiexLoader crashed.');
};
/**
* Schedules unloading the NaCl module after IDLE_TIMEOUT_MS passes.
* @private
*/
PiexLoader.prototype.scheduleUnloadOnIdle_ = function() {
if (this.unloadTimer_) {
clearTimeout(this.unloadTimer_);
}
this.unloadTimer_ =
setTimeout(this.onIdleTimeout_.bind(this), this.idleTimeoutMs_);
};
/**
* @private
*/
PiexLoader.prototype.onIdleTimeout_ = function() {
this.unloadNaclModule_();
};
/**
* Simulates time passed required to fire the closure enqueued with setTimeout.
*
* Note, that if there is no active timer set with setTimeout earlier, then
* nothing will happen.
*
* This method is used to avoid waiting for DEFAULT_IDLE_TIMEOUT_MS in tests.
* Also, it allows to avoid flakyness by effectively removing any dependency
* on execution speed of the test (tests set the timeout to a very large value
* and only rely on this method to simulate passed time).
*/
PiexLoader.prototype.simulateIdleTimeoutPassedForTests = function() {
if (this.unloadTimer_) {
clearTimeout(this.unloadTimer_);
this.onIdleTimeout_();
}
};
/**
* Starts to load RAW image.
* @param {string} url
* @return {!Promise<!PiexLoaderResponse>}
*/
PiexLoader.prototype.load = function(url) {
var requestId = this.requestIdCount_++;
if (this.unloadTimer_) {
clearTimeout(this.unloadTimer_);
this.unloadTimer_ = 0;
}
// Prevents unloading the NaCl module during handling the promises below.
this.requests_[requestId] = null;
return this.loadNaclModule_().then(function(loaded) {
if (!loaded) {
return Promise.reject('Piex is not loaded');
}
var message = {id: requestId, name: 'loadThumbnail', url: url};
this.naclModule_.postMessage(message);
return new Promise(function(fulfill, reject) {
delete this.requests_[requestId];
this.requests_[message.id] = {fulfill: fulfill, reject: reject};
}.bind(this))
.catch(function(error) {
delete this.requests_[requestId];
console.error('PiexLoaderError: ', error);
return Promise.reject(error);
});
}.bind(this));
};