Skip to main content

src/http_server_mock/internal/server_ffi.mjs

// Node.js HTTP server for the JS target of http_server_mock.
//
// The HTTP server and its state (stubs, recorded requests, scenarios) run in a
// dedicated Worker thread (see worker.mjs).  All exported functions are
// synchronous: they post a command to the worker and spin on a SharedArrayBuffer
// signal until the worker replies, then drain the reply with receiveMessageOnPort.
// Worker threads run in true OS threads so the main-thread spin loop does NOT
// prevent the worker from making progress.

import { Ok, Error as GError } from "../../gleam.mjs";
import { Worker, MessageChannel, receiveMessageOnPort } from "node:worker_threads";

// Atomics.wait() is not allowed on the main thread (V8/Node restriction), so
// we spin on Atomics.load(). Worker threads run as true OS threads, so the
// main-thread spin loop does NOT starve the worker.
const SPIN_TIMEOUT_MS = 5000;

function spinWait(signal) {
  const deadline = Date.now() + SPIN_TIMEOUT_MS;
  while (Atomics.load(signal, 0) === 0) {
    if (Date.now() > deadline) return false;
  }
  return true;
}

function sendCommand(handle, command, data) {
  Atomics.store(handle.signal, 0, 0);
  handle.port.postMessage({ cmd: command, data });
  if (!spinWait(handle.signal)) return null;
  Atomics.store(handle.signal, 0, 0);
  const envelope = receiveMessageOnPort(handle.port);
  return envelope ? envelope.message.result : null;
}

function waitForStartup(handle) {
  if (!spinWait(handle.signal)) return null;
  Atomics.store(handle.signal, 0, 0);
  return receiveMessageOnPort(handle.port);
}

export function startServer(port) {
  const { port1: mainPort, port2: workerPort } = new MessageChannel();
  const signalBuffer = new SharedArrayBuffer(4);
  const signal = new Int32Array(signalBuffer);

  let worker;
  try {
    worker = new Worker(new URL("./worker.mjs", import.meta.url), {
      workerData: { signalBuffer, requestedPort: port, workerPort },
      transferList: [workerPort],
    });
  } catch (err) {
    return new GError("Failed to start worker: " + err.message);
  }

  const handle = { worker, port: mainPort, signal };
  const envelope = waitForStartup(handle);
  if (!envelope) return new GError("Worker did not respond within timeout");

  const message = envelope.message;
  if (!message.ok) return new GError(message.error);

  return new Ok([message.port, handle]);
}

export function stopServer(handle) {
  if (!handle) return;
  sendCommand(handle, "stop", null);
  handle.worker.terminate();
}

export function addStub(handle, stubJson) {
  const stubId = sendCommand(handle, "addStub", stubJson);
  if (typeof stubId === "string") return new Ok(stubId);
  return new GError((stubId && stubId.error) || "Unknown error");
}

export function removeStub(handle, stubId) {
  sendCommand(handle, "removeStub", stubId);
}

export function clearStubs(handle) {
  sendCommand(handle, "clearStubs", null);
}

export function getStubs(handle) {
  return sendCommand(handle, "getStubs", null) ?? "[]";
}

export function getRequests(handle) {
  return sendCommand(handle, "getRequests", null) ?? "[]";
}

export function clearRequests(handle) {
  sendCommand(handle, "clearRequests", null);
}