noxy 0.0.7

HTTP forward and reverse proxy with a pluggable tower middleware pipeline
Documentation
function encodeUtf8(str) {
  const bytes = [];
  for (let i = 0; i < str.length; i++) {
    let c = str.charCodeAt(i);
    if (c < 0x80) {
      bytes.push(c);
    } else if (c < 0x800) {
      bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
    } else if (c >= 0xd800 && c <= 0xdbff) {
      const hi = c;
      const lo = str.charCodeAt(++i);
      c = 0x10000 + ((hi - 0xd800) << 10) + (lo - 0xdc00);
      bytes.push(
        0xf0 | (c >> 18),
        0x80 | ((c >> 12) & 0x3f),
        0x80 | ((c >> 6) & 0x3f),
        0x80 | (c & 0x3f)
      );
    } else {
      bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
    }
  }
  return new Uint8Array(bytes);
}

class Headers {
  #map;

  constructor(init) {
    this.#map = new Map();
    if (Array.isArray(init)) {
      for (const [k, v] of init) {
        this.#map.set(k.toLowerCase(), String(v));
      }
    }
  }

  get(name) {
    return this.#map.get(name.toLowerCase()) ?? null;
  }
  set(name, value) {
    this.#map.set(name.toLowerCase(), String(value));
  }
  delete(name) {
    this.#map.delete(name.toLowerCase());
  }
  has(name) {
    return this.#map.has(name.toLowerCase());
  }
  entries() {
    return [...this.#map.entries()];
  }
  [Symbol.iterator]() {
    return this.#map[Symbol.iterator]();
  }
}

class Request {
  #bodyConsumed = false;

  constructor(method, url, headers) {
    this.method = method;
    this.url = url;
    this.headers = new Headers(headers);
  }

  async body() {
    if (this.#bodyConsumed) throw new Error("Body already consumed");
    this.#bodyConsumed = true;
    return await Deno.core.ops.op_noxy_req_body();
  }
}

class Response {
  #bodyConsumed = false;
  #customBody;
  #fromUpstream;

  constructor(body, init) {
    init = init ?? {};
    if (typeof body === "string") {
      this.#customBody = encodeUtf8(body);
    } else if (body instanceof Uint8Array) {
      this.#customBody = body;
    } else if (body === null || body === undefined) {
      this.#customBody = new Uint8Array(0);
    } else {
      this.#customBody = body;
    }
    this.status = init.status ?? 200;
    if (init.headers instanceof Headers) {
      this.headers = init.headers;
    } else if (Array.isArray(init.headers)) {
      this.headers = new Headers(init.headers);
    } else if (init.headers && typeof init.headers === "object") {
      this.headers = new Headers(Object.entries(init.headers));
    } else {
      this.headers = new Headers([]);
    }
    this.#fromUpstream = false;
  }

  static _fromUpstream(status, headers) {
    const res = new Response(null, { status, headers });
    res.#fromUpstream = true;
    res.#customBody = undefined;
    res.#bodyConsumed = false;
    return res;
  }

  async body() {
    if (this.#bodyConsumed) throw new Error("Body already consumed");
    this.#bodyConsumed = true;
    if (this.#customBody !== undefined) return this.#customBody;
    return await Deno.core.ops.op_noxy_res_body();
  }

  get _isPassthrough() {
    return this.#fromUpstream && !this.#bodyConsumed && this.#customBody === undefined;
  }

  get _getCustomBody() {
    return this.#customBody;
  }
}

const env = {
  get(key) {
    return Deno.core.ops.op_noxy_env_get(key) ?? undefined;
  },
};

globalThis.Headers = Headers;
globalThis.Request = Request;
globalThis.Response = Response;
globalThis.env = env;
globalThis.Deno.env = env;

globalThis.__noxy_handle = async function (handler, method, url, headers) {
  const req = new Request(method, url, headers);
  let responded = false;

  async function respond(req, newBody) {
    if (responded) throw new Error("respond() already called");
    responded = true;

    const headerEntries = req.headers.entries();
    let bodyBytes = null;
    if (newBody !== undefined && newBody !== null) {
      bodyBytes =
        typeof newBody === "string"
          ? encodeUtf8(newBody)
          : newBody;
    }

    const result = await Deno.core.ops.op_noxy_respond(
      req.method,
      req.url,
      headerEntries,
      bodyBytes
    );

    return Response._fromUpstream(result.status, result.headers);
  }

  const res = await handler(req, respond);

  if (!(res instanceof Response)) {
    throw new Error("Handler must return a Response");
  }

  const headerEntries = res.headers.entries();

  if (res._isPassthrough) {
    Deno.core.ops.op_noxy_finish(res.status, headerEntries, null, true);
    return;
  }

  let body = res._getCustomBody;
  if (body === undefined) {
    body = new Uint8Array(0);
  }

  Deno.core.ops.op_noxy_finish(res.status, headerEntries, body, false);
};