IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

service.ts

Buka raw
import "server-only";

import { z } from "zod";
import {
  connectWifiFromHelper,
  disconnectWifiFromHelper,
  getNetworkStatusFromHelper,
  isNetworkHelperUnavailableError,
  type NetworkHelperError,
  scanWifiNetworksFromHelper,
} from "@/lib/server/modules/network/helper-client";
import { logServerAction, withServerTiming } from "@/lib/server/logging/logger";
import { getSystemMetricsSnapshot } from "@/lib/server/modules/system/service";
import type {
  ConnectNetworkRequest,
  DisconnectNetworkRequest,
  NetworkServiceErrorCode,
  NetworkStatus,
  WifiAccessPoint,
} from "@/lib/shared/contracts/network";

type RequestContext = {
  requestId?: string;
};

type NetworkSource = "helper" | "fallback";

type NetworkStatusResult = {
  data: NetworkStatus;
  source: NetworkSource;
};

type WifiNetworksResult = {
  data: WifiAccessPoint[];
  source: NetworkSource;
};

const NETWORK_FALLBACK_CACHE_TTL_MS = 30_000;
const FALLBACK_LOG_COOLDOWN_MS = 60_000;

let fallbackStatusCache:
  | {
      value: NetworkStatus;
      expiresAt: number;
    }
  | null = null;

let fallbackWifiNetworksCache:
  | {
      value: WifiAccessPoint[];
      expiresAt: number;
    }
  | null = null;
const fallbackLogAtByAction = new Map<string, number>();

const connectNetworkSchema = z.object({
  ssid: z.string().trim().min(1).max(128),
  password: z.string().min(1).max(128).optional(),
});

const disconnectNetworkSchema = z.object({
  iface: z.string().trim().min(1).max(64).optional(),
});

export class NetworkServiceError extends Error {
  readonly code: NetworkServiceErrorCode;
  readonly statusCode: number;

  constructor(
    message: string,
    options?: {
      code?: NetworkServiceErrorCode;
      statusCode?: number;
      cause?: unknown;
    },
  ) {
    super(message, {
      cause: options?.cause,
    });

    this.name = "NetworkServiceError";
    this.code = options?.code ?? "internal_error";
    this.statusCode = options?.statusCode ?? 500;
  }
}

function mapFallbackStatusFromMetrics(snapshot: Awaited<ReturnType<typeof getSystemMetricsSnapshot>>) {
  return {
    connected: snapshot.wifi.connected,
    iface: snapshot.wifi.iface,
    ssid: snapshot.wifi.ssid,
    ipv4: snapshot.wifi.ipv4,
    signalPercent: snapshot.wifi.signalPercent,
  } satisfies NetworkStatus;
}

function mapFallbackNetworksFromMetrics(snapshot: Awaited<ReturnType<typeof getSystemMetricsSnapshot>>) {
  return snapshot.wifi.availableNetworks
    .map((network) => ({
      ssid: network.ssid,
      bssid: null,
      signalPercent: network.qualityPercent,
      channel: network.channel,
      frequencyMhz: null,
      security: network.security,
    }))
    .sort((left, right) => (right.signalPercent ?? 0) - (left.signalPercent ?? 0));
}

function readCachedFallbackStatus() {
  if (!fallbackStatusCache) return null;
  if (fallbackStatusCache.expiresAt <= Date.now()) {
    fallbackStatusCache = null;
    return null;
  }

  return fallbackStatusCache.value;
}

function readCachedFallbackWifiNetworks() {
  if (!fallbackWifiNetworksCache) return null;
  if (fallbackWifiNetworksCache.expiresAt <= Date.now()) {
    fallbackWifiNetworksCache = null;
    return null;
  }

  return fallbackWifiNetworksCache.value;
}

function writeFallbackCaches(snapshot: Awaited<ReturnType<typeof getSystemMetricsSnapshot>>) {
  const expiresAt = Date.now() + NETWORK_FALLBACK_CACHE_TTL_MS;
  fallbackStatusCache = {
    value: mapFallbackStatusFromMetrics(snapshot),
    expiresAt,
  };
  fallbackWifiNetworksCache = {
    value: mapFallbackNetworksFromMetrics(snapshot),
    expiresAt,
  };
}

function logFallbackNotice(options: {
  action: string;
  requestId?: string;
  message: string;
  error?: unknown;
}) {
  const now = Date.now();
  const lastLoggedAt = fallbackLogAtByAction.get(options.action) ?? 0;
  if (now - lastLoggedAt < FALLBACK_LOG_COOLDOWN_MS) {
    return;
  }
  fallbackLogAtByAction.set(options.action, now);

  logServerAction({
    level: "warn",
    layer: "service",
    action: options.action,
    status: "info",
    requestId: options.requestId,
    message: options.message,
    error: options.error,
  });
}

