Frontend Labs
⋯

public/js/lib/loop-model.js

// A model of the HTML event loop (§12, §81): it "executes" a snippet
// described as a list of operations and records a snapshot after every
// step: the call stack, timers held by the browser, the task queue, the
// microtask queue, and the console.
//
// Operations (`at` is a substring of the source line, for highlighting):
//   { at, log: "text" }
//   { at, timeout: { delay, label, body } }       setTimeout(fn, delay)
//   { at, then:    { label, body, next? } }       Promise.resolve().then(fn)[.then(next)]
//   { at, micro:   { label, body } }              queueMicrotask(fn)
//   { at, call:    { name, body } }               fn() — sync call (async fns too)
//   { at, await:   { label, rest } }              await <settled value>: the rest of the
//                                                 function becomes one microtask

export function simulate(ops, source = "") {
  const lines = source.split("\n");
  const lineOf = (at) => (at ? lines.findIndex((l) => l.includes(at)) : -1);
  const S = { stack: [], webapis: [], tasks: [], micro: [], console: [], now: 0 };
  const steps = [];
  let seq = 0;

  const snap = (kind, note, at) =>
    steps.push({
      kind,
      note,
      line: lineOf(at),
      stack: [...S.stack],
      webapis: S.webapis.map((t) => `${t.label} (${t.delay}ms)`),
      tasks: S.tasks.map((t) => t.label),
      micro: S.micro.map((m) => m.label),
      console: [...S.console],
    });

  function exec(body) {
    for (const op of body) {
      if ("log" in op) {
        S.console.push(op.log);
        snap("log", `console.log prints "${op.log}"`, op.at);
      } else if (op.timeout) {
        const t = op.timeout;
        S.webapis.push({ ...t, due: S.now + t.delay, seq: seq++ });
        snap("webapi", `setTimeout hands "${t.label}" to the browser's timer (${t.delay}ms). JS moves on immediately.`, op.at);
      } else if (op.then) {
        S.micro.push(op.then);
        snap("micro", `The promise is already resolved, so "${op.then.label}" goes straight onto the microtask queue.`, op.at);
      } else if (op.micro) {
        S.micro.push(op.micro);
        snap("micro", `queueMicrotask puts "${op.micro.label}" on the microtask queue.`, op.at);
      } else if (op.call) {
        S.stack.push(`${op.call.name}()`);
        snap("push", `Calling ${op.call.name}() pushes a new frame onto the call stack.`, op.at);
        exec(op.call.body);
        S.stack.pop();
        snap("pop", `${op.call.name}() returns: its frame is popped.`, op.at);
      } else if (op.await) {
        S.micro.push({ label: op.await.label, body: op.await.rest });
        snap("micro", `await suspends the function. Its continuation "${op.await.label}" is queued as a microtask.`, op.at);
        return; // the rest of this body runs later, as that microtask
      }
    }
  }

  function drainMicrotasks() {
    if (!S.micro.length) return;
    snap("checkpoint", "The call stack is empty: microtask checkpoint. Drain the WHOLE microtask queue, including microtasks queued along the way.");
    while (S.micro.length) {
      const m = S.micro.shift();
      S.stack.push(m.label);
      snap("run-micro", `Run microtask "${m.label}".`, m.at);
      exec(m.body);
      S.stack.pop();
      if (m.next) {
        S.micro.push(m.next);
        snap("micro", `"${m.label}" returned, which resolves the next promise in the chain: "${m.next.label}" is queued.`);
      } else {
        snap("pop", `Microtask "${m.label}" done.`);
      }
    }
  }

  function moveExpiredTimers() {
    const due = S.webapis.filter((t) => t.due <= S.now).sort((a, b) => a.due - b.due || a.seq - b.seq);
    for (const t of due) {
      S.webapis.splice(S.webapis.indexOf(t), 1);
      S.tasks.push(t);
      snap("task-queued", `Timer "${t.label}" expired: its callback joins the task (macrotask) queue.`);
    }
  }

  S.stack.push("<script>");
  snap("task", "The whole <script> is the first task. It runs to completion on the call stack.");
  exec(ops);
  S.stack.pop();
  snap("pop", "The script is done and the call stack is empty.");
  drainMicrotasks();

  for (let guard = 0; guard < 500; guard++) {
    moveExpiredTimers();
    if (!S.tasks.length) {
      if (!S.webapis.length) break;
      S.now = Math.min(...S.webapis.map((t) => t.due));
      continue;
    }
    const task = S.tasks.shift();
    S.stack.push(task.label);
    snap("task", `The event loop takes ONE task from the task queue: "${task.label}".`);
    exec(task.body);
    S.stack.pop();
    snap("pop", `Task "${task.label}" done.`);
    drainMicrotasks();
  }
  snap("idle", "Every queue is empty. The event loop sleeps until the next event, timer or network response.");
  return { steps, output: S.console };
}

Generated from SOURCE.md · 8 Oct 2026 16:36