Search code examples
javascriptreactjsreact-hookses6-modules

Pause and unpause async function


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;

Solution

  • 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>
    }