miércoles, 5 de junio de 2019

Que las promesas no te controlen a ti.

Pues sí; aquí estamos tirando código Javascript que tenga que funcionar en navegadores relativamente antiguos y sin que me dejen usar Babel debido al gañanismo atávico de Españistan.

Aquí estamos usando la implementación de las promesas provistas por JQuery que deja mucho que desear. Pero a lo tonto he interiorizado varias cosas interesantes que quiero compartir.



Encadenar outputs personalizados

Cuando uno se crea sus propias promesas es fácil utilizar una estructura que se vaya pasando por todas las promesas encadenadas usando los parámetros de resolve([args]) y reject([args]) pero cuando utilizamos las promesas que retorna el framework nos encontramos que se nos fastidia la cadena de respuesta.

Por ejemplo, la función $.ajax retorna una promesa con los datos de la respuesta xmlhttprequest y si la retornamos directamente podemos controlar que la promesa ha sido resuelta o rechazada pero perderemos la cadena de información que queríamos mantener.

Pongamos el siguiente ejemplo:

function getDato1Promise(){
return $.ajax({
              method: "POST",
              dataType: "json",
              url: APIurl,
           });
}

function getDato2Promise(dato1){
return $.ajax({
    method: "POST",
    dataType: "json",
    url: APIurl,
    data: {
      dato1: dato1
    }
  });
}

function getDato3Promise(dato1, dato2){
return $.ajax({
    method: "POST",
    dataType: "json",
    url: APIurl,
    data: {
      dato1: dato1,
      dato2: dato2
    }
  });
}

getDato1Promise().done(getDato2Promise).done(getDato3Promise)

getDato1Promise obtiene el dato1 y se lo pasa por resolve(dato1) a getDato2Promise; este último usa dato1 para obtener el dato2 y todo parece que va bien hasta que nos damos cuenta que necesitamos dato1 y dato2 para que la promesa getDato3Promise funcione. Desgraciadamente a la última promesa sólo le llega el resultado de getDato2Promise que es dato2.

Para solucionar esto tenemos que controlar el resolve y reject de la promesa que retornamos y no delegar en la que utiliza $.ajax. La mejor manera que he encontrado es crear y retornar una promesa propia; la cual se resolverá o rechazará según la respuesta de los callbacks de $.ajax.

function getDato1Promise() {

  var estructura = {};

  var df = $.Deferred();
  
  $.ajax({
    method: "POST",
    dataType: "json",
    url: APIurl,
    success: function (data) {
      estructura.dato1 = data;
      df.resolve(estructura);
    },
    error: function (jqXHR, textStatus, errorThrown) {
      df.reject();
    }
  });

  return df.promise();

}

function getDato2Promise(estructura) {

  var df = $.Deferred();
  
  $.ajax({
    method: "POST",
    dataType: "json",
    url: APIurl,
    data: {
      dato1: estructura.dato1
    },
    success: function (data) {
      estructura.dato2= data;
      df.resolve(estructura);
    },
    error: function (jqXHR, textStatus, errorThrown) {
      df.reject();
    }
  });

  return df.promise();

}

function getDato3Promise(estructura) {

  var df = $.Deferred();
  
  $.ajax({
    method: "POST",
    dataType: "json",
    url: APIurl,
    data: {
      dato1: estructura.dato1,
      dato2: estructura.dato2
    },
    success: function (data) {
      estructura.dato3= data;
      df.resolve(estructura);
    },
    error: function (jqXHR, textStatus, errorThrown) {
      df.reject();
    }
  });

  return df.promise();

}

getDato1Promise().done(getDato2Promise).done(getDato3Promise).done(()=>console.dir(estructura));

Ejecutar promesas de forma secuencial

Esta necesidad es rara, muy muy rara; quizás para algo de throttling en ejecuciones asíncronas podría ser útil. En mi caso es la interacción con un programa externo que se lanza por la típica asociación de protocolo a un programa específico (UrlAssociations). Si voy demasiado rápido y lanzo las tareas asíncronas en paralelo el programa se queda frito.

Supongamos que queremos lanzar de forma secuencial tareas asíncronas que retornan una promesa por cada uno de los valores contenidos en un array (que puede ser variable).

var params = [1,2,3,4,5,6];

function doSomethingAsync(param){
return $.ajax({
    method: "POST",
    dataType: "json",
    url: APIurl,
    data: {
      paramparam
    }
  });
}

Si recorremos el array y llamamos a la función asíncrona por cada valor las estaríamos ejecutando en paralelo:

params.forEach(function(element) {
  doSomethingAsync(element);
});

Si intentamos encadenarlas tampoco funciona porque al pasarles el parámetro las estamos lanzando:

var start = $.when();
params.forEach(function(element) {
  start = start.then(doSomethingAsync(element));
});

La solución se basa en crear una funciona anónima enlazada con el elemento que le toque y utilizar esa función como callback para el then:

var start = $.when(); //comenzamos con una promesa ya resuelta
params.forEach(function(element) {
  start = start.then(() => doSomethingAsync(element));
});

Así no estamos ejecutando la función doSomethingAsync, si no que estamos creando una función que ejecuta doSomethingAsync cuando es llamada como callback del then; que es cuando la promesa anterior ya ha sido resuelta y por tanto habrá terminado.

El programador avispado ya se habrá dado cuenta de que el patrón que aquí se presenta; el de agregar el resutado a la misma variable y seguir con el siguiente calculo; es lo que hace la función reduce() de un array. Guay, vamos a usarlo para que quede más cuco:

params.reduce((previousPromise, element) => {
      return previousPromise.then(() => doSomethingAsync(element));
    }
    , $.when());

Y he terminado por hoy. Espero que os haya gustado y os sea útil de alguna manera.

Happy coding bastardillos!!!

3 comentarios: