javascriptnode.jsrecursionbrowsersettimeout

Prevent recursive function running setTimeout from compounding


I wrote a Chip-8 emulator in JavaScript (source) and made a playable browser version here.

It contains an HTML select:

<select>
  <option value="PONG">PONG</option>
  <option value="TETRIS">TETRIS</option>
</select>

Which loads a ROM from a file every time one is selected:

document.querySelector('select').addEventListener('change', event => {
  const rom = event.target.value
  loadRom(rom)
})

This loadRom function fetches the ROM, converts it to a useful form, and loads it into an instance of a CPU class. The cpu has a fetch-decode-execute cycle that gets called with step(). I created this cycle (main) function to call itself in a setTimeout.

const loadRom = async rom => {
  const response = await fetch(`./roms/${rom}`)
  const arrayBuffer = await response.arrayBuffer()
  const uint8View = new Uint8Array(arrayBuffer);
  const romBuffer = new RomBuffer(uint8View)

  cpu.interface.clearDisplay()
  cpu.load(romBuffer)

  let timer = 0
  async function cycle() {
    timer++
    if (timer % 5 === 0) {
      cpu.tick()
      timer = 0
    }

    await cpu.step()

    setTimeout(cycle, 3)
  }

  cycle()
}

This works fine, until I load a new ROM with the select. Now the cycle is compounded, and the game goes twice as fast. Every time you load a new ROM, it compounds again and creates a new cycle.

How can I create an infinite loop, but stop it and start a brand new one without compounding it?


Solution

  • To start with, have the current timeout to be a persistent variable, and then call clearTimeout with it right before calling loadRom. If nothing has been loaded yet, the clearTimeout just won't do anything.

    But because you have awaits as well, you'll need to check whether a new rom gets loaded while the awaits are going on. One way to accomplish this would be to have another persistent variable, the current romBuffer being used - if it's not the same as the romBuffer in the function closure, then another rom has started, so return immediately (and don't recursively create a timeout).

    let timeout;
    let currentRomBuffer;
    const loadRom = async rom => {
      const response = await fetch(`./roms/${rom}`)
      const arrayBuffer = await response.arrayBuffer()
      const uint8View = new Uint8Array(arrayBuffer);
      const romBuffer = new RomBuffer(uint8View)
      currentRomBuffer = romBuffer;
      cpu.interface.clearDisplay()
      cpu.load(romBuffer)
    
      let timer = 0
      async function cycle() {
        timer++
        if (timer % 5 === 0) {
          cpu.tick()
          timer = 0
        }
        await cpu.step();
        if (romBuffer !== currentRomBuffer) {
          return;
        }
        timeout = setTimeout(cycle, 3);
      }
      cycle()
    };