Obietnica - czy można wymusić anulowanie obietnicy

91

Używam ES6 Promises do zarządzania wszystkimi moimi danymi w sieci i są sytuacje, w których muszę wymusić ich anulowanie.

Zasadniczo scenariusz jest taki, że mam wyszukiwanie z wyprzedzeniem w interfejsie użytkownika, w którym żądanie jest delegowane do zaplecza, musi przeprowadzić wyszukiwanie na podstawie częściowych danych wejściowych. Chociaż to żądanie sieciowe (nr 1) może zająć trochę czasu, użytkownik kontynuuje wpisywanie, co ostatecznie wyzwala kolejne wywołanie zaplecza (nr 2)

Tutaj numer 2 ma oczywiście pierwszeństwo przed numerem 1, więc chciałbym anulować żądanie opakowania obietnicy nr 1. Mam już pamięć podręczną wszystkich obietnic w warstwie danych, więc teoretycznie mogę ją odzyskać, próbując złożyć obietnicę nr 2.

Ale jak mogę anulować obietnicę nr 1, gdy odzyskam ją z pamięci podręcznej?

Czy ktoś mógłby zasugerować podejście?

Moonwalker
źródło
2
czy jest to opcja użycia jakiegoś odpowiednika funkcji debounce, aby nie wywoływać częstych i przestarzałych żądań? Powiedzmy, że 300 ms opóźnienia wystarczy. Na przykład Lodash ma jedną z implementacji - lodash.com/docs#debounce
shershen
Wtedy przydają się takie rzeczy jak Bacon i Rx.
elclanrs
@shershen yes - mamy to, ale nie chodzi tu tak bardzo o problem z interfejsem użytkownika ... zapytanie serwera może zająć trochę czasu, więc chcę móc anulować obietnice ...
Moonwalker
Wypróbuj Observables z Rxjs
FieryCod

Odpowiedzi:

163

Nie. Jeszcze nie możemy tego zrobić.

Obietnice ES6 nie obsługują jeszcze anulowania . Jest w drodze, a jego projekt to coś, nad czym naprawdę ciężko pracowało wiele osób. Semantyka eliminacji dźwięku jest trudna do ustalenia, a prace nad tym trwają. Trwają interesujące debaty na temat repozytorium „fetch”, esdiscussów i kilku innych repozytoriów na GH, ale byłbym cierpliwy, gdybym był tobą.

Ale, ale, ale… odwołanie jest naprawdę ważne!

To znaczy, rzeczywistość jest taka, że anulowanie jest naprawdę ważnym scenariuszem w programowaniu po stronie klienta. Przypadki, które opisujesz jako przerywanie żądań sieci Web, są ważne i są wszędzie.

Więc ... język mnie przeleciał!

Tak, przepraszam za to. Obietnice musiały pojawić się jako pierwsze, zanim dalsze rzeczy zostały określone - więc weszły bez pewnych przydatnych rzeczy, takich jak .finallyi .cancel- jednak jest w drodze do specyfikacji przez DOM. Anulowanie to nie tylko refleksja, to tylko ograniczenie czasowe i bardziej iteracyjne podejście do projektowania interfejsu API.

Więc co mogę zrobić?

Masz kilka alternatyw:

  • Skorzystaj z biblioteki innej firmy, takiej jak bluebird, która może poruszać się znacznie szybciej niż specyfikacja, a tym samym ma anulowanie, a także kilka innych gadżetów - tak robią duże firmy, takie jak WhatsApp.
  • Przekaż token anulowania .

Korzystanie z biblioteki innej firmy jest dość oczywiste. Jeśli chodzi o token, możesz sprawić, aby Twoja metoda przyjęła funkcję, a następnie ją wywołała:

function getWithCancel(url, token) { // the token is for cancellation
   var xhr = new XMLHttpRequest;
   xhr.open("GET", url);
   return new Promise(function(resolve, reject) {
      xhr.onload = function() { resolve(xhr.responseText); });
      token.cancel = function() {  // SPECIFY CANCELLATION
          xhr.abort(); // abort request
          reject(new Error("Cancelled")); // reject the promise
      };
      xhr.onerror = reject;
   });
};

Co pozwoliłoby ci:

var token = {};
var promise = getWithCancel("/someUrl", token);

// later we want to abort the promise:
token.cancel();

Twój rzeczywisty przypadek użycia - last

Nie jest to zbyt trudne z podejściem tokenowym:

function last(fn) {
    var lastToken = { cancel: function(){} }; // start with no op
    return function() {
        lastToken.cancel();
        var args = Array.prototype.slice.call(arguments);
        args.push(lastToken);
        return fn.apply(this, args);
    };
}

Co pozwoliłoby ci:

var synced = last(getWithCancel);
synced("/url1?q=a"); // this will get canceled 
synced("/url1?q=ab"); // this will get canceled too
synced("/url1?q=abc");  // this will get canceled too
synced("/url1?q=abcd").then(function() {
    // only this will run
});

I nie, biblioteki takie jak Bacon i Rx nie „błyszczą” tutaj, ponieważ są bibliotekami obserwowalnymi, mają po prostu taką samą przewagę, jaką obietnice mają biblioteki na poziomie użytkownika, ponieważ nie są ograniczone do specyfikacji. Myślę, że będziemy czekać i zobaczyć w ES2016, kiedy obserwowalne staną się natywne. Oni ładne na wpisywanie znaków z wyprzedzeniem chociaż.

Benjamin Gruenbaum
źródło
25
Benjamin, naprawdę podobała mi się twoja odpowiedź. Bardzo dobrze przemyślane, zorganizowane, elokwentne i z dobrymi praktycznymi przykładami i alternatywami. Naprawdę pomocny. Dziękuję Ci.
Moonwalker
Tokeny anulowania @FranciscoPresencia są w drodze jako propozycja pierwszego etapu.
Benjamin Gruenbaum
Gdzie możemy przeczytać informacje na temat anulowania na podstawie tego tokena? Gdzie jest propozycja?
krzywda
@harm propozycja jest martwa na etapie 1.
Benjamin Gruenbaum
1
Uwielbiam pracę Rona, ale myślę, że powinniśmy trochę poczekać, zanim przedstawimy rekomendacje dla bibliotek, których ludzie jeszcze nie używają:] Dzięki za link, chociaż sprawdzę to!
Benjamin Gruenbaum
24

Standardowe propozycje dotyczące anulowanych obietnic nie powiodły się.

Obietnica nie jest powierzchnią kontrolną dla spełniającej ją czynności asynchronicznej; myli właściciela z konsumentem. Zamiast tego utwórz funkcje asynchroniczne, które można anulować za pomocą przekazanego tokenu.

Kolejna obietnica to dobry token, dzięki czemu anulowanie jest łatwe do wdrożenia dzięki Promise.race:

Przykład: użyj, Promise.raceaby anulować efekt poprzedniego łańcucha:

let cancel = () => {};

input.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancel();
  let p = new Promise(resolve => cancel = resolve);
  Promise.race([p, getSearchResults(term)]).then(results => {
    if (results) {
      console.log(`results for "${term}"`,results);
    }
  });
}

function getSearchResults(term) {
  return new Promise(resolve => {
    let timeout = 100 + Math.floor(Math.random() * 1900);
    setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
  });
}
Search: <input id="input">

Tutaj „anulujemy” poprzednie wyszukiwania, wprowadzając undefinedwynik i testując go, ale możemy łatwo wyobrazić sobie odrzucenie za pomocą "CancelledError".

Oczywiście nie powoduje to anulowania wyszukiwania w sieci, ale jest to ograniczenie fetch. Gdyby fetchprzyjąć obietnicę anulowania jako argument, mogłoby to anulować aktywność sieciową.

Ja proponuje to „Cancel” na wzór obietnicy es-dyskutować, dokładnie na to, że fetchto zrobić.

wysięgnik
źródło
@jib, dlaczego odrzucić moją modyfikację? Po prostu to wyjaśniam.
allenyllee
8

Sprawdziłem odniesienie do Mozilla JS i znalazłem to:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race

Sprawdźmy to:

var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, "one"); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, "two"); 
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // Both resolve, but p2 is faster
});

Mamy tutaj p1 i p2 wstawione Promise.race(...)jako argumenty, to w rzeczywistości tworzenie nowej obietnicy rozwiązania, czego potrzebujesz.

