Ajax: async or not async? callbacks, promesses

Ha effectivement je comprends :smile:

Mais si la personne tape, s’arrête, et recommence, ben la requête elle part, non ? Tu empêches la saisie tant qu’il n’y a pas de retour ?

Après effectivement avec le timeout de 4 sec en promesse ça fonctionnait, mais il y a risque d’avoir des substitutions de données si ça traîne non ?
Édit :

Ha mais a priori les watchers Vue ne permettent pas de faire cela de base. En fait ça détecte un événement réactif, le listener du clavier est transparent.
Ou alors faire cette partie en vanilla JS, mais si je n’ai pas de souci si la requête traine et que l’utilisateur ne stresse pas l’input.

Ah oui toutafé, si tu mets un timeout à 1s par exemple, si l’utilisateur tape avant la fin de la seconde, tu clear le timeout et le relance, donc s’il s’arrête plus d’1s, la requête part.

Je te mets le bout de code que j’utilise en vanilla :

let timeout;
const timer = () => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
        tu_envoies_ta_requete();
    }, 1000);
};
some_input_field.addEventListener("input", timer);

Bon j’ai vite extrait ça d’une classe, pas testé mais c’est l’idée.

Oui c’est pas mal le truc de @romlefou. Si je devais faire un truc propre je pense que je ferai ces 3 solutions qui peuvent marcher ensemble:

  • Ne pas lancer de requête du tout si le mot ne contient pas au moins n caractères, pour éviter de balancer une requête qui renvoie des centaines de lignes.
  • Faire un timeout comme romlefou le suggère pour ne pas lancer de requêtes tant que l’utilisateur continue a taper.
  • Si tu lance une nouvelle requête et qu’une ancienne requête n’est pas terminée, tu annule l’ancienne requête avec xmlhttprequest.abort(). L’idée c’est que même si la requête n’est pas annulée côté serveur, quand elle revient côté client le retour est ignoré. Pas besoin de gérer une pile d’appel, l’idée c’est d’avoir un seul appel actif à la fois ; si on a besoin d’appeler la requête à nouveau on annule le précédent appel de sorte que le nouvel appel actif remplace l’ancien appel actif.

Ce que j’avais fait également pour optimiser le bousin, c’est qu’à partir du moment où la réponse renvoyée contient moins de 100 résultats, je les garde en mémoire et continue les recherches sur les résultats en local, comme de toute façon on écrème à chaque fois qu’on rajoute un caractère à la recherche, avec retour à la requête serveur si on enlève un caractère, et je dégage les résultats en local si nouvelle recherche ou recherche finie ou après un certain temps d’inactivité.

1 « J'aime »

Je ne peux pas. Comme ce sont majoritairement des codes recherchés à partir de 2 caractères (voire certains cas avec un caractère).

Autant je peux éventuellement faire un effet retard d’envoi, par contre comme j’utilise un watch VueJs je n’ai pas accès au listener du clavier.

Toutes les solutions trouvées ne semblent être que pour les envois unitaires et non les boucles, watcher ou autres observateurs d’évenements. Autrement dit il n’arrive pas à annuler les envois précédents. Je n’ai pas essayé CancelToken, ou peu, car déprécié. Et toutes les solutions à base d’abort se basent sur signal qui n’a pas l’air spécifique à chaque itération du get, et donc annule le get courant et pas les précédents.

La réponse ci-dessous, adaptée pour VueJs, aussi avec des piles, mais des piles d’«adresse» d’HttpRequest, ne donne pas non plus de bon résultat mais au moins j’ai réussi à l’adapter en récupérant chaque HttpRequest. Mais l’abort() de chacune dans la boucle des requêtes précédentes ne donne rien. En tout cas elle prouve qu’il faut arriver à discerner chaque envoi pour stopper le dernier avec sa bonne « adresse », car sinon ça bloque le get courant mais pas les instanciations précédentes.

Mais merci quand même :slight_smile:

Tu as combien d’éléments max? Tu ne peux pas tout charger et faire uniquement un traitement local ?

Plusieurs milliers de lignes ça fait beaucoup pour tout charger, pour des récupérations de 30 à 200 lignes en moyenne.

