MarkTraceur has uploaded a new change for review.
https://gerrit.wikimedia.org/r/197347
Change subject: WIP Use promises for handlers and transports
......................................................................
WIP Use promises for handlers and transports
WIP because there's a potential bug in core chunked uploads that I want
to make sure is a bug in core, then I can finish testing this.
Bug: T92640
Change-Id: I010ad9aa93999ad8a2fca5a5b4bce871f8255a7d
---
M resources/controller/uw.controller.Upload.js
M resources/mw.ApiUploadFormDataHandler.js
M resources/mw.ApiUploadHandler.js
M resources/mw.FirefoggHandler.js
M resources/mw.FirefoggTransport.js
M resources/mw.FormDataTransport.js
M resources/mw.IframeTransport.js
7 files changed, 267 insertions(+), 220 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/UploadWizard
refs/changes/47/197347/1
diff --git a/resources/controller/uw.controller.Upload.js
b/resources/controller/uw.controller.Upload.js
index 3c800bc..f9fccac 100644
--- a/resources/controller/uw.controller.Upload.js
+++ b/resources/controller/uw.controller.Upload.js
@@ -143,7 +143,7 @@
};
UP.transitionStarter = function ( upload ) {
- upload.start();
+ return upload.start();
};
uw.controller.Upload = Upload;
diff --git a/resources/mw.ApiUploadFormDataHandler.js
b/resources/mw.ApiUploadFormDataHandler.js
index a45fcbe..c1f9efa 100644
--- a/resources/mw.ApiUploadFormDataHandler.js
+++ b/resources/mw.ApiUploadFormDataHandler.js
@@ -24,21 +24,7 @@
this.transport = new mw.FormDataTransport(
this.$form[0].action,
this.formData
- ).on( 'progress', function ( evt, xhr ) {
- var fraction;
-
- if ( upload.state === 'aborted' ) {
- xhr.abort();
- return;
- }
-
- if ( evt.lengthComputable ) {
- fraction = parseFloat( evt.loaded / evt.total );
- upload.setTransportProgress( fraction );
- }
- } ).on( 'transported', function ( result ) {
- upload.setTransported( result );
- } ).on( 'update-stage', function ( stage ) {
+ ).on( 'update-stage', function ( stage ) {
upload.ui.setStatus( 'mwe-upwiz-' + stage );
} );
};
@@ -47,36 +33,46 @@
/**
* Optain a fresh edit token.
* If successful, store token and call a callback.
- * @param ok callback on success
- * @param err callback on error
+ * @return {jQuery.Promise}
*/
- configureEditToken: function ( callerOk, err ) {
- var handler = this,
- ok = function ( token ) {
- handler.formData.token = token;
- callerOk();
- };
+ configureEditToken: function () {
+ var handler = this;
- this.api.getEditToken().done( ok ).fail( err );
+ return this.api.getEditToken().then( function ( token )
{
+ handler.formData.token = token;
+ } );
},
/**
* Kick off the upload!
+ * @return {jQuery.Promise}
*/
start: function () {
- function ok() {
+ var handler = this;
+
+ return this.configureEditToken().then( function () {
handler.beginTime = ( new Date() ).getTime();
handler.upload.ui.setStatus(
'mwe-upwiz-transport-started' );
handler.upload.ui.showTransportProgress();
- handler.transport.upload( handler.upload.file );
- }
+ return handler.transport.upload(
handler.upload.file )
+ .progress( function ( evt, xhr ) {
+ var fraction;
- function err( code, info ) {
+ if ( upload.state === 'aborted'
) {
+ xhr.abort();
+ return;
+ }
+
+ if ( evt.lengthComputable ) {
+ fraction = parseFloat(
evt.loaded / evt.total );
+
handler.upload.setTransportProgress( fraction );
+ }
+ } ).then( function ( result ) {
+ handler.upload.setTransported(
result );
+ } );
+ }, function ( code, info ) {
handler.upload.setError( code, info );
- }
-
- var handler = this;
- this.configureEditToken( ok, err );
+ } );
}
};
}( mediaWiki, jQuery ) );
diff --git a/resources/mw.ApiUploadHandler.js b/resources/mw.ApiUploadHandler.js
index d0e42c6..b61c3fd 100644
--- a/resources/mw.ApiUploadHandler.js
+++ b/resources/mw.ApiUploadHandler.js
@@ -65,14 +65,12 @@
* @param callback to return true on success
*/
configureEditToken: function ( callerOk, err ) {
- function ok( token ) {
- handler.addFormInputIfMissing( 'token', token );
- callerOk();
- }
-
var handler = this;
- this.api.getEditToken().done( ok ).fail( err );
+ return this.api.getEditToken()
+ .then( function ( token ) {
+ handler.addFormInputIfMissing( 'token',
token );
+ } );
},
/**
@@ -100,23 +98,23 @@
/**
* Kick off the upload!
+ * @return {jQuery.Promise}
*/
start: function () {
- function ok() {
- handler.beginTime = ( new Date() ).getTime();
- handler.upload.ui.setStatus(
'mwe-upwiz-transport-started' );
- handler.upload.ui.showTransportProgress();
- handler.transport.getSetUpStatus().done(
function () {
- handler.$form.submit();
- } );
- }
-
- function err( code, info ) {
- handler.upload.setError( code, info );
- }
-
var handler = this;
- this.configureEditToken( ok, err );
+
+ return this.configureEditToken()
+ .then( function () {
+ var deferred = $.Deferred();
+
+ handler.beginTime = ( new Date()
).getTime();
+ handler.upload.ui.setStatus(
'mwe-upwiz-transport-started' );
+
handler.upload.ui.showTransportProgress();
+
+ return handler.transport.upload();
+ }, function ( code, info ) {
+ handler.upload.setError( code, info );
+ } );
}
};
}( mediaWiki, jQuery ) );
diff --git a/resources/mw.FirefoggHandler.js b/resources/mw.FirefoggHandler.js
index d9a4101..b56c5df 100644
--- a/resources/mw.FirefoggHandler.js
+++ b/resources/mw.FirefoggHandler.js
@@ -44,25 +44,8 @@
file,
this.api,
fogg
- ).on( 'progress', function ( data ) {
- if ( upload.state === 'aborted' ) {
- fogg.cancel();
- } else {
- upload.setTransportProgress(
data.progress );
- upload.ui.setStatus(
'mwe-upwiz-encoding' );
- }
- } ).on( 'transported', function ( result ) {
- mw.log(
'FirefoggTransport::getTransport> Transport done ' + JSON.stringify( result ) );
- upload.setTransported( result );
- } ).on( 'encoding', function () {
- upload.ui.setStatus(
'mwe-upwiz-encoding' );
- } ).on( 'starting', function ( file ) {
- upload.ui.setStatus(
'mwe-upwiz-uploading' );
- upload.file = file;
- transport.uploadHandler = new
mw.ApiUploadFormDataHandler( upload, handler.api );
- transport.uploadHandler.start();
- } );
-
+ );
+
this.transport = transport;
}
@@ -72,9 +55,13 @@
/**
* If chunks are disabled transcode then upload else
* upload and transcode at the same time
+ * @return {jQuery.Promise}
*/
start: function () {
- var title;
+ var title,
+ transport = this.getTransport(),
+ handler = this,
+ upload = this.upload;
mw.log( 'mw.FirefoggHandler::start> Upload start!' );
@@ -90,7 +77,26 @@
this.beginTime = ( new Date() ).getTime();
this.upload.ui.setStatus( 'mwe-upwiz-transport-started'
);
this.upload.ui.showTransportProgress();
- this.getTransport().doUpload();
+ return this.getTransport().upload()
+ .progress( function ( data ) {
+ if ( data === 'encoding' ) {
+ upload.ui.setStatus(
'mwe-upwiz-encoding' );
+ } else if ( upload.state === 'aborted'
) {
+ fogg.cancel();
+ } else {
+ upload.setTransportProgress(
data.progress );
+ upload.ui.setStatus(
'mwe-upwiz-encoding' );
+ }
+ } ).then( function ( file ) {
+ // Encoding finished, now transport.
+ upload.ui.setStatus(
'mwe-upwiz-uploading' );
+ upload.file = file;
+ transport.uploadHandler = new
mw.ApiUploadFormDataHandler( upload, handler.api );
+ return transport.uploadHandler.start();
+ }, function ( result ) {
+ mw.log(
'FirefoggTransport::getTransport> Transport done ' + JSON.stringify( result ) );
+ upload.setTransported( result );
+ } );
}
};
}( mediaWiki ) );
diff --git a/resources/mw.FirefoggTransport.js
b/resources/mw.FirefoggTransport.js
index 534b0c5..ee2bbc6 100644
--- a/resources/mw.FirefoggTransport.js
+++ b/resources/mw.FirefoggTransport.js
@@ -4,61 +4,55 @@
/**
* Represents a "transport" for files to upload; in this case using
Firefogg.
* @class mw.FirefoggTransport
- * @mixins OO.EventEmitter
* @constructor
* @param {File} file
* @param {mw.Api} api
* @param {Firefogg} fogg Firefogg instance
*/
mw.FirefoggTransport = function ( file, api, fogg ) {
- oo.EventEmitter.call( this );
-
this.fileToUpload = file;
this.api = api;
this.fogg = fogg;
};
- oo.mixinClass( mw.FirefoggTransport, oo.EventEmitter );
-
FTP = mw.FirefoggTransport.prototype;
/**
* Do an upload
+ * @return {jQuery.Promise}
*/
- FTP.doUpload = function () {
- var fileToUpload = this.fileToUpload, transport = this;
+ FTP.upload = function () {
+ var fileToUpload = this.fileToUpload, transport = this,
+ deferred = $.Deferred();
//Encode or passthrough Firefogg before upload
if ( this.isUploadFormat() ) {
- this.doFormDataUpload( fileToUpload );
- } else {
- this.emit( 'encoding' );
- this.fogg.encode( JSON.stringify(
this.getEncodeSettings() ),
- function (result, file) {
- result = JSON.parse(result);
- if ( result.progress === 1 ) {
- //encoding done
-
transport.doFormDataUpload(file);
- } else {
- //encoding failed
- var response = {
- error: {
- code: 500,
- info: 'Encoding
failed'
- }
- };
-
- transport.emit( 'transported',
response );
- }
- }, function ( progress ) { //progress
- transport.emit( 'progress', JSON.parse(
progress ) );
- }
- );
+ return $.Deferred().resolve( fileToUpload );
}
- };
- FTP.doFormDataUpload = function ( file ) {
- this.emit( 'starting', file );
+ deferred.notify( 'encoding' );
+
+ this.fogg.encode( JSON.stringify( this.getEncodeSettings() ),
+ function ( result, file ) {
+ result = JSON.parse(result);
+ if ( result.progress === 1 ) {
+ //encoding done
+ deferred.resolve( file );
+ } else {
+ //encoding failed
+ deferred.reject( {
+ error: {
+ code: 500,
+ info: 'Encoding failed'
+ }
+ } );
+ }
+ }, function ( progress ) {
+ deferred.notify( JSON.parse( progress ) );
+ }
+ );
+
+ return deferred.promise();
};
/**
diff --git a/resources/mw.FormDataTransport.js
b/resources/mw.FormDataTransport.js
index 36c84c4..87bb2f9 100644
--- a/resources/mw.FormDataTransport.js
+++ b/resources/mw.FormDataTransport.js
@@ -55,18 +55,19 @@
/**
* Creates an XHR and sets some generic event handlers on it.
+ * @param {jQuery.Deferred} deferred Object to send events to.
* @return XMLHttpRequest
*/
- FDTP.createXHR = function () {
+ FDTP.createXHR = function ( deferred ) {
var xhr = new XMLHttpRequest(),
transport = this;
xhr.upload.addEventListener( 'progress', function ( evt ) {
- transport.emit( 'progress', evt );
+ deferred.progress( evt, xhr );
}, false );
xhr.addEventListener( 'abort', function ( evt ) {
- transport.emitParsedResponse( evt );
+ deferred.reject( transport.parseResponse( evt ) );
}, false );
return xhr;
@@ -122,8 +123,12 @@
}
};
+ /**
+ * Start the upload with the provided file.
+ * @return {jQuery.Promise}
+ */
FDTP.upload = function ( file ) {
- var formData,
+ var formData, deferred,
transport = this;
// use timestamp + filename to avoid conflicts on server
@@ -134,28 +139,39 @@
}).join('');
if ( this.config.enableChunked && file.size > this.chunkSize ) {
- this.uploadChunk( file, 0 );
+ return this.uploadChunk( file, 0 );
} else {
- this.xhr = this.createXHR();
- this.xhr.addEventListener('load', function (evt) {
- transport.emitParsedResponse( evt );
+ deferred = $.Deferred();
+ this.xhr = this.createXHR( deferred );
+ this.xhr.addEventListener( 'load', function ( evt ) {
+ deferred.resolve( transport.parseResponse( evt
) );
}, false);
- this.xhr.addEventListener('error', function (evt) {
- transport.emitParsedResponse( evt );
+ this.xhr.addEventListener( 'error', function ( evt ) {
+ deferred.reject( transport.parseResponse( evt )
);
}, false);
formData = this.createFormData( this.tempname );
formData.append( 'file', file );
this.sendData( this.xhr, formData );
+
+ return deferred.promise();
}
};
+ /**
+ * Upload a single chunk.
+ * @param {File} file
+ * @param {number} offset Offset in bytes.
+ * @return {jQuery.Promise}
+ */
FDTP.uploadChunk = function ( file, offset ) {
var formData,
+ deferred = $.Deferred(),
transport = this,
bytesAvailable = file.size,
chunk;
+
if ( this.aborted ) {
if ( this.xhr ) {
this.xhr.abort();
@@ -172,55 +188,9 @@
chunk = file.slice(offset, offset + this.chunkSize,
file.type);
}
- this.xhr = this.createXHR();
- this.xhr.addEventListener('load', function (evt) {
- transport.responseText = evt.target.responseText;
- transport.parseResponse(evt, function (response) {
- if (response.upload && response.upload.filekey)
{
- transport.filekey =
response.upload.filekey;
- }
- if (response.upload && response.upload.result
=== 'Success') {
- //upload finished and can be unstashed
later
- transport.emit( 'transported', response
);
- } else if (response.upload &&
response.upload.result === 'Poll') {
- //Server not ready, wait for 3 seconds
- setTimeout(function () {
- transport.checkStatus();
- }, 3000);
- } else if (response.upload &&
response.upload.result === 'Continue') {
- //reset retry counter
- transport.retries = 0;
- //start uploading next chunk
- transport.uploadChunk( file,
response.upload.offset );
- } else {
- //failed to upload, try again in 3
seconds
- transport.retries++;
- if (transport.maxRetries > 0 &&
transport.retries >= transport.maxRetries) {
- mw.log.warn( 'Max retries
exceeded on unknown response' );
- //upload failed, raise response
- transport.emit( 'transported',
response );
- } else {
- mw.log( 'Retry #' +
transport.retries + ' on unknown response' );
- setTimeout(function () {
- transport.uploadChunk(
file, offset );
- }, 3000);
- }
- }
- });
- }, false);
- this.xhr.addEventListener('error', function (evt) {
- //failed to upload, try again in 3 second
- transport.retries++;
- if (transport.maxRetries > 0 && transport.retries >=
transport.maxRetries) {
- mw.log.warn( 'Max retries exceeded on error
event' );
- transport.emitParsedResponse( evt );
- } else {
- mw.log( 'Retry #' + transport.retries + ' on
error event' );
- setTimeout(function () {
- transport.uploadChunk( file, offset );
- }, 3000);
- }
- }, false);
+ this.xhr = this.createXHR( deferred );
+ this.xhr.addEventListener( 'load', deferred.resolve, false );
+ this.xhr.addEventListener( 'error', deferred.reject, false );
formData = this.createFormData( this.tempname, offset );
@@ -240,8 +210,99 @@
}
this.sendData( this.xhr, formData );
+
+ return deferred.promise().then( function ( evt ) {
+ return transport.parseResponse( evt );
+ }, function ( evt ) {
+ return transport.parseResponse( evt );
+ } ).then( function ( response ) {
+ if ( response.upload && response.upload.result ) {
+ switch ( response.upload.result ) {
+ case 'Continue':
+ // Reset retry counter
+ transport.retries = 0;
+ // Start uploading next chunk
+ return transport.uploadChunk(
file, response.upload.offset );
+ case 'Success':
+ // Just pass the response
through.
+ return response;
+ case 'Poll':
+ // Need to retry with
checkStatus.
+ return
transport.retryWithMethod( 'checkStatus' );
+ }
+ } else {
+ // Failed to upload, try again in 3 seconds
+ return transport.maybeRetry(
+ 'on unknown response',
+ response,
+ 'uploadChunk',
+ file, offset
+ );
+ }
+ }, function ( response ) {
+ return transport.maybeRetry(
+ 'on error event',
+ response,
+ 'uploadChunk',
+ file, offset
+ );
+ } );
};
+ /**
+ * Handle possible retry event - rejected if maximum retries already
fired.
+ * @param {string} contextMsg
+ * @param {Object} response
+ * @param {string} retryMethod
+ * @param {File} [file]
+ * @param {number} [offset]
+ * @return {jQuery.Promise}
+ */
+ FDTP.maybeRetry = function ( contextMsg, response, retryMethod, file,
offset ) {
+ var transport = this;
+
+ this.retries++;
+
+ if ( this.tooManyRetries() ) {
+ mw.log.warn( 'Max retries exceeded ' + contextMsg );
+ return $.Deferred().reject( response );
+ } else {
+ mw.log( 'Retry #' + this.retries + ' ' + contextMsg );
+ return this.retryWithMethod( retryMethod, file, offset
);
+ }
+ };
+
+ /**
+ * Have we retried too many times already?
+ * @return {boolean}
+ */
+ FDTP.tooManyRetries = function () {
+ return this.maxRetries > 0 && this.retries >= this.maxRetries;
+ };
+
+ /**
+ * Either retry uploading or checking the status.
+ * @param {'uploadChunk'|'checkStatus'} methodName
+ * @param {File} [file]
+ * @param {number} [offset]
+ * @return {jQuery.Promise}
+ */
+ FDTP.retryWithMethod = function ( methodName, file, offset ) {
+ var retryDeferred,
+ transport = this;
+
+ retryDeferred = $.Deferred();
+ setTimeout( function () {
+ transport[methodName]( file, offset ).then(
retryDeferred.resolve, retryDeferred.reject );
+ }, 3000 );
+
+ return retryDeferred.promise();
+ };
+
+ /**
+ * Check the status of the upload.
+ * @return {jQuery.Promise}
+ */
FDTP.checkStatus = function () {
var transport = this,
params = {};
@@ -258,41 +319,40 @@
});
params.checkstatus = true;
params.filekey = this.filekey;
- this.api.post( params )
- .done( function (response) {
- if (response.upload && response.upload.result
=== 'Poll') {
+ return this.api.post( params )
+ .then( function ( response ) {
+ if ( response.upload && response.upload.result
=== 'Poll' ) {
//If concatenation takes longer than 10
minutes give up
if ( ( ( new Date() ).getTime() -
transport.firstPoll ) > 10 * 60 * 1000 ) {
- transport.emit( 'transported', {
+ return $.Deferred().reject( {
code: 'server-error',
info: 'unknown server
error'
} );
- //Server not ready, wait for 3 more
seconds
} else {
if ( response.upload.stage ===
undefined && window.console ) {
window.console.log(
'Unable to check file\'s status' );
+ return
$.Deferred().reject();
} else {
//Statuses that can be
returned:
// * queued
// * publish
// * assembling
transport.emit(
'update-stage', response.upload.stage );
- setTimeout(function () {
-
transport.checkStatus();
- }, 3000);
+ return
transport.retryWithMethod( 'checkStatus' );
}
}
- } else {
- transport.emit( 'transported', response
);
}
- } )
- .fail( function (status, response) {
- transport.emit( 'transported', response );
} );
};
- FDTP.parseResponse = function ( evt, cb ) {
+ /**
+ * Parse response from the server.
+ * @param {Event} evt
+ * @return {Object}
+ */
+ FDTP.parseResponse = function ( evt ) {
var response;
+
try {
response = $.parseJSON(evt.target.responseText);
} catch ( e ) {
@@ -304,20 +364,7 @@
};
}
- cb( response );
- };
-
- /**
- * Emits a 'transported' event with an object indicating status,
- * parsed from JSON in the event body.
- * @param {Event} evt The response event.
- */
- FDTP.emitParsedResponse = function ( evt ) {
- var transport = this;
-
- this.parseResponse( evt, function ( response ) {
- transport.emit( 'transported', response );
- } );
+ return response;
};
FDTP.geckoFormData = function () {
diff --git a/resources/mw.IframeTransport.js b/resources/mw.IframeTransport.js
index 063c97a..90cb719 100644
--- a/resources/mw.IframeTransport.js
+++ b/resources/mw.IframeTransport.js
@@ -17,7 +17,6 @@
oo.EventEmitter.call( this );
function setupFormCallback() {
- transport.configureForm();
transport.$iframe.off( 'load', setupFormCallback );
transport.setUpStatus.resolve();
}
@@ -71,29 +70,6 @@
};
/**
- * Configure a form with a File Input so that it submits to the iframe
- * Ensure callback on completion of upload
- */
- ITP.configureForm = function () {
- var transport = this;
-
- // Set the form target to the iframe
- this.$form.prop( 'target', this.iframeId );
-
- // attach an additional handler to the form, so, when
submitted, it starts showing the progress
- // XXX this is lame .. there should be a generic way to
indicate busy status...
- this.$form.submit( function () {
- return true;
- } );
-
- // Set up the completion callback
- this.$iframe.load( function () {
- transport.emit( 'progress', 1.0 );
- transport.processIframeResult( this );
- } );
- };
-
- /**
* Process the result of the form submission, returned to an iframe.
* This is the iframe's onload event.
*
@@ -136,6 +112,36 @@
}
// Process the API result
- this.emit( 'transported', response );
+ return response;
+ };
+
+ /**
+ * Start the upload.
+ * @return {jQuery.Promise}
+ */
+ ITP.upload = function () {
+ var transport = this;
+
+ return this.getSetUpStatus().then( function () {
+ var deferred = $.Deferred();
+
+ // Set the form target to the iframe
+ transport.$form.prop( 'target', this.iframeId );
+
+ // attach an additional handler to the form, so, when
submitted, it starts showing the progress
+ // XXX this is lame .. there should be a generic way to
indicate busy status...
+ transport.$form.submit( function () {
+ return true;
+ } );
+
+ transport.$iframe.on( 'load', function () {
+ deferred.notify( 1.0 );
+ deferred.resolve(
transport.processIframeResult() );
+ } );
+
+ transport.$form.submit();
+
+ return deferred.promise();
+ } );
};
}( mediaWiki, jQuery, OO ) );
--
To view, visit https://gerrit.wikimedia.org/r/197347
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I010ad9aa93999ad8a2fca5a5b4bce871f8255a7d
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/UploadWizard
Gerrit-Branch: master
Gerrit-Owner: MarkTraceur <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits