1

I have an async function which I used in sorting visualizer. I want to add a feature of pause and unpause. I tried to use a while(isTrue){} (isTrue is a usestate variable) but this method is making the page unresponsive. Is there any better way to add this feature?

   import asyncSetTimeout from '../helpers/asyncSetTimeout';

const bubbleSort = async ({
  array,
  setArray,
  setColorsArray,
  visualizationSpeed,
  setI,
  setJ,
  setNum1,
  setNum2,
  comparisons,
  setComparisons,
  swaps,
  setswaps,
  isTrue
} = {}) => {
  comparisons=0;
  swaps=0;
  let len = array.length;
  for (let i = 0; i < len - 1; i++) {
    setI(i);
    for (let j = 0; j < len - 1 - i; j++) {
      setJ(j);
      let newColorsArray = new Array(len).fill(0);
      newColorsArray[len - 1 - i] = 3;
      newColorsArray[j] = 1;
      newColorsArray[j + 1] = 2;
      setColorsArray(newColorsArray);
      await asyncSetTimeout({
        timeout: 10 * visualizationSpeed
      });
      setNum1(array[j]);
      setNum2(array[j + 1]);
      comparisons++;
      setComparisons(comparisons)
      if (array[j + 1] < array[j]) {
        let temp = array[j + 1];
        array[j + 1] = array[j];
        array[j] = temp;
        swaps++;
        setswaps(swaps)
        setArray(array);
      }
      await asyncSetTimeout({
        timeout: 10 * visualizationSpeed
      })
      while(isTrue){}
      console.log(isTrue);
    }
  }
  setColorsArray([])
};

export default bubbleSort;
Ayush
  • 49
  • 2
  • 9
  • 1
    What unpauses or resumes your loop? I would thing creating a promise that you await, and resolve elsewhere would be the best way to pause/unpause. I'm happy to give an example if I know where it needs to be resumed from, or a generic example if it's not important to be specific to the way this code is functioning. – async await May 01 '23 at 18:46
  • Your function has way too many parameters. Instead of all these setters, it should take a single setter that you either pass the whole state or an identifier and the value. – Bergi May 01 '23 at 19:31
  • `while(isTrue){}` cannot work, there is no code inside the loop body that would change `isTrue` so it becomes an infinite loop. – Bergi May 01 '23 at 19:32
  • For demo please visit https://master--timely-gelato-d3ed4f.netlify.app/ – Ayush May 02 '23 at 14:30
  • @Bergi i want something in place of while(isTrue){} line that pauses above function for an indefinite time and then resumes after we set the isTrue value to false – Ayush May 02 '23 at 14:34
  • @Ayush Yes. Go with Mulan's approach. – Bergi May 02 '23 at 15:27

1 Answers1

3

start from basics

I would recommend a generator -

function* bubbleSort(array) {
  yield array
  let swapping = true
  while (swapping) {
    swapping = false
    for (let i = 0; i < array.length - 1; i++) {
      if (array[i] > array[i + 1]) {
        [array[i], array[i + 1]] = [array[i + 1], array[i]]
        swapping = true
        yield array
      }
    }
  }
}

Now you can use for..of to step through the generator, displaying one step at a time -

const array = [5, 3, 1, 4, 2];
for (const step of bubbleSort(array)) {
  console.log(step); // ✅ display one unit of progress
}

To add pause behaviour, we can define a controller that allows us to interrupt, pause,

let controller = // ... ✅

const array = [5, 3, 1, 4, 2];

for (const step of bubbleSort(array)) {
  if (controller.cancelled) break // ✅
  await controller.promise // ✅
  console.log(step);
}

controller.pause()   // ✅ pause action
controller.unpause() // ✅ unpause action
controller.cancel()  // ✅ cancel action

Implementation of the controller might look like this -

const controller = {
  cancelled: false,
  promise: null,
  pause() {
    this.unpause()
    this.promise = new Promise(r => this.unpause = r)
  },
  unpause() {},
  cancel() {
    this.cancelled = true
  }
}

vanilla demo

Run the demo and verify the result -

function* bubbleSort(array) {
  yield array
  let swapping = true
  while (swapping) {
    swapping = false
    for (let i = 0; i < array.length - 1; i++) {
      if (array[i] > array[i + 1]) {
        [array[i], array[i + 1]] = [array[i + 1], array[i]]
        swapping = true
        yield array
      }
    }
  }
}

async function main(form, arr, controller) {
  for (const sortedArray of bubbleSort(arr)) {
    if (controller.cancelled) break
    await controller.promise
    form.output.value += JSON.stringify(sortedArray) + "\n"
    await sleep(700)
  }
  form.output.value += "done\n"
}

async function sleep(ms) {
  return new Promise(r => setTimeout(r, ms))
}

const array = [5, 8, 3, 6, 1, 7, 4, 2]

const controller = {
  cancelled: false,
  promise: null,
  pause() {
    this.unpause()
    this.promise = new Promise(r => this.unpause = r)
  },
  unpause() {},
  cancel() {
    this.cancelled = true
  }
}

const f = document.forms[0]
f.pause.addEventListener("click", e => controller.pause())
f.unpause.addEventListener("click", e => controller.unpause())
f.cancel.addEventListener("click", e => controller.cancel())

