Merge pull request #3340 from brendandahl/chainable-promises2

Use A+ spec compatible promises.
This commit is contained in:
Yury Delendik 2013-06-10 12:46:09 -07:00
commit 65d861c5dc
7 changed files with 229 additions and 200 deletions

View File

@ -48,7 +48,8 @@
*/ */
PDFJS.getDocument = function getDocument(source, PDFJS.getDocument = function getDocument(source,
pdfDataRangeTransport, pdfDataRangeTransport,
passwordCallback) { passwordCallback,
progressCallback) {
var workerInitializedPromise, workerReadyPromise, transport; var workerInitializedPromise, workerReadyPromise, transport;
if (typeof source === 'string') { if (typeof source === 'string') {
@ -76,7 +77,7 @@ PDFJS.getDocument = function getDocument(source,
workerInitializedPromise = new PDFJS.Promise(); workerInitializedPromise = new PDFJS.Promise();
workerReadyPromise = new PDFJS.Promise(); workerReadyPromise = new PDFJS.Promise();
transport = new WorkerTransport(workerInitializedPromise, transport = new WorkerTransport(workerInitializedPromise,
workerReadyPromise, pdfDataRangeTransport); workerReadyPromise, pdfDataRangeTransport, progressCallback);
workerInitializedPromise.then(function transportInitialized() { workerInitializedPromise.then(function transportInitialized() {
transport.passwordCallback = passwordCallback; transport.passwordCallback = passwordCallback;
transport.fetchDocument(params); transport.fetchDocument(params);
@ -480,10 +481,11 @@ var PDFPageProxy = (function PDFPageProxyClosure() {
*/ */
var WorkerTransport = (function WorkerTransportClosure() { var WorkerTransport = (function WorkerTransportClosure() {
function WorkerTransport(workerInitializedPromise, workerReadyPromise, function WorkerTransport(workerInitializedPromise, workerReadyPromise,
pdfDataRangeTransport) { pdfDataRangeTransport, progressCallback) {
this.pdfDataRangeTransport = pdfDataRangeTransport; this.pdfDataRangeTransport = pdfDataRangeTransport;
this.workerReadyPromise = workerReadyPromise; this.workerReadyPromise = workerReadyPromise;
this.progressCallback = progressCallback;
this.commonObjs = new PDFObjects(); this.commonObjs = new PDFObjects();
this.pageCache = []; this.pageCache = [];
@ -705,11 +707,7 @@ var WorkerTransport = (function WorkerTransportClosure() {
}, this); }, this);
messageHandler.on('DocProgress', function transportDocProgress(data) { messageHandler.on('DocProgress', function transportDocProgress(data) {
// TODO(mack): The progress event should be resolved on a different this.progressCallback({
// promise that tracks progress of whole file, since workerReadyPromise
// is for file being ready to render, not for when file is fully
// downloaded
this.workerReadyPromise.progress({
loaded: data.loaded, loaded: data.loaded,
total: data.total total: data.total
}); });

View File

@ -533,7 +533,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
// keep track of each font we translated so the caller can // keep track of each font we translated so the caller can
// load them asynchronously before calling display on a page // load them asynchronously before calling display on a page
font.loadedName = 'g_font_' + this.uniquePrefix + font.loadedName = 'g_font_' + this.uniquePrefix +
(this.idCounters.font + 1); (++this.idCounters.font);
if (!font.translated) { if (!font.translated) {
var translated; var translated;
@ -567,14 +567,12 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
} }
font.translated.charProcOperatorList = charProcOperatorList; font.translated.charProcOperatorList = charProcOperatorList;
font.loaded = true; font.loaded = true;
++this.idCounters.font;
promise.resolve({ promise.resolve({
font: font, font: font,
dependencies: dependencies dependencies: dependencies
}); });
}.bind(this)); }.bind(this));
} else { } else {
++this.idCounters.font;
font.loaded = true; font.loaded = true;
promise.resolve({ promise.resolve({
font: font, font: font,

View File

@ -1072,13 +1072,20 @@ var PDFObjects = (function PDFObjectsClosure() {
PDFObjects.prototype = { PDFObjects.prototype = {
/** /**
* Internal function. * Internal function.
* Ensures there is an object defined for `objId`. Stores `data` on the * Ensures there is an object defined for `objId`.
* object *if* it is created.
*/ */
ensureObj: function PDFObjects_ensureObj(objId, data) { ensureObj: function PDFObjects_ensureObj(objId) {
if (this.objs[objId]) if (this.objs[objId])
return this.objs[objId]; return this.objs[objId];
return this.objs[objId] = new Promise(objId, data);
var obj = {
promise: new Promise(objId),
data: null,
resolved: false
};
this.objs[objId] = obj;
return obj;
}, },
/** /**
@ -1094,7 +1101,7 @@ var PDFObjects = (function PDFObjectsClosure() {
// If there is a callback, then the get can be async and the object is // If there is a callback, then the get can be async and the object is
// not required to be resolved right now // not required to be resolved right now
if (callback) { if (callback) {
this.ensureObj(objId).then(callback); this.ensureObj(objId).promise.then(callback);
return null; return null;
} }
@ -1104,7 +1111,7 @@ var PDFObjects = (function PDFObjectsClosure() {
// If there isn't an object yet or the object isn't resolved, then the // If there isn't an object yet or the object isn't resolved, then the
// data isn't ready yet! // data isn't ready yet!
if (!obj || !obj.isResolved) if (!obj || !obj.resolved)
error('Requesting object that isn\'t resolved yet ' + objId); error('Requesting object that isn\'t resolved yet ' + objId);
return obj.data; return obj.data;
@ -1114,36 +1121,25 @@ var PDFObjects = (function PDFObjectsClosure() {
* Resolves the object `objId` with optional `data`. * Resolves the object `objId` with optional `data`.
*/ */
resolve: function PDFObjects_resolve(objId, data) { resolve: function PDFObjects_resolve(objId, data) {
var objs = this.objs; var obj = this.ensureObj(objId);
// In case there is a promise already on this object, just resolve it. obj.resolved = true;
if (objs[objId]) { obj.data = data;
objs[objId].resolve(data); obj.promise.resolve(data);
} else {
this.ensureObj(objId, data);
}
},
onData: function PDFObjects_onData(objId, callback) {
this.ensureObj(objId).onData(callback);
}, },
isResolved: function PDFObjects_isResolved(objId) { isResolved: function PDFObjects_isResolved(objId) {
var objs = this.objs; var objs = this.objs;
if (!objs[objId]) { if (!objs[objId]) {
return false; return false;
} else { } else {
return objs[objId].isResolved; return objs[objId].resolved;
} }
}, },
hasData: function PDFObjects_hasData(objId) { hasData: function PDFObjects_hasData(objId) {
var objs = this.objs; return this.isResolved(objId);
if (!objs[objId]) {
return false;
} else {
return objs[objId].hasData;
}
}, },
/** /**
@ -1151,22 +1147,13 @@ var PDFObjects = (function PDFObjectsClosure() {
*/ */
getData: function PDFObjects_getData(objId) { getData: function PDFObjects_getData(objId) {
var objs = this.objs; var objs = this.objs;
if (!objs[objId] || !objs[objId].hasData) { if (!objs[objId] || !objs[objId].resolved) {
return null; return null;
} else { } else {
return objs[objId].data; return objs[objId].data;
} }
}, },
/**
* Sets the data of an object but *doesn't* resolve it.
*/
setData: function PDFObjects_setData(objId, data) {
// Watchout! If you call `this.ensureObj(objId, data)` you're going to
// create a *resolved* promise which shouldn't be the case!
this.ensureObj(objId).data = data;
},
clear: function PDFObjects_clear() { clear: function PDFObjects_clear() {
this.objs = {}; this.objs = {};
} }

View File

@ -649,41 +649,121 @@ function isPDFFunction(v) {
} }
/** /**
* 'Promise' object. * The following promise implementation tries to generally implment the
* Each object that is stored in PDFObjects is based on a Promise object that * Promise/A+ spec. Some notable differences from other promise libaries are:
* contains the status of the object and the data. There might be situations * - There currently isn't a seperate deferred and promise object.
* where a function wants to use the value of an object, but it isn't ready at * - Unhandled rejections eventually show an error if they aren't handled.
* that time. To get a notification, once the object is ready to be used, s.o. *
* can add a callback using the `then` method on the promise that then calls * Based off of the work in:
* the callback once the object gets resolved. * https://bugzilla.mozilla.org/show_bug.cgi?id=810490
* A promise can get resolved only once and only once the data of the promise
* can be set. If any of these happens twice or the data is required before
* it was set, an exception is throw.
*/ */
var Promise = PDFJS.Promise = (function PromiseClosure() { var Promise = PDFJS.Promise = (function PromiseClosure() {
var EMPTY_PROMISE = {}; var STATUS_PENDING = 0;
var STATUS_RESOLVED = 1;
var STATUS_REJECTED = 2;
/** // In an attempt to avoid silent exceptions, unhandled rejections are
* If `data` is passed in this constructor, the promise is created resolved. // tracked and if they aren't handled in a certain amount of time an
* If there isn't data, it isn't resolved at the beginning. // error is logged.
*/ var REJECTION_TIMEOUT = 500;
function Promise(name, data) {
this.name = name; var HandlerManager = {
this.isRejected = false; handlers: [],
this.error = null; running: false,
this.exception = null; unhandledRejections: [],
// If you build a promise and pass in some data it's already resolved. pendingRejectionCheck: false,
if (data !== null && data !== undefined) {
this.isResolved = true; scheduleHandlers: function scheduleHandlers(promise) {
this._data = data; if (promise._status == STATUS_PENDING) {
this.hasData = true; return;
} else { }
this.isResolved = false;
this._data = EMPTY_PROMISE; this.handlers = this.handlers.concat(promise._handlers);
promise._handlers = [];
if (this.running) {
return;
}
this.running = true;
setTimeout(this.runHandlers.bind(this), 0);
},
runHandlers: function runHandlers() {
while (this.handlers.length > 0) {
var handler = this.handlers.shift();
var nextStatus = handler.thisPromise._status;
var nextValue = handler.thisPromise._value;
try {
if (nextStatus === STATUS_RESOLVED) {
if (typeof(handler.onResolve) == 'function') {
nextValue = handler.onResolve(nextValue);
}
} else if (typeof(handler.onReject) === 'function') {
nextValue = handler.onReject(nextValue);
nextStatus = STATUS_RESOLVED;
if (handler.thisPromise._unhandledRejection) {
this.removeUnhandeledRejection(handler.thisPromise);
}
}
} catch (ex) {
nextStatus = STATUS_REJECTED;
nextValue = ex;
}
handler.nextPromise._updateStatus(nextStatus, nextValue);
}
this.running = false;
},
addUnhandledRejection: function addUnhandledRejection(promise) {
this.unhandledRejections.push({
promise: promise,
time: Date.now()
});
this.scheduleRejectionCheck();
},
removeUnhandeledRejection: function removeUnhandeledRejection(promise) {
promise._unhandledRejection = false;
for (var i = 0; i < this.unhandledRejections.length; i++) {
if (this.unhandledRejections[i].promise === promise) {
this.unhandledRejections.splice(i);
i--;
}
}
},
scheduleRejectionCheck: function scheduleRejectionCheck() {
if (this.pendingRejectionCheck) {
return;
}
this.pendingRejectionCheck = true;
setTimeout(function rejectionCheck() {
this.pendingRejectionCheck = false;
var now = Date.now();
for (var i = 0; i < this.unhandledRejections.length; i++) {
if (now - this.unhandledRejections[i].time > REJECTION_TIMEOUT) {
console.error('Unhandled rejection: ' +
this.unhandledRejections[i].promise._value);
this.unhandledRejections.splice(i);
i--;
}
}
if (this.unhandledRejections.length) {
this.scheduleRejectionCheck();
}
}.bind(this), REJECTION_TIMEOUT);
} }
this.callbacks = []; };
this.errbacks = [];
this.progressbacks = []; function Promise() {
this._status = STATUS_PENDING;
this._handlers = [];
} }
/** /**
* Builds a promise that is resolved when all the passed in promises are * Builds a promise that is resolved when all the passed in promises are
@ -700,7 +780,7 @@ var Promise = PDFJS.Promise = (function PromiseClosure() {
return deferred; return deferred;
} }
function reject(reason) { function reject(reason) {
if (deferred.isRejected) { if (deferred._status === STATUS_REJECTED) {
return; return;
} }
results = []; results = [];
@ -710,7 +790,7 @@ var Promise = PDFJS.Promise = (function PromiseClosure() {
var promise = promises[i]; var promise = promises[i];
promise.then((function(i) { promise.then((function(i) {
return function(value) { return function(value) {
if (deferred.isRejected) { if (deferred._status === STATUS_REJECTED) {
return; return;
} }
results[i] = value; results[i] = value;
@ -722,102 +802,63 @@ var Promise = PDFJS.Promise = (function PromiseClosure() {
} }
return deferred; return deferred;
}; };
Promise.prototype = {
hasData: false,
set data(value) { Promise.prototype = {
if (value === undefined) { _status: null,
_value: null,
_handlers: null,
_unhandledRejection: null,
_updateStatus: function Promise__updateStatus(status, value) {
if (this._status === STATUS_RESOLVED ||
this._status === STATUS_REJECTED) {
return; return;
} }
if (this._data !== EMPTY_PROMISE) {
error('Promise ' + this.name +
': Cannot set the data of a promise twice');
}
this._data = value;
this.hasData = true;
if (this.onDataCallback) { if (status == STATUS_RESOLVED &&
this.onDataCallback(value); value && typeof(value.then) === 'function') {
value.then(this._updateStatus.bind(this, STATUS_RESOLVED),
this._updateStatus.bind(this, STATUS_REJECTED));
return;
} }
this._status = status;
this._value = value;
if (status === STATUS_REJECTED && this._handlers.length === 0) {
this._unhandledRejection = true;
HandlerManager.addUnhandledRejection(this);
}
HandlerManager.scheduleHandlers(this);
}, },
get data() { get isResolved() {
if (this._data === EMPTY_PROMISE) { return this._status === STATUS_RESOLVED;
error('Promise ' + this.name + ': Cannot get data that isn\'t set');
}
return this._data;
}, },
onData: function Promise_onData(callback) { get isRejected() {
if (this._data !== EMPTY_PROMISE) { return this._status === STATUS_REJECTED;
callback(this._data);
} else {
this.onDataCallback = callback;
}
}, },
resolve: function Promise_resolve(data) { resolve: function Promise_resolve(value) {
if (this.isResolved) { this._updateStatus(STATUS_RESOLVED, value);
error('A Promise can be resolved only once ' + this.name);
}
if (this.isRejected) {
error('The Promise was already rejected ' + this.name);
}
this.isResolved = true;
this.data = (typeof data !== 'undefined') ? data : null;
var callbacks = this.callbacks;
for (var i = 0, ii = callbacks.length; i < ii; i++) {
callbacks[i].call(null, data);
}
}, },
progress: function Promise_progress(data) { reject: function Promise_reject(reason) {
var callbacks = this.progressbacks; this._updateStatus(STATUS_REJECTED, reason);
for (var i = 0, ii = callbacks.length; i < ii; i++) {
callbacks[i].call(null, data);
}
}, },
reject: function Promise_reject(reason, exception) { then: function Promise_then(onResolve, onReject) {
if (this.isRejected) { var nextPromise = new Promise();
error('A Promise can be rejected only once ' + this.name); this._handlers.push({
} thisPromise: this,
if (this.isResolved) { onResolve: onResolve,
error('The Promise was already resolved ' + this.name); onReject: onReject,
} nextPromise: nextPromise
});
this.isRejected = true; HandlerManager.scheduleHandlers(this);
this.error = reason || null; return nextPromise;
this.exception = exception || null;
var errbacks = this.errbacks;
for (var i = 0, ii = errbacks.length; i < ii; i++) {
errbacks[i].call(null, reason, exception);
}
},
then: function Promise_then(callback, errback, progressback) {
// If the promise is already resolved, call the callback directly.
if (this.isResolved && callback) {
var data = this.data;
callback.call(null, data);
} else if (this.isRejected && errback) {
var error = this.error;
var exception = this.exception;
errback.call(null, error, exception);
} else {
if (callback) {
this.callbacks.push(callback);
}
if (errback) {
this.errbacks.push(errback);
}
}
if (progressback)
this.progressbacks.push(progressback);
} }
}; };

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
/* globals PDFJS, getPdf, combineUrl, StatTimer, SpecialPowers */ /* globals PDFJS, getPdf, combineUrl, StatTimer, SpecialPowers, Promise */
'use strict'; 'use strict';
@ -264,6 +264,7 @@ function nextPage(task, loadError) {
clear(ctx); clear(ctx);
var drawContext, textLayerBuilder; var drawContext, textLayerBuilder;
var initPromise = new Promise();
if (task.type == 'text') { if (task.type == 'text') {
// using dummy canvas for pdf context drawing operations // using dummy canvas for pdf context drawing operations
if (!dummyCanvas) { if (!dummyCanvas) {
@ -275,10 +276,12 @@ function nextPage(task, loadError) {
page.getTextContent().then(function(textContent) { page.getTextContent().then(function(textContent) {
textLayerBuilder.setTextContent(textContent); textLayerBuilder.setTextContent(textContent);
initPromise.resolve();
}); });
} else { } else {
drawContext = ctx; drawContext = ctx;
textLayerBuilder = new NullTextLayerBuilder(); textLayerBuilder = new NullTextLayerBuilder();
initPromise.resolve();
} }
var renderContext = { var renderContext = {
canvasContext: drawContext, canvasContext: drawContext,
@ -291,11 +294,13 @@ function nextPage(task, loadError) {
page.stats = new StatTimer(); page.stats = new StatTimer();
snapshotCurrentPage(task, error); snapshotCurrentPage(task, error);
}); });
page.render(renderContext).then(function() { initPromise.then(function () {
completeRender(false); page.render(renderContext).then(function() {
}, completeRender(false);
function(error) { },
completeRender('render : ' + error); function(error) {
completeRender('render : ' + error);
});
}); });
}, },
function(error) { function(error) {

View File

@ -8,26 +8,25 @@ describe('api', function() {
// TODO run with worker enabled // TODO run with worker enabled
PDFJS.disableWorker = true; PDFJS.disableWorker = true;
var basicApiUrl = combineUrl(window.location.href, '../pdfs/basicapi.pdf'); var basicApiUrl = combineUrl(window.location.href, '../pdfs/basicapi.pdf');
function waitsForPromise(promise) { function waitsForPromise(promise, successCallback) {
waitsFor(function() { var data;
return promise.isResolved || promise.isRejected; promise.then(function(val) {
}, 10000); data = val;
} successCallback(data);
function expectAfterPromise(promise, successCallback) { },
waitsForPromise(promise); function(error) {
runs(function() { // Shouldn't get here.
promise.then(successCallback, expect(false).toEqual(true);
function(error, e) {
// Shouldn't get here.
expect(false).toEqual(true);
});
}); });
waitsFor(function() {
return data !== undefined;
}, 10000);
} }
describe('PDFJS', function() { describe('PDFJS', function() {
describe('getDocument', function() { describe('getDocument', function() {
it('creates pdf doc from URL', function() { it('creates pdf doc from URL', function() {
var promise = PDFJS.getDocument(basicApiUrl); var promise = PDFJS.getDocument(basicApiUrl);
expectAfterPromise(promise, function(data) { waitsForPromise(promise, function(data) {
expect(true).toEqual(true); expect(true).toEqual(true);
}); });
}); });
@ -40,10 +39,9 @@ describe('api', function() {
}); });
describe('PDFDocument', function() { describe('PDFDocument', function() {
var promise = PDFJS.getDocument(basicApiUrl); var promise = PDFJS.getDocument(basicApiUrl);
waitsForPromise(promise);
var doc; var doc;
runs(function() { waitsForPromise(promise, function(data) {
promise.then(function(data) { doc = data; }); doc = data;
}); });
it('gets number of pages', function() { it('gets number of pages', function() {
expect(doc.numPages).toEqual(3); expect(doc.numPages).toEqual(3);
@ -53,19 +51,19 @@ describe('api', function() {
}); });
it('gets page', function() { it('gets page', function() {
var promise = doc.getPage(1); var promise = doc.getPage(1);
expectAfterPromise(promise, function(data) { waitsForPromise(promise, function(data) {
expect(true).toEqual(true); expect(true).toEqual(true);
}); });
}); });
it('gets destinations', function() { it('gets destinations', function() {
var promise = doc.getDestinations(); var promise = doc.getDestinations();
expectAfterPromise(promise, function(data) { waitsForPromise(promise, function(data) {
// TODO this seems to be broken for the test pdf // TODO this seems to be broken for the test pdf
}); });
}); });
it('gets outline', function() { it('gets outline', function() {
var promise = doc.getOutline(); var promise = doc.getOutline();
expectAfterPromise(promise, function(outline) { waitsForPromise(promise, function(outline) {
// Two top level entries. // Two top level entries.
expect(outline.length).toEqual(2); expect(outline.length).toEqual(2);
// Make sure some basic attributes are set. // Make sure some basic attributes are set.
@ -76,7 +74,7 @@ describe('api', function() {
}); });
it('gets metadata', function() { it('gets metadata', function() {
var promise = doc.getMetadata(); var promise = doc.getMetadata();
expectAfterPromise(promise, function(metadata) { waitsForPromise(promise, function(metadata) {
expect(metadata.info['Title']).toEqual('Basic API Test'); expect(metadata.info['Title']).toEqual('Basic API Test');
expect(metadata.metadata.get('dc:title')).toEqual('Basic API Test'); expect(metadata.metadata.get('dc:title')).toEqual('Basic API Test');
}); });
@ -89,13 +87,11 @@ describe('api', function() {
promise.resolve(data); promise.resolve(data);
}); });
}); });
waitsForPromise(promise);
var page; var page;
runs(function() { waitsForPromise(promise, function(data) {
promise.then(function(data) { page = data;
page = data;
});
}); });
it('gets ref', function() { it('gets ref', function() {
expect(page.ref).toEqual({num: 15, gen: 0}); expect(page.ref).toEqual({num: 15, gen: 0});
}); });

View File

@ -250,6 +250,8 @@ var PDFFindController = {
extractTextPromises: [], extractTextPromises: [],
pendingFindMatches: {},
// If active, find results will be highlighted. // If active, find results will be highlighted.
active: false, active: false,
@ -425,13 +427,13 @@ var PDFFindController = {
this.updatePage(i); this.updatePage(i);
// As soon as the text is extracted start finding the matches. // As soon as the text is extracted start finding the matches.
this.extractTextPromises[i].onData(function(pageIdx) { if (!(i in this.pendingFindMatches)) {
// Use a timeout since all the pages may already be extracted and we this.pendingFindMatches[i] = true;
// want to start highlighting before finding all the matches. this.extractTextPromises[i].then(function(pageIdx) {
setTimeout(function() { delete self.pendingFindMatches[pageIdx];
self.calcFindMatch(pageIdx); self.calcFindMatch(pageIdx);
}); });
}); }
} }
} }
@ -1355,7 +1357,12 @@ var PDFView = {
} }
}; };
PDFJS.getDocument(parameters, pdfDataRangeTransport, passwordNeeded).then( function getDocumentProgress(progressData) {
self.progress(progressData.loaded / progressData.total);
}
PDFJS.getDocument(parameters, pdfDataRangeTransport, passwordNeeded,
getDocumentProgress).then(
function getDocumentCallback(pdfDocument) { function getDocumentCallback(pdfDocument) {
self.load(pdfDocument, scale); self.load(pdfDocument, scale);
self.loading = false; self.loading = false;
@ -1390,9 +1397,6 @@ var PDFView = {
}; };
self.error(loadingErrorMessage, moreInfo); self.error(loadingErrorMessage, moreInfo);
self.loading = false; self.loading = false;
},
function getDocumentProgress(progressData) {
self.progress(progressData.loaded / progressData.total);
} }
); );
}, },