data consistency when using debounce js

Viewed 129

suppose we have a debounce function to make Http request in javascript.if the second request resolves faster than the first request, how to make sure that we are showing the relevant data to the user according to his/her last input?

consider the first Request's response comes in 10000 ms and the second Request's response comes sooner (like 4000 ms) because of the async nature of the network request.

3 Answers

For racing requests, usually you want to cancel the first one before initiating the second one. This helps avoid problems like the one you encounter, and also help you get rid of the unused responses. If you are using fetch, modern browsers have pretty good support for Abort Controller.

const controller = new AbortController();
const { signal } = controller;

fetch(url, { signal })
  .then((response) => {
    // ...
  })
  .catch((e) => {
    //
  });

// To abort the fetch call
controller.abort(); // Edited, thanks Peter!

Note that when you abort a fetch call, it will results in a rejected promise, so make sure to chain a catch to it, or wrap the whole thing inside a try-catch block if you are using async-await.

try {
  await fetch(url, { signal });
} catch (error) {
  // ...
}

You could use something like RxJS that makes use of observer pattern and can run your tasks based on their priorities so that you can use debounce time with appropriate priorities and that could possibly be a solution to your question and you can also have a look at this solution for that too.

If you don't want to use RxJS and you use a state container like Redux then you can have a state to store your debounced tasks priorities and check if the last one has been done then the current one can run. You can read more about debouncing with Redux middleware here.

One could come up wit the implementation of an analogy of a never ending elimination race (e.g. an eternal heat) where one endlessly can push/add raceways to it.

Each raceway item would be presented by a closure which encapsulates a user/usage specific Promise instance, the promise specific AbortController instance and the EternalHeat specific handler which is executed every time a sprint winning (outpacing all previously pushed/added) promise resolves.

Whenever the latter case happens an EternalHeat instance's locally scoped raceways array will be mutated by slicing the outpaced raceway items from it, and for each outpaced raceway item the execution of the EternalHeat-instance sprint-end handler will be prevented by aborting the item- and promised-related AbortController instance.

// implementation of the "eternal heat" and "raceways" approach.

function initializeRaceway(raceways, promise, controller, handler) {
  promise
    .then(response => {

      raceways
        // mutate an `EternalHeat` instance's locally scoped `raceways`
        // array by slicing the outpaced `raceway` items from it ...
        .splice(0, raceways.indexOf(controller))
        // ... and for each outpaced `raceway` item prevent the execution
        // of the `EternalHeat`-instance sprint-end handler by aborting
        // the item- and promised-related `AbortController` instance.
        .forEach(controller =>
          controller.abort()
        );

      handler(response);

    }).catch(reason => {});

  raceways
    .push(controller);
}

class EternalHeat {

  constructor(sprintEndHandler) {
    const raceways = [];

    this.addRaceway = (promise, abortController) =>
      initializeRaceway(
        raceways,
        promise,
        abortController,
        sprintEndHandler,
      );
  }
}

// test case specific code

console.log(`... running ...

should sequentially log the 'userId'
related response data of ... 3, 6, 7.`);

const delayedApiRequestData = [
  ['https://jsonplaceholder.typicode.com/users/1/posts', 4600],
  ['https://jsonplaceholder.typicode.com/users/2/posts', 4300],
  ['https://jsonplaceholder.typicode.com/users/3/posts', 4000], // 1st

  ['https://jsonplaceholder.typicode.com/users/4/posts', 5600],
  ['https://jsonplaceholder.typicode.com/users/5/posts', 5300],
  ['https://jsonplaceholder.typicode.com/users/6/posts', 5000], // 2nd
]
function createDelayedAbortableApiRequest(url, delay) {
  const controller = new AbortController();
  const { signal } = controller;

  return [
    (async function delayedAbortableApiRequest() {
      await new Promise(resolve => setTimeout(resolve, delay));
      return await fetch(url, { signal });
    })(),
    controller,
  ];
}

async function handleResponseData(response) {
  const data = await response.json();
  console.log({ data });
}
const heat = new EternalHeat(handleResponseData);

setTimeout(() => heat
  .addRaceway(
    ...createDelayedAbortableApiRequest(
      'https://jsonplaceholder.typicode.com/users/7/posts', 0,  // 3rd
    )
  ),
  6000
);

delayedApiRequestData
  .map(([url, delay]) =>
    createDelayedAbortableApiRequest(url, delay)
  )
  .forEach(([promise, abortController]) =>
    heat.addRaceway(promise, abortController)
  );
.as-console-wrapper { min-height: 100%!important; top: 0; }

OP

suppose we have a debounce function to make Http request in javascript.if the second request resolves faster than the first request, how to make sure that we are showing the relevant data to the user according to his/her last input?

consider the first Request's response comes in 10000 ms and the second Request's response comes sooner (like 4000 ms) because of the async nature of the network request.

Thus, as for the OP's scenario, a user code based on the above introduced abstraction could look like the following example code ...

// implementation of the "eternal heat" and "raceways" approach.

function initializeRaceway(raceways, promise, controller, handler) {
  promise
    .then(response => {

      raceways
        // mutate an `EternalHeat` instance's locally scoped `raceways`
        // array by slicing the outpaced `raceway` items from it ...
        .splice(0, raceways.indexOf(controller))
        // ... and for each outpaced `raceway` item prevent the execution
        // of the `EternalHeat`-instance sprint-end handler by aborting
        // the item- and promised-related `AbortController` instance.
        .forEach(controller =>
          controller.abort()
        );

      handler(response);

    }).catch(reason => {});

  raceways
    .push(controller);
}

class EternalHeat {

  constructor(sprintEndHandler) {
    const raceways = [];

    this.addRaceway = (promise, abortController) =>
      initializeRaceway(
        raceways,
        promise,
        abortController,
        sprintEndHandler,
      );
  }
}


// user specific code

function createAbortableApiRequest(url) {
  const controller = new AbortController();
  const { signal } = controller;

  return [fetch(url, { signal }), controller];
}

async function handleResponseData(response) {
  const data = await response.json();
  console.log({ data });
}
const heat = new EternalHeat(handleResponseData);

// usage ... for instance ...
heat
  .addRaceway(
    ...createAbortableApiRequest(
      'https://jsonplaceholder.typicode.com/users/1/posts'
    )
  );
heat
  .addRaceway(
    ...createAbortableApiRequest(
      'https://jsonplaceholder.typicode.com/users/2/posts'
    )
  );
heat
  .addRaceway(
    ...createAbortableApiRequest(
      'https://jsonplaceholder.typicode.com/users/3/posts'
    )
  );
heat
  .addRaceway(
    ...createAbortableApiRequest(
      'https://jsonplaceholder.typicode.com/users/4/posts'
    )
  );
heat
  .addRaceway(
    ...createAbortableApiRequest(
      'https://jsonplaceholder.typicode.com/users/5/posts'
    )
  );
.as-console-wrapper { min-height: 100%!important; top: 0; }

INFO ... run the above snippet several times and most probably see different race results.

Related