Contrairement à mon avant dernière solution des max(), qui ne me plaisait pas, pour l’instant ma solution avec le filtre sur la pile remplit à mon avis très bien son office, sans bavure et rapidement, une fois qu’on m’avait montré l’ordre inverse des récupérations des données.

J’ai quand même essayé ces propositions sur l’histoire des abort() car ça me paraissait intéressant sur le papier et pertinent :slight_smile: , mais bon pour moi ce souci en particulier est résolu, et surtout fiable et rapide, pour le cas d’usage que j’ai, en l’absence de POC qui fonctionne pour les abort().

Après je garde cela dans un coin de ma tête, car s’il y avait des gros volumes en réception ça poserait problème. Ça ne sera pas la dernière question que je me poserai sur la récupération asynchrone des données. :slight_smile:

Pas forcément, et ça dépend aussi de ce que chaque ligne contient.

  • Tu as essayé de voir combien de temps ça prend de tout récupérer?

  • Est-ce que dans les années à venir, le nombre total de lignes est appelé à augmenter de manière significative (genre plus de x10)?

  • Est-ce que les lignes changent souvent côté serveur, ou bien est-ce que ça change genre une fois par jour ou moins souvent? Si ça ne change pas souvent tu peux toujours stocker la liste complète dans le LocalStorage, et ne la recharger du serveur que lorsque c’est nécéssaire (si il y a un moyen rapide de savoir lorsque c’est le cas bien sûr). Il me semble que le storage est limité genre à 2MB sur certains navigateurs, donc à surveiller en fonction de la quantité de données récupérées.

Oui elles peuvent changer souvent. Ce ne sont pas des données de référence. Et puis on parle de client léger avec un serveur REST.

Pour le coup je préfère beaucoup ma solution avec des piles puisque ce sont des petites recherches que de faire grossir le navigateur.

Mais ça me fait penser à la mise en œuvre de datatable par la librairie wrapper pour R Shiny nommé DT. Qui cache tout mais côté serveur, mais au moins ne fait pas grossir le client léger. J’en parle pour continuer la discussion mais ça ne répond pas à ma question présente, par contre pour une table maitre je ne dis pas.

En fait j’avais mis du temps à comprendre. Datatable est un composant uniquement client avec des API ouvert aux paginations, recherche etc… mais dont j’ai découvert qu’il fallait tout se coltiner côté Flask contrairement à son wrapper DT.

Tandis que pour DT, c’est-à-dire un wrapper Datatable + une réécriture complète côté serveur R Shiny, on peut écrire une requête SQL + R qui ramène des milliers de ligne sans paginations(*), et c’est DT côté serveur qui pagine ce gros cache qui le file à R Shiny, qui le file à nodejs, qui est le moteur de R Shiny, qui le file à Datatable côté client le tout en paginé.

Alors qu’en général, et c’est ce que j’ai fait en Flask, il faut créer une API avec des paramètres de pagination, et créer une requête SQL faite pour découper les données par page. Ou utiliser un ORM comme SqlAlchemy. J’utilise l’un ou l’autre selon mes besoins.

Quitte à tout cacher je préfère la solution DT où on cache côté serveur.

Mais bon on s’éloigne du sujet des requêtes asynchrones :stuck_out_tongue: puisqu’en plus Datatable est synchrone. Mais c’est intéressant de discuter de toutes les mises en œuvre possible selon les besoins. :slight_smile:

(*) Comme R a été conçu au départ davantage pour les scientifiques que les informaticiens je suppose qu’ils voulaient leur éviter, dans DT pour R Shiny, les affres des paginations à la main en SQL ou de se plonger dans des ORM comme SqlAlchemy.

Et donc si on voulait faire un POC pour la problématique de n’avoir que les données du dernier appel HttpRequest, et annuler les précédentes, au lieu d’avoir un watcher spécifique à VueJs, pour le POC agnostique ES6+ il faudrait pour tester partir de cet ajout de proche en proche (que j’ai déjà utilisé dans ce topic il y a un certain nombre de messages pour une question précédente):

