Work Loop

workLoop is the core execution function. It pulls ready tasks from taskQueue, runs their callbacks, promotes delayed tasks when they become ready, and yields when Scheduler has used its current time slice.

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
      break;
    }

    const callback = currentTask.callback;
    if (typeof callback === "function") {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === "function") {
        currentTask.callback = continuationCallback;

        advanceTimers(currentTime);
        return true;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }

  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

Start with Delayed Tasks

The function begins by using the initialTime passed from performWorkUntilDeadline:

let currentTime = initialTime;
advanceTimers(currentTime);

Calling advanceTimers before reading taskQueue is important. A delayed task may have become ready since the last Scheduler slice. If so, it needs to move from timerQueue into taskQueue before Scheduler chooses the next task.

Pick the Next Ready Task

Scheduler then reads the highest-priority ready task:

currentTask = peek(taskQueue);
while (currentTask !== null) {
  // process currentTask
}

Because taskQueue is a min-heap ordered by expirationTime, peek(taskQueue) returns the ready task with the earliest deadline.

Yield Before Starting More Non-Expired Work

Before executing the task, Scheduler checks whether it should yield:

if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
  break;
}

This condition has two parts.

First, the task must be non-expired:

currentTask.expirationTime > currentTime

If the task is already expired, Scheduler treats it as urgent and does not yield before running it.

Second, Scheduler asks whether the current time slice has been used:

shouldYieldToHost()

In this project, the practical rule is the time budget:

const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
  return false;
}
return true;

If the task is non-expired and the time budget is used, workLoop breaks. That gives the browser a chance to process input, paint, and run other work before Scheduler continues in another event cycle.

Run the Callback

If the task has a function callback, Scheduler prepares and runs it:

const callback = currentTask.callback;
if (typeof callback === "function") {
  currentTask.callback = null;
  currentPriorityLevel = currentTask.priorityLevel;
  const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
  const continuationCallback = callback(didUserCallbackTimeout);
  currentTime = getCurrentTime();
  // ...
}

Setting currentTask.callback = null prevents the same callback from being reused accidentally. If the callback returns a continuation, Scheduler will explicitly store that continuation back on the task.

currentPriorityLevel is updated so code running inside the callback can read the active priority through getCurrentPriorityLevel.

didUserCallbackTimeout tells the callback whether the task has passed its deadline:

const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;

Then Scheduler calls the callback and updates currentTime, because the callback may have taken time to run.

Handle Continuations

A callback can return another function:

if (typeof continuationCallback === "function") {
  currentTask.callback = continuationCallback;

  advanceTimers(currentTime);
  return true;
}

That returned function is a continuation of the same task. Scheduler stores it back on currentTask.callback and returns true, which tells performWorkUntilDeadline to schedule another Scheduler slice.

Notice that Scheduler returns immediately when a continuation is produced. Even if there is still time left in the current slice, yielding here gives the host environment a chance to run before continuing a long piece of work.

Remove Finished or Cancelled Tasks

If the callback does not return a continuation, the task is complete:

if (currentTask === peek(taskQueue)) {
  pop(taskQueue);
}
advanceTimers(currentTime);

The currentTask === peek(taskQueue) check matters because the callback may have scheduled a new task while it was running. If that new task became the first item in the heap, Scheduler must not accidentally remove it.

If callback was not a function, the task was cancelled:

else {
  pop(taskQueue);
}

Cancellation is lazy. cancelCallback clears the callback, and workLoop removes the task when it reaches the front of the queue.

After each task, Scheduler checks timers again and then reads the next task:

advanceTimers(currentTime);
currentTask = peek(taskQueue);

Decide Whether More Work Remains

When the loop exits, Scheduler returns a boolean:

if (currentTask !== null) {
  return true;
} else {
  const firstTimer = peek(timerQueue);
  if (firstTimer !== null) {
    requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
  }
  return false;
}

If currentTask !== null, the loop stopped before all ready work finished. Usually that means Scheduler yielded because the time budget was used. Returning true tells performWorkUntilDeadline to schedule another host callback.

If there is no ready task left, Scheduler checks timerQueue. If a delayed task remains, it schedules a host timeout for the earliest startTime.

If both queues are empty, workLoop returns false, and the message loop stops.

Summary

workLoop is where Scheduler's main behavior comes together:

  1. Promote ready delayed tasks.
  2. Read the next task from taskQueue.
  3. Yield before non-expired work if the time budget is used.
  4. Execute the task callback.
  5. Preserve continuations.
  6. Remove completed or cancelled tasks.
  7. Return whether Scheduler needs another host callback.