Use A+ spec compatible promises.

This commit is contained in:
Brendan Dahl 2013-06-05 12:28:31 -07:00
parent b996e1b781
commit ae1f973204
7 changed files with 240 additions and 198 deletions

View File

@ -48,7 +48,8 @@
*/
PDFJS.getDocument = function getDocument(source,
pdfDataRangeTransport,
passwordCallback) {
passwordCallback,
progressCallback) {
var workerInitializedPromise, workerReadyPromise, transport;
if (typeof source === 'string') {
@ -76,7 +77,7 @@ PDFJS.getDocument = function getDocument(source,
workerInitializedPromise = new PDFJS.Promise();
workerReadyPromise = new PDFJS.Promise();
transport = new WorkerTransport(workerInitializedPromise,
workerReadyPromise, pdfDataRangeTransport);
workerReadyPromise, pdfDataRangeTransport, progressCallback);
workerInitializedPromise.then(function transportInitialized() {
transport.passwordCallback = passwordCallback;
transport.fetchDocument(params);
@ -480,10 +481,11 @@ var PDFPageProxy = (function PDFPageProxyClosure() {
*/
var WorkerTransport = (function WorkerTransportClosure() {
function WorkerTransport(workerInitializedPromise, workerReadyPromise,
pdfDataRangeTransport) {
pdfDataRangeTransport, progressCallback) {
this.pdfDataRangeTransport = pdfDataRangeTransport;
this.workerReadyPromise = workerReadyPromise;
this.progressCallback = progressCallback;
this.commonObjs = new PDFObjects();
this.pageCache = [];
@ -705,11 +707,7 @@ var WorkerTransport = (function WorkerTransportClosure() {
}, this);
messageHandler.on('DocProgress', function transportDocProgress(data) {
// TODO(mack): The progress event should be resolved on a different
// 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({
this.progressCallback({
loaded: data.loaded,
total: data.total
});

View File

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

View File

@ -1072,13 +1072,20 @@ var PDFObjects = (function PDFObjectsClosure() {
PDFObjects.prototype = {
/**
* Internal function.
* Ensures there is an object defined for `objId`. Stores `data` on the
* object *if* it is created.
* Ensures there is an object defined for `objId`.
*/
ensureObj: function PDFObjects_ensureObj(objId, data) {
ensureObj: function PDFObjects_ensureObj(objId) {
if (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
// not required to be resolved right now
if (callback) {
this.ensureObj(objId).then(callback);
this.ensureObj(objId).promise.then(callback);
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
// data isn't ready yet!
if (!obj || !obj.isResolved)
if (!obj || !obj.resolved)
error('Requesting object that isn\'t resolved yet ' + objId);
return obj.data;
@ -1114,36 +1121,25 @@ var PDFObjects = (function PDFObjectsClosure() {
* Resolves the object `objId` with optional `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.
if (objs[objId]) {
objs[objId].resolve(data);
} else {
this.ensureObj(objId, data);
}
},
onData: function PDFObjects_onData(objId, callback) {
this.ensureObj(objId).onData(callback);
obj.resolved = true;
obj.data = data;
obj.promise.resolve(data);
},
isResolved: function PDFObjects_isResolved(objId) {
var objs = this.objs;
if (!objs[objId]) {
return false;
} else {
return objs[objId].isResolved;
return objs[objId].resolved;
}
},
hasData: function PDFObjects_hasData(objId) {
var objs = this.objs;
if (!objs[objId]) {
return false;
} else {
return objs[objId].hasData;
}
return this.isResolved(objId);
},
/**
@ -1151,22 +1147,13 @@ var PDFObjects = (function PDFObjectsClosure() {
*/
getData: function PDFObjects_getData(objId) {
var objs = this.objs;
if (!objs[objId] || !objs[objId].hasData) {
if (!objs[objId] || !objs[objId].resolved) {
return null;
} else {
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() {
this.objs = {};
}

View File

@ -649,41 +649,129 @@ function isPDFFunction(v) {
}
/**
* 'Promise' object.
* Each object that is stored in PDFObjects is based on a Promise object that
* contains the status of the object and the data. There might be situations
* where a function wants to use the value of an object, but it isn't ready at
* 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
* the callback once the object gets resolved.
* 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.
* The following promise implementation tries to generally implment the
* Promise/A+ spec. Some notable differences from other promise libaries are:
* - There currently isn't a seperate deferred and promise object.
* - Unhandled rejections eventually show an error if they aren't handled.
* - Progress events are supported.
*
* Based off of the work in:
* https://bugzilla.mozilla.org/show_bug.cgi?id=810490
*/
var Promise = PDFJS.Promise = (function PromiseClosure() {
var EMPTY_PROMISE = {};
var STATUS_PENDING = 0;
var STATUS_RESOLVED = 1;
var STATUS_REJECTED = 2;
var STATUS_PROGRESS = 3;
/**
* If `data` is passed in this constructor, the promise is created resolved.
* If there isn't data, it isn't resolved at the beginning.
*/
function Promise(name, data) {
this.name = name;
this.isRejected = false;
this.error = null;
this.exception = null;
// If you build a promise and pass in some data it's already resolved.
if (data !== null && data !== undefined) {
this.isResolved = true;
this._data = data;
this.hasData = true;
} else {
this.isResolved = false;
this._data = EMPTY_PROMISE;
// In an attempt to avoid silent exceptions, unhandled rejections are
// tracked and if they aren't handled in a certain amount of time an
// error is logged.
var REJECTION_TIMEOUT = 500;
var HandlerManager = {
handlers: [],
running: false,
unhandledRejections: [],
pendingRejectionCheck: false,
scheduleHandlers: function scheduleHandlers(promise) {
if (promise._status == STATUS_PENDING) {
return;
}
this.handlers = this.handlers.concat(promise._handlers);
if (promise._status !== STATUS_PROGRESS) {
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 (nextStatus === STATUS_PROGRESS) {
if (typeof(handler.onProgress) === 'function') {
nextValue = handler.onProgress(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
@ -700,7 +788,7 @@ var Promise = PDFJS.Promise = (function PromiseClosure() {
return deferred;
}
function reject(reason) {
if (deferred.isRejected) {
if (deferred._status === STATUS_REJECTED) {
return;
}
results = [];
@ -710,7 +798,7 @@ var Promise = PDFJS.Promise = (function PromiseClosure() {
var promise = promises[i];
promise.then((function(i) {
return function(value) {
if (deferred.isRejected) {
if (deferred._status === STATUS_REJECTED) {
return;
}
results[i] = value;
@ -722,102 +810,68 @@ var Promise = PDFJS.Promise = (function PromiseClosure() {
}
return deferred;
};
Promise.prototype = {
hasData: false,
set data(value) {
if (value === undefined) {
Promise.prototype = {
_status: null,
_value: null,
_handlers: null,
_unhandledRejection: null,
_updateStatus: function Promise__updateStatus(status, value) {
if (this._status === STATUS_RESOLVED ||
this._status === STATUS_REJECTED) {
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) {
this.onDataCallback(value);
if (status == STATUS_RESOLVED &&
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() {
if (this._data === EMPTY_PROMISE) {
error('Promise ' + this.name + ': Cannot get data that isn\'t set');
}
return this._data;
get isResolved() {
return this._status === STATUS_RESOLVED;
},
onData: function Promise_onData(callback) {
if (this._data !== EMPTY_PROMISE) {
callback(this._data);
} else {
this.onDataCallback = callback;
}
get isRejected() {
return this._status === STATUS_REJECTED;
},
resolve: function Promise_resolve(data) {
if (this.isResolved) {
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);
}
resolve: function Promise_resolve(value) {
this._updateStatus(STATUS_RESOLVED, value);
},
progress: function Promise_progress(data) {
var callbacks = this.progressbacks;
for (var i = 0, ii = callbacks.length; i < ii; i++) {
callbacks[i].call(null, data);
}
reject: function Promise_reject(reason) {
this._updateStatus(STATUS_REJECTED, reason);
},
reject: function Promise_reject(reason, exception) {
if (this.isRejected) {
error('A Promise can be rejected only once ' + this.name);
}
if (this.isResolved) {
error('The Promise was already resolved ' + this.name);
}
this.isRejected = true;
this.error = reason || null;
this.exception = exception || null;
var errbacks = this.errbacks;
for (var i = 0, ii = errbacks.length; i < ii; i++) {
errbacks[i].call(null, reason, exception);
}
notify: function Promise_notify(update) {
this._updateStatus(STATUS_PROGRESS, update);
},
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);
then: function Promise_then(onResolve, onReject, onProgress) {
var nextPromise = new Promise();
this._handlers.push({
thisPromise: this,
onResolve: onResolve,
onReject: onReject,
onProgress: onProgress,
nextPromise: nextPromise
});
HandlerManager.scheduleHandlers(this);
return nextPromise;
}
};

View File

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

View File

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

View File

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