(async function () {
  for (let courant of await getMyDatas(mes paramètres )) {
    let transfo=FctTransfoDatas(courant)
    Mavariablelocale.push(transfo)
  }
})().then(() => {
  REACT.datasTableCible=[...Mavariablelocale]
})

et en faire cela qui au contraire ne prend que les données du dernier appel :

(async function () {
  let reqprecedent
  let reqcourant
  // en fait dans la réalité un watcher, un while true
  for (let i=1;i< 10;i++) {
    reqcourant=await GetMyHttpRequest(mes paramètres * * )
    
    if (i>1) {
      if (reqprecedent) {
         reqprecedent.abort()
      }
    }
    REACT.datasTableCible=[...reqcourant.datas]
    // sera vraisemblablement dans une pile dans le watcher: 
    reqprecedent=reqcourant
  }
})()

Je ne l’ai pas testé, c’est plus du pseudo code.

(* *)= mes paramètres qui font en sorte de ramener de moins en moins de données, pour être similaire à ce que ramène une requête liée à un champ de recherche.

Bon. J’ai trouvé sans max ni pile mais avec abort(), merci @Rabban @romlefou @Mistermick.

Le souci n’était pas sur le principe mais sur ces petits réglages techniques:

  • axios doit être en >=0.22.0 pour gérer les abort() or j’étais en 0.21.1. (Bon visiblement ils sont passés en version majeur, donc au bout du compte je suis passé en 1.2.0)
  • J’ai testé en rajoutant abortcontroller-polyfill mais finalement ce module n’est pas nécessaire: en fait les polyfill avait été supprimés d’axios mais remis pour la 0.27. En effet beaucoup de réponses sur internet parlent pour NodeJs mais oublie l’implémentation côté navigateur. Il faut donc utiliser abortcontroller-polyfill
  • signal doit être en 3é paramètre du post

et donc

  • yarn --cwd frontend/ add axios => 1.2.0
  • yarn --cwd frontend/ add abortcontroller-polyfill => 1.7.5
import axios from "axios";

// pour ApiFct.headers_authHeader()   fonction perso pour le header 
// pour { "x-access-token": user.accessToken }
import ApiFct from "@/api/api-fct.api";  
// non nécessaire pour axios >=0.27.0
// import "abortcontroller-polyfill";

let REACT = reactive({
  moninput: "",
  datasTableCible_charge: false,
  datasTableCible: [],
  datasTableCible_abortController: null,
});
watch(
  () => [REACT.moninput1, REACT.moninput2],
  async ([newRech1, newRech2], [prevRech1, prevRech2]) => {
    let datas_params = {
      recherche_1: newRech1 || "",
      recherche_2: newRech2 || "",
    };

    REACT.datasTableCible_charge = false;

    if (!(datas_params.recherche_1 == "" && datas_params.recherche_2 == "")) {
      if (REACT.datasTableCible_abortController) {
        REACT.datasTableCible_abortController.abort();
      }

      // Les données sont ramenées de façon désordonnée du fait de
      // l'environnement asynchrone,
      // mais nb restera le même qu'avant l'appel des données.
      // Par exemple selon la durée le dernier et 4e appel ajax
      // va arriver en 2é dans la pile, car rapide, mais avec un idx à 4.

      // window.AbortController, a priori pas nécessaire dans mon cas, à rajouter en cas de souci de DOM côté navigateur
      // const AbortController = window.AbortController;

      REACT.datasTableCible_abortController = new AbortController();

      const signal = REACT.datasTableCible_abortController.signal;

      let ret = await axios
        .post(
          "/ma_chaine_d_api/",
          { datas_params: datas_params },
          Object.assign({}, ApiFct.headers_authHeader(), { signal: signal })
        )
        .catch((error) => {
          // You can catch the error thrown by the polyfill here.
          if (error.message == "canceled") {
            console.log("axios Fetch Aborted");
          } else {
            return error;
          }
        });

      if (typeof ret != "undefined") {
        REACT.datasTableCible = ret.data;
        REACT.datasTableCible_abortController = null;
      }
    }
    REACT.datasTableCible_charge = true;
  }
);

Liens que j’ai utilisé

2 « J'aime »