i2psnark/res/js/tunnelCounter.js

/**
 * @module tunnelCounter
 * @file tunnelCounter.js - Counts active in/out I2PSnark tunnels and displays them in the UI.
 * @description Periodically fetches tunnel data from the I2P tunnel configuration page, counts
 * active inbound and outbound snark tunnels, and injects the counts into the UI via CSS
 * pseudo-element content. Fetches and extracts through the snarkWork worker, falling back
 * to a direct fetch when the worker is unavailable. Requires I2P+ and does not work with
 * standalone I2PSnark.
 * @author dr|z3d
 * @license AGPL3 or later
 */

import {fetchTunnelCounts, isDocumentVisible} from "./refreshTorrents.js";
import {extractTunnelCounts} from "./refreshPayload.js";

/**
 * @type {Object}
 * @description Cached tunnel count data with a timestamp to avoid excessive fetches.
 * @property {number} timestamp - Timestamp of the last successful fetch in milliseconds.
 * @property {?Object} data - The cached tunnel count data, or null if not yet fetched.
 */
const cachedTunnelCounts = { timestamp: 0, data: null };

/**
 * @type {string}
 * @description Label text for the inbound tunnel count badge.
 */
let inLabel = "";

/**
 * @type {string}
 * @description Label text for the outbound tunnel count badge.
 */
let outLabel = "";

/**
 * @async
 * @function fetchTunnelData
 * @description Fetches the I2P tunnel configuration page and extracts the snark inbound
 * and outbound tunnel counts from the #snarkIn and #snarkOut elements. Prefers the
 * snark worker so the download and extraction happen off the main thread, falling back
 * to a direct fetch with a timeout when the worker is unavailable.
 * @param {string} [url="/configtunnels"] - The URL of the tunnel configuration page.
 * @param {number} [timeout=10000] - Request timeout in milliseconds for the direct fallback.
 * @returns {Promise<?Object>} An object with inCount and outCount properties, or null on failure.
 */
async function fetchTunnelData(url = "/configtunnels", timeout = 10000) {
  if (!isDocumentVisible) {return;}
  try {
    const counts = await fetchTunnelCounts();
    if (counts) {return counts;}
    return null;
  } catch (error) {}
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) { throw new Error(response.statusText); }
    return extractTunnelCounts(await response.text());
  } catch (error) {}
  finally { clearTimeout(id); }
  return null;
}

/**
 * @async
 * @function getSnarkTunnelCount
 * @description Returns cached tunnel count data if within the specified interval,
 * otherwise fetches fresh data from the server and updates the cache.
 * @param {number} [interval=30000] - Minimum interval in milliseconds between fetches.
 * @returns {Promise<?Object>} The tunnel count data with inCount and outCount, or null on failure.
 */
async function getSnarkTunnelCount(interval = 30000) {
  if (cachedTunnelCounts.timestamp + interval > Date.now()) return cachedTunnelCounts.data;
  cachedTunnelCounts.data = await fetchTunnelData();
  cachedTunnelCounts.timestamp = Date.now();
  return cachedTunnelCounts.data;
}

/**
 * @function updateTunnelCounts
 * @description Updates the UI badges for inbound and outbound tunnel counts by injecting
 * CSS content rules. Creates a style element if one doesn't exist, and only updates
 * the content if the values have changed. Without tunnel data the badges show 0.
 * @param {?Object} result - The tunnel count data with inCount and outCount properties.
 * @returns {void}
 */
function updateTunnelCounts(result) {
  const snarkInCount = document.querySelector("#tnlInCount .badge");
  const snarkOutCount = document.querySelector("#tnlOutCount .badge");
  if (snarkInCount && snarkOutCount) {
    [inLabel, outLabel] = result ? [result.inCount, result.outCount] : ["0", "0"];
    const styleTag = document.head.querySelector("#tc") || createStyleTag();
    const styles = `#tnlInCount .badge::after{content:"${inLabel}"}#tnlOutCount .badge::after{content:"${outLabel}"}`;
    if (styleTag.textContent !== styles) { styleTag.textContent = styles; }
  }
}

/**
 * @function createStyleTag
 * @description Creates and appends a <style> element with id "tc" to the document head.
 * Used for injecting CSS rules that display tunnel counts.
 * @returns {HTMLStyleElement} The newly created style element.
 */
function createStyleTag() {
  const styleTag = document.createElement("style");
  styleTag.id = "tc";
  document.head.appendChild(styleTag);
  return styleTag;
}

document.addEventListener("DOMContentLoaded", async () => {
  const result = await getSnarkTunnelCount();
  const configured = typeof snarkRefreshDelay === "number" ? snarkRefreshDelay - 500 : 10000;
  const refresh = Number.isFinite(configured) ? Math.max(configured, 10000) : 10000;
  updateTunnelCounts(result);
  setInterval(async () => {
    const result = await getSnarkTunnelCount();
    updateTunnelCounts(result);
  }, refresh);
});