main(f, array, controller).catch(console.error)
<form>
  <button type="button" name="pause">pause</button>
  <button type="button" name="unpause">unpause</button>
  <button type="button" name="cancel">cancel</button>
  <pre><output name="output"></output></pre>
</form>

react

To make this compatible with React, we have to make a few changes. Most notably, bubbleSort should not mutate its input. But we'll also yield the indices which have changed at each step so we can create a better visualization -

App preview
app-preview
bubbleSort React demo
function* bubbleSort(t) {
  yield [t, -1, -1]            // ✅ yield starting point
  let swapping = true
  while (swapping) {
    swapping = false
    for (let i = 0; i < t.length- 1; i++) {
      if (t[i] > t[i + 1]) {
        t = [                  //
          ...t.slice(0, i),    //
          t.at(i + 1),         // ✅ immutable swap
          t.at(i),             //
          ...t.slice(i + 2)    // 
        ]
        swapping = true
        yield [t, i, i + 1]    // ✅ yield array and indices
      }
    }
  }
}

The controller is moved into a useController hook -

function useController() {
  return React.useMemo(
    () => ({
      cancelled: false,
      promise: null,
      pause() {
        this.unpause()
        this.promise = new Promise(r => this.unpause = r)
      },
      unpause() {},
      cancel() {
        this.cancelled = true
      }
    }),
    [] // no dependencies
  )
}

Finally the component has steps and done state, a controller instance, and an effect to step through the generator. Note the careful use of mounted to ignore stale component updates -

function App({ unsortedArray = [] }) {
  const [steps, setSteps] = React.useState([])
  const [done, setDone] = React.useState(false)
  const controller = useController()
  React.useEffect(() => {
    let mounted = true
    async function main() {
      mounted && setDone(false)
      mounted && setSteps([])
      for (const step of bubbleSort(unsortedArray)) {
        if (controller.cancelled) break
        await controller.promise
        mounted && setSteps(s => [...s, step])
        await sleep(700)
      }
    }
    main()
      .then(() => mounted && setDone(true))
      .catch(console.error)
    return () => { mounted = false }
  }, [unsortedArray, controller])

  return <div>
    <button onClick={e => controller.pause()} children="pause" />
    <button onClick={e => controller.unpause()} children="unpause" />
    <button onClick={e => controller.cancel()} children="cancel" />
    <pre>
      {steps.map(([data, i, j]) =>
        <>
          [ {data.map((n, index) =>
            index == i || index == j
              ? <span style={{color: "red", textDecoration: "underline"}}>{n}, </span>
            :   <span style={{color: "silver"}}>{n}, </span>
          )}]{"\n"}
        </>
      )}
      {done && "done"}
    </pre>
  </div>
}
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • No point in making the generator `async` if it doesn't `await` anything. You can still run a normal generator asynchronously. – Bergi May 01 '23 at 19:28
  • 1
    thanks for review Bergi. I was in a bit of a rush posting this, I'll edit it now – Mulan May 01 '23 at 23:19
  • @Mulan is there any other way to to pause the async function rather than the generator? – Ayush May 02 '23 at 14:37
  • @Ayush no. once a promise is in flight, you have extremely limited options to interact with it. I made an update that includes a functioning React component. – Mulan May 02 '23 at 14:59
  • What if we use a promise i.e if isTrue is true then we reject it and after it become false we resolve and then resumes the function? – Ayush May 02 '23 at 15:05
  • 1
    @Ayush That's exactly what `if (controller.cancelled) break; await controller.promise;` is doing. You can also place this directly inside the `bubbleSort` function instead of the `yield`, but the generator approach is much more elegant and a better separation of concerns. – Bergi May 02 '23 at 15:30
  • @Mulan maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. – Ayush May 03 '23 at 12:24
  • 1
    You'll have to open a new question or paste updated code. It sounds like this error does not apply to the code in the answer because the effect does *not* have an empty dependency array and the dependencies do not change over the lifetime of the component. Beyond that, `setState` is called once per 700ms. So something you're doing is different and I can't debug it from an error message alone. – Mulan May 03 '23 at 13:37
  • 1
    Btw, the error you pasted is a very popular Q&A on the site: see [this](https://stackoverflow.com/q/57853288/633183) and [this](https://stackoverflow.com/questions/63243216/error-maximum-update-depth-exceeded-this-can-happen-when-a-component-calls-set). This type of error happens when the effect dependency changes every render or `setState` is called thousands of times between renders. – Mulan May 03 '23 at 13:40
  • @Mulan why immutable swap? – Ayush May 03 '23 at 15:45
  • @Mulan can you help me how to add a pause functionality in my code (change in my code rather than an example)? – Ayush May 03 '23 at 15:48
  • @Ayush why immutable swap? because react expects underlying data not to change. using mutable data in rea[ct requires special care and comes with certain restrictions. – Mulan May 03 '23 at 21:12
  • 1
    @Ayush no i cannot help your code because it is unsalvageable. you turned a one-argument function into a function that takes 13 arguments. there is no separation of concerns. i provided two complete functioning examples above. if you have a specific question about anything demonstrated here, i'm happy to help. i cannot debug your issues in comments. i have no idea where you are getting stuck. – Mulan May 03 '23 at 21:14