Tengo un método de JavaScript que promete un objeto. Ese objeto debe recuperarse la primera vez, pero a partir de entonces, debe devolverse una instancia en caché. Para simular la recuperación, pondremos un retraso aquí.

var Promise = require('bluebird');
var retrievedObject = null;
var retrievalCount = 0;

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
  if (retrievedObject) {
    return retrievedObject;
  }

  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
});

En este caso, por simplicidad, solo esperamos un segundo y luego devolvemos un conteo incrementado. Si el almacenamiento en caché funciona correctamente, el recuento debe incrementarse solo una vez, mientras que todas las veces posteriores, debe mantenerse en el mismo valor, que es 1. Esto debería servir a nuestras necesidades como un mecanismo de prueba de si el código de incremento ha sido o no alcanzado o no.

Ahora, también tengo una función que se supone que prueba la recuperación del objeto, definida así:

var testRetrievals = Promise.coroutine(
function *testRetrievals() {
  var count = yield retrieveObject();
  console.log(count);
});

Al llamar a esa función varias veces, (así):

testRetrievals();
testRetrievals();
testRetrievals();
testRetrievals();
testRetrievals();

El registro de la consola debería leer:

1 1 1 1 1

Lo que realmente lee, sin embargo, es

1 2 3 4 5

La razón de eso es obvia. El método retrieveObject se llama antes de que la promesa de la primera llamada rinda porque lleva menos de un segundo que se inicien todas las llamadas. Sin embargo, estoy buscando una manera de sincronizar de alguna manera el método retrieveObject de modo que después de la primera llamada, no entre en el procedimiento de recuperación (el retraso en nuestro caso), pero espere la primera llamada para completar. ¿Cuál sería la forma más idiomática de JavaScript / Node de hacer eso? Estoy pensando en introducir estructuras parecidas a semáforos, pero me preocupa que eso rompa el comportamiento de Node que no espera.

3
arik 10 dic. 2015 a las 22:25

3 respuestas

La mejor respuesta

Estoy pensando en introducir estructuras parecidas a semáforos.

Pues sí, podrías hacer eso. Lo principal que debemos hacer es crear la ranura en nuestro caché justo cuando se llama al método, no después de esperar la recuperación.

Entonces, junto al valor del resultado para almacenar, necesitaríamos una bandera pendiente que rastrea si ya comenzamos a recuperar, y una forma de notificar a los oyentes que esperan el cambio de la bandera mientras aún no está resuelto ...
¿Suena familiar? Bien, esa es una promesa , tiene exactamente estos semáforos ya integrados.

Simplemente guarda la promesa en el caché:

var Promise = require('bluebird');
var retrievedPromise = null;
var retrievalCount = 0;

var retrieveObject = Promise.coroutine(function* retrieveObject() {
    if (retrievedPromise) {
        return yield retrievedPromise;
    }

    retrievedPromise = Promise.delay(++retrievalCount, 1000); // wait one second
    var retrievedObject = yield retrievedPromise;
    return retrievedObject;
});

De hecho, ni siquiera necesita una rutina con yield s para eso:

…
var retrievedPromise = null;
function retrieveObject() {
    if (!retrievedPromise) 
        retrievedPromise = Promise.delay(++retrievalCount, 1000); // wait one second
    return retrievedPromise;
}
1
Bergi 11 dic. 2015 a las 08:41

RetrievedObject = ++ retrievalCount; está agregando +1 en el valor aquí mismo ... si no desea este resultado, debe convertir la variable retrievalCount en local o perder la iteración ... Consulte:

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
   var retrievalCount = 0;

  if (retrievedObject) {
    return retrievedObject;
  }

  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
});
0
SZD 10 dic. 2015 a las 19:40

Aparentemente, las promesas solo pueden resolverse o rechazarse una vez. Eso significa que si reemplazamos la función generadora de promesas retrieveObject con una promesa, lo que hacemos simplemente agregando un par de paréntesis al final, de esta manera:

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
  if (retrievedObject) {
    return retrievedObject;
  }

  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
})();

retrieveObject ya no creará nuevas promesas cuando se llame, sino que será una promesa en sí misma. Después de que se resuelve por primera vez, todas las "resoluciones" posteriores serán, de hecho, respuestas instantáneas del valor que se ha almacenado en la memoria caché. Lo que significa que podemos descartar por completo la memorización que hacemos nosotros mismos:

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
})();

Eso significa que ahora, el método testRetrievals tiene que perder los paréntesis al generar retrieveObject:

var testRetrievals = Promise.coroutine(
function *testRetrievals() {
  var count = yield retrieveObject;
  console.log(count);
});

Y voilá, la salida de la consola se ve así:

1 1 1 1 1

Sin embargo, como señaló acertadamente el usuario Bergi, eso daría lugar a que la promesa siempre se llame (una vez), incluso si nunca fue requerida. Entonces, una solución alternativa sería hacer que la función retrieveObject vuelva a ser una promesa, como esta:

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
});

Y lo que hacemos ahora es que, en la función que se basa en retrieveObject, testRetrievals, recordamos la promesa durante la primera ejecución:

var retrieveObjectPromise = null;
var testRetrievals = Promise.coroutine(
function *testRetrievals() {
  if (!retrieveObjectPromise){
    retrieveObjectPromise = retrieveObject();
  }
  var count = yield retrieveObjectPromise;
  console.log(count);
});
1
arik 11 dic. 2015 a las 18:00