In browser-based JavaScript environments (such as the FrontPanel Platform), asynchronous programming is the norm. This is largely due to the single-threaded nature of JavaScript, which must remain responsive to user interactions while handling tasks like network requests, timers, or event-driven callbacks. Asynchrony allows long-running operations to complete without freezing the UI.

However, when JavaScript is used to interact with real-world devices—such as USB peripherals, serial ports, or Bluetooth sensors—asynchronous behavior can introduce challenges. Events may arrive out of order, responses can be delayed unpredictably, and concurrent operations may compete for limited resources. Without careful coordination, this can lead to inconsistent UI updates, race conditions, or user input being ignored or misapplied.

A work queue offers a structured solution. By serializing tasks into an ordered sequence, a work queue ensures predictable, sequential execution of asynchronous operations. This can help maintain consistency between the UI and the underlying device state, simplify error handling, and reduce complexity when coordinating multiple asynchronous tasks.

Problem

Consider an example where you want to assert or de-assert a signal in the FPGA when a button is pressed or released.

Here is a naive approach to handle button press and release without using a Work Queue:

  async OnButtonDown() {
    device.setWireInValue(epAddress, 1);
    await device.updateWireIns();
  }

  async OnButtonUp() {
    device.setWireInValue(epAddress, 0);
    await device.updateWireIns();
  }Code language: JavaScript (javascript)

The problem with this approach is that if the button press and release happen close enough to each other, the instructions could interleave. This creates a race condition in the source code, where the sequence of operations may not execute as intended. For example, the interleaving could result in the actual order of operations looking like this:

device.setWireInValue(epAddress, 1);
device.setWireInValue(epAddress, 0);
await device.updateWireIns();
await device.updateWireIns();Code language: JavaScript (javascript)

The setWireInValue function stores the WireIn endpoint values internally on the Host PC side. The updateWireIns function commits these changes to the FrontPanel device. If the wire is initially at 0, and the code above sets the internal storage from 0 to 1 and then from 1 to 0, and then commits 0 to the device twice, nothing ever changes. This is the core issue: the intended change to the WireIn endpoint is effectively nullified due to the interleaving of operations.

This is the intended order of operations:

device.setWireInValue(epAddress, 1);
await device.updateWireIns();
device.setWireInValue(epAddress, 0);
await device.updateWireIns();Code language: JavaScript (javascript)

Solution: Use the Work Queue

To ensure the setWireIn and updateWireIns calls happen in the correct order, we can use the Work Queue. The Work Queue acts like a ticketing system, where each ticket represents a series of instructions to be executed by the device. These work tickets are processed one at a time, ensuring atomic execution and preventing interleaving of instructions.

Work Queue Approach

Using the Work Queue, we can perform the same assert/deassert operations but ensure they execute in the correct order. Here is the modified code:

  OnButtonDown() {
    this.props.workQueue.post(async () => {
      device.setWireInValue(epAddress, 1);
      await device.updateWireIns();
    });
  }
  
  OnButtonUp() {
    this.props.workQueue.post(async () => {
      device.setWireInValue(epAddress, 0);
      await device.updateWireIns();
    });
  }Code language: JavaScript (javascript)

Here we wrap the operations with a function to define the work ticket. Then we schedule the ticket by calling the Post method of the Work Queue and passing it the function. If it is the next ticket in the queue, the function will execute immediately. Otherwise, it will wait until all the previously queued tickets have completed. By using the Work Queue, we ensure that work tickets containing sequences of FrontPanel API calls are executed atomically, one ticket at a time. This prevents interleaving and maintains the correct execution order.

Potential Pitfalls

Overloading the WorkQueue

Issue: Placing too many tickets into the Work Queue can overwhelm the device (meaning that work tickets are arriving faster than the device can keep up), leading to performance degradation (particularly increasing latency).

Solution: Monitor and manage the number of tasks being queued to ensure the device can keep up.

Mixed Access Methods

Issue: Mixing work-queue-managed instructions with non-work-queue-managed device accesses can lead to instruction interleaving.

Solution: Ensure all device instructions are routed through the work queue to maintain atomicity.

Use the Same Instance of Work Queue

Issue: Related to the previous issue, if you post to two separate Work Queues, the tickets may still interleave.

Solution: Use the same instance of the Work Queue to ensure serialized execution of tasks.