nikola-miljkovic
źródło
MIŁE - może dokładnie to, czego potrzebuję. Spróbuję.
Moonwalker
Jeśli masz z tym problem, możesz wkleić kod tutaj, więc
będę
5
Spróbował tego. Nie całkiem. To rozwiązuje najszybszą obietnicę ... Muszę zawsze rozwiązać ostatnią złożoną, tj. Bezwarunkowo anulować starsze obietnice ..
Moonwalker
1
W ten sposób wszystkie inne obietnice nie są już obsługiwane, nie można faktycznie anulować obietnicy.
nikola-miljkovic
Spróbowałem, druga obietnica (jedna w tym ex) nie pozwól, aby proces zakończył się :(
morteza ataiy
3

W przypadku Node.js i Electron zdecydowanie polecam używanie Promise Extensions dla JavaScript (Prex) . Jego autor Ron Buckton jest jednym z kluczowych inżynierów TypeScript, a także autorem aktualnej propozycji anulowania ECMAScript TC39 . Biblioteka jest dobrze udokumentowana i są szanse, że część Prex spełni standard.

Osobiście i wywodząc się z C # tła, bardzo podoba mi się fakt, że Prex jest wzorowany na istniejącym frameworku Cancellation in Managed Threads , tj. W oparciu o podejście przyjęte z CancellationTokenSource/ CancellationToken. Z mojego doświadczenia wynika, że ​​były one bardzo przydatne do implementowania niezawodnej logiki anulowania w zarządzanych aplikacjach.

Sprawdziłem również, czy działa w przeglądarce, łącząc Prex za pomocą Browserify .

Oto przykład opóźnienia z anulowaniem ( Gist i RunKit , używając Prex do jego CancellationTokeni Deferred):

// by @noseratio
// https://gist.github.com/noseratio/141a2df292b108ec4c147db4530379d2
// https://runkit.com/noseratio/cancellablepromise

const prex = require('prex');

/**
 * A cancellable promise.
 * @extends Promise
 */
class CancellablePromise extends Promise {
  static get [Symbol.species]() { 
    // tinyurl.com/promise-constructor
    return Promise; 
  }

  constructor(executor, token) {
    const withCancellation = async () => {
      // create a new linked token source 
      const linkedSource = new prex.CancellationTokenSource(token? [token]: []);
      try {
        const linkedToken = linkedSource.token;
        const deferred = new prex.Deferred();
  
        linkedToken.register(() => deferred.reject(new prex.CancelError()));
  
        executor({ 
          resolve: value => deferred.resolve(value),
          reject: error => deferred.reject(error),
          token: linkedToken
        });

        await deferred.promise;
      } 
      finally {
        // this will also free all linkedToken registrations,
        // so the executor doesn't have to worry about it
        linkedSource.close();
      }
    };

    super((resolve, reject) => withCancellation().then(resolve, reject));
  }
}

/**
 * A cancellable delay.
 * @extends Promise
 */
class Delay extends CancellablePromise {
  static get [Symbol.species]() { return Promise; }

  constructor(delayMs, token) {
    super(r => {
      const id = setTimeout(r.resolve, delayMs);
      r.token.register(() => clearTimeout(id));
    }, token);
  }
}

// main
async function main() {
  const tokenSource = new prex.CancellationTokenSource();
  const token = tokenSource.token;
  setTimeout(() => tokenSource.cancel(), 2000); // cancel after 2000ms

  let delay = 1000;
  console.log(`delaying by ${delay}ms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should reach here

  delay = 2000;
  console.log(`delaying by ${delay}ms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should not reach here
}

main().catch(error => console.error(`Error caught, ${error}`));

Pamiętaj, że odwołanie to wyścig. Oznacza to, że obietnica mogła zostać pomyślnie rozwiązana, ale zanim ją wypełnisz (za pomocą awaitlub then), anulowanie również mogło zostać uruchomione. To od Ciebie zależy, jak poradzisz sobie z tym wyścigiem, ale nie boli wezwanie token.throwIfCancellationRequested()dogrywki, tak jak ja powyżej.

noseratio
źródło
1

Ostatnio miałem podobny problem.

Miałem klienta opartego na obietnicach (nie sieciowego) i zawsze chciałem podawać użytkownikowi najnowsze dane, o które prosił, aby interfejs użytkownika był płynny.

Po zmaga się z anulowania pomysłu, Promise.race(...)a Promise.all(..)ja dopiero się zaczęła moja ostatnia prośba pamiętając identyfikatora i gdy obietnica została spełniona ja renderowania tylko moje dane, gdy dopasowane id ostatnią prośbę.

Mam nadzieję, że to komuś pomoże.

Igor Słomski
źródło
Slomski nie chodzi o to, co pokazać w UI.
Chodzi
0

Możesz odmówić obietnicy przed zakończeniem:

// Our function to cancel promises receives a promise and return the same one and a cancel function
const cancellablePromise = (promiseToCancel) => {
  let cancel
  const promise = new Promise((resolve, reject) => {
    cancel = reject
    promiseToCancel
      .then(resolve)
      .catch(reject)
  })
  return {promise, cancel}
}

// A simple promise to exeute a function with a delay
const waitAndExecute = (time, functionToExecute) => new Promise((resolve, reject) => {
  timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    resolve(functionToExecute())
  }, timeInMs)
})

// The promise that we will cancel
const fetchURL = () => fetch('https://pokeapi.co/api/v2/pokemon/ditto/')

// Create a function that resolve in 1 seconds. (We will cancel it in 0.5 secs)
const {promise, cancel} = cancellablePromise(waitAndExecute(1, fetchURL))

promise
  .then((res) => {
    console.log('then', res) // This will executed in 1 second
  })
  .catch(() => {
    console.log('catch') // We will force the promise reject in 0.5 seconds
  })

waitAndExecute(0.5, cancel) // Cancel previous promise in 0.5 seconds, so it will be rejected before finishing. Commenting this line will make the promise resolve

Niestety połączenie zostało już wykonane, więc zobaczysz rozwiązanie połączenia na karcie Sieć. Twój kod po prostu go zignoruje.

Rashomon
źródło
0

Korzystając z podklasy Promise dostarczonej przez pakiet zewnętrzny, można to zrobić w następujący sposób: Demo na żywo

import CPromise from "c-promise2";

function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
    return new CPromise((resolve, reject, {signal}) => {
        fetch(url, {...fetchOptions, signal}).then(resolve, reject)
    }, timeout)
}

const chain= fetchWithTimeout('http://localhost/')
    .then(response => response.json())
    .then(console.log, console.warn);

//chain.cancel(); call this to abort the promise and releated request
Dmitriy Mozgovoy
źródło
-1

Ponieważ @jib odrzuca moją modyfikację, więc zamieszczam tutaj swoją odpowiedź. To tylko modyfikacja odpowiedzi @ jib z komentarzami i użycie bardziej zrozumiałych nazw zmiennych.

Poniżej pokazuję tylko przykłady dwóch różnych metod: jedna to solution (), druga to odrzucanie ()

let cancelCallback = () => {};

input.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancelCallback(); //cancel previous promise by calling cancelCallback()

  let setCancelCallbackPromise = () => {
    return new Promise((resolve, reject) => {
      // set cancelCallback when running this promise
      cancelCallback = () => {
        // pass cancel messages by resolve()
        return resolve('Canceled');
      };
    })
  }

  Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
    // check if the calling of resolve() is from cancelCallback() or getSearchResults()
    if (results == 'Canceled') {
      console.log("error(by resolve): ", results);
    } else {
      console.log(`results for "${term}"`, results);
    }
  });
}


input2.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancelCallback(); //cancel previous promise by calling cancelCallback()

  let setCancelCallbackPromise = () => {
    return new Promise((resolve, reject) => {
      // set cancelCallback when running this promise
      cancelCallback = () => {
        // pass cancel messages by reject()
        return reject('Canceled');
      };
    })
  }

  Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
    // check if the calling of resolve() is from cancelCallback() or getSearchResults()
    if (results !== 'Canceled') {
      console.log(`results for "${term}"`, results);
    }
  }).catch(error => {
    console.log("error(by reject): ", error);
  })
}

function getSearchResults(term) {
  return new Promise(resolve => {
    let timeout = 100 + Math.floor(Math.random() * 1900);
    setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
  });
}
Search(use resolve): <input id="input">
<br> Search2(use reject and catch error): <input id="input2">

allenyllee
źródło