function mapKnownServiceError(error: unknown) {
  if (error instanceof NetworkServiceError) return error;

  if (error instanceof Error && error.name === "NetworkHelperError") {
    const helperError = error as NetworkHelperError;
    return new NetworkServiceError(helperError.message, {
      code: helperError.code,
      statusCode: helperError.statusCode,
      cause: error,
    });
  }

  return new NetworkServiceError("Network request failed", {
    code: "internal_error",
    statusCode: 500,
    cause: error,
  });
}

function validateConnectInput(input: unknown) {
  const parsed = connectNetworkSchema.safeParse(input);
  if (!parsed.success) {
    throw new NetworkServiceError("Invalid network connect payload", {
      code: "invalid_request",
      statusCode: 400,
      cause: parsed.error,
    });
  }

  return parsed.data satisfies ConnectNetworkRequest;
}

function validateDisconnectInput(input: unknown) {
  const parsed = disconnectNetworkSchema.safeParse(input ?? {});
  if (!parsed.success) {
    throw new NetworkServiceError("Invalid network disconnect payload", {
      code: "invalid_request",
      statusCode: 400,
      cause: parsed.error,
    });
  }

  return parsed.data satisfies DisconnectNetworkRequest;
}

export async function getNetworkStatus(context?: RequestContext): Promise<NetworkStatusResult> {
  return withServerTiming(
    {
      level: "debug",
      layer: "service",
      action: "network.status.get",
      requestId: context?.requestId,
    },
    async () => {
      try {
        const data = await getNetworkStatusFromHelper({
          requestId: context?.requestId,
        });

        return {
          data,
          source: "helper",
        } satisfies NetworkStatusResult;
      } catch (error) {
        if (!isNetworkHelperUnavailableError(error)) {
          throw mapKnownServiceError(error);
        }

        const cached = readCachedFallbackStatus();
        if (cached) {
          logFallbackNotice({
            action: "network.status.get.fallback",
            requestId: context?.requestId,
            message: "DBus helper unavailable; using in-memory fallback cache",
          });

          return {
            data: cached,
            source: "fallback",
          } satisfies NetworkStatusResult;
        }

        logFallbackNotice({
          action: "network.status.get.fallback",
          requestId: context?.requestId,
          message:
            "DBus helper unavailable; refreshing fallback cache from system metrics",
          error,
        });

        const snapshot = await getSystemMetricsSnapshot();
        writeFallbackCaches(snapshot);

        return {
          data: readCachedFallbackStatus() ?? mapFallbackStatusFromMetrics(snapshot),
          source: "fallback",
        } satisfies NetworkStatusResult;
      }
    },
  );
}

export async function getWifiNetworks(
  context?: RequestContext,
): Promise<WifiNetworksResult> {
  return withServerTiming(
    {
      level: "debug",
      layer: "service",
      action: "network.networks.get",
      requestId: context?.requestId,
    },
    async () => {
      try {
        const data = await scanWifiNetworksFromHelper({
          requestId: context?.requestId,
        });

        return {
          data,
          source: "helper",
        } satisfies WifiNetworksResult;
      } catch (error) {
        if (!isNetworkHelperUnavailableError(error)) {
          throw mapKnownServiceError(error);
        }

        const cached = readCachedFallbackWifiNetworks();
        if (cached) {
          logFallbackNotice({
            action: "network.networks.get.fallback",
            requestId: context?.requestId,
            message: "DBus helper unavailable; using in-memory fallback cache",
          });

          return {
            data: cached,
            source: "fallback",
          } satisfies WifiNetworksResult;
        }

        logFallbackNotice({
          action: "network.networks.get.fallback",
          requestId: context?.requestId,
          message:
            "DBus helper unavailable; refreshing fallback cache from system metrics",
          error,
        });

        const snapshot = await getSystemMetricsSnapshot();
        writeFallbackCaches(snapshot);

        return {
          data:
            readCachedFallbackWifiNetworks() ??
            mapFallbackNetworksFromMetrics(snapshot),
          source: "fallback",
        } satisfies WifiNetworksResult;
      }
    },
  );
}

export async function connectNetwork(
  input: unknown,
  context?: RequestContext,
): Promise<NetworkStatus> {
  const validated = validateConnectInput(input);

  return withServerTiming(
    {
      layer: "service",
      action: "network.connect.post",
      requestId: context?.requestId,
      meta: {
        ssid: validated.ssid,
      },
    },
    async () => {
      try {
        return await connectWifiFromHelper(validated, {
          requestId: context?.requestId,
        });
      } catch (error) {
        throw mapKnownServiceError(error);
      }
    },
  );
}

export async function disconnectNetwork(
  input: unknown,
  context?: RequestContext,
): Promise<NetworkStatus> {
  const validated = validateDisconnectInput(input);

  return withServerTiming(
    {
      layer: "service",
      action: "network.disconnect.post",
      requestId: context?.requestId,
      meta: {
        iface: validated.iface ?? null,
      },
    },
    async () => {
      try {
        return await disconnectWifiFromHelper(validated, {
          requestId: context?.requestId,
        });
      } catch (error) {
        throw mapKnownServiceError(error);
      }
    },
  );
}
byte
9.832
baris
382
Jenis
ts
Commit terakhir
16 Oktober 2026