IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

helper-client.ts

Buka raw
import "server-only";

import net from "node:net";
import { serverEnv } from "@/lib/server/env";
import { logServerAction } from "@/lib/server/logging/logger";
import type {
  ConnectNetworkRequest,
  NetworkEvent,
  NetworkServiceErrorCode,
  NetworkStatus,
  WifiAccessPoint,
} from "@/lib/shared/contracts/network";

type HelperRequest = {
  id: string;
  method: string;
  params?: unknown;
  requestId?: string;
};

type HelperErrorPayload = {
  code?: NetworkServiceErrorCode;
  message?: string;
};

type HelperResponse = {
  id?: string;
  ok?: boolean;
  result?: unknown;
  error?: HelperErrorPayload;
  type?: string;
  event?: NetworkEvent;
};

type HelperCallOptions = {
  requestId?: string;
  timeoutMs?: number;
};

type HelperEventSubscriptionOptions = {
  requestId?: string;
  onError?: (error: unknown) => void;
};

const DEFAULT_TIMEOUT_MS = 8_000;
const HELPER_UNAVAILABLE_BACKOFF_MS = 10_000;
let helperUnavailableUntil = 0;

function parseNetworkServiceErrorCode(value: unknown): NetworkServiceErrorCode {
  if (
    value === "helper_unavailable" ||
    value === "network_manager_unavailable" ||
    value === "auth_failed" ||
    value === "timeout" ||
    value === "invalid_request" ||
    value === "internal_error"
  ) {
    return value;
  }

  return "internal_error";
}

export class NetworkHelperError 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 = "NetworkHelperError";
    this.code = options?.code ?? "internal_error";
    this.statusCode = options?.statusCode ?? 500;
  }
}

function mapSocketError(error: unknown) {
  if (!(error instanceof Error)) {
    return new NetworkHelperError("DBus helper request failed");
  }

  const nodeError = error as NodeJS.ErrnoException;
  const code = nodeError.code ?? "";
  if (code === "ENOENT" || code === "ECONNREFUSED") {
    return new NetworkHelperError("DBus helper unavailable", {
      code: "helper_unavailable",
      statusCode: 503,
      cause: error,
    });
  }

  if (code === "ETIMEDOUT") {
    return new NetworkHelperError("DBus helper timeout", {
      code: "timeout",
      statusCode: 504,
      cause: error,
    });
  }

  return new NetworkHelperError(error.message, {
    cause: error,
  });
}

function parseJsonLine(line: string): HelperResponse | null {
  try {
    const parsed = JSON.parse(line) as unknown;
    if (!parsed || typeof parsed !== "object") return null;
    return parsed as HelperResponse;
  } catch {
    return null;
  }
}

async function callHelper<T>(
  method: string,
  params: unknown,
  options?: HelperCallOptions,
) {
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const requestId = options?.requestId;
  const callId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
  if (Date.now() < helperUnavailableUntil) {
    throw new NetworkHelperError("DBus helper unavailable", {
      code: "helper_unavailable",
      statusCode: 503,
    });
  }

  const startedAt = performance.now();

  try {
    const result = await new Promise<T>((resolve, reject) => {
      const socket = net.createConnection({
        path: serverEnv.DBUS_HELPER_SOCKET_PATH,
      });
      let settled = false;
      let buffer = "";

      const request: HelperRequest = {
        id: callId,
        method,
        params,
        requestId,
      };

      const timeout = setTimeout(() => {
        if (settled) return;
        settled = true;
        socket.destroy();
        reject(
          new NetworkHelperError("DBus helper timeout", {
            code: "timeout",
            statusCode: 504,
          }),
        );
      }, timeoutMs);

      const finalize = (fn: () => void) => {
        if (settled) return;
        settled = true;
        clearTimeout(timeout);
        socket.end();
        fn();
      };

      socket.on("connect", () => {
        socket.write(`${JSON.stringify(request)}\n`);
      });

      socket.on("data", (chunk) => {
        buffer += chunk.toString("utf8");
        let newline = buffer.indexOf("\n");

        while (newline >= 0) {
          const line = buffer.slice(0, newline).trim();
          buffer = buffer.slice(newline + 1);
          newline = buffer.indexOf("\n");

          if (!line) continue;
          const message = parseJsonLine(line);
          if (!message) continue;
          if (message.type === "event") continue;
          if (message.id !== callId) continue;

          if (!message.ok) {
            const helperCode = parseNetworkServiceErrorCode(message.error?.code);
            const statusCode =
              helperCode === "auth_failed"
                ? 401
                : helperCode === "helper_unavailable"
                  ? 503
                  : helperCode === "network_manager_unavailable"
                    ? 503
                    : helperCode === "timeout"
                      ? 504
                      : helperCode === "invalid_request"
                        ? 400
                        : 500;

            finalize(() =>
              reject(
                new NetworkHelperError(
                  message.error?.message ?? "DBus helper request failed",
                  {
                    code: helperCode,
                    statusCode,
                  },
                ),
              ),
            );
            return;
          }

          finalize(() => resolve(message.result as T));
          return;
        }
      });

      socket.on("error", (error) => {
        finalize(() => reject(mapSocketError(error)));
      });

      socket.on("close", () => {
        if (settled) return;
        settled = true;
        clearTimeout(timeout);
        reject(
          new NetworkHelperError("DBus helper closed before response", {
            code: "helper_unavailable",
            statusCode: 503,
          }),
        );
      });
    });

    helperUnavailableUntil = 0;
    return result;
  } catch (error) {
    const mapped =
      error instanceof NetworkHelperError ? error : mapSocketError(error);

    if (
      mapped.code === "helper_unavailable" ||
      mapped.code === "timeout" ||
      mapped.code === "network_manager_unavailable"
    ) {
      helperUnavailableUntil = Math.max(
        helperUnavailableUntil,
        Date.now() + HELPER_UNAVAILABLE_BACKOFF_MS,
      );
      logServerAction({
        level: "warn",
        layer: "system",
        action: "dbus.network.call",
        status: "error",
        requestId,
        durationMs: Number((performance.now() - startedAt).toFixed(2)),
        message: mapped.message,
        meta: {
          method,
          timeoutMs,
        },
        error: mapped,
      });
    } else {
      logServerAction({
        level: "error",
        layer: "system",
        action: "dbus.network.call",
        status: "error",
        requestId,
        durationMs: Number((performance.now() - startedAt).toFixed(2)),
        message: mapped.message,
        meta: {
          method,
          timeoutMs,
        },
        error: mapped,
      });
    }

    throw mapped;
  }
}

export function isNetworkHelperUnavailableError(error: unknown) {
  return error instanceof NetworkHelperError && error.code === "helper_unavailable";
}

export async function getNetworkStatusFromHelper(options?: HelperCallOptions) {
  return callHelper<NetworkStatus>("network.getStatus", {}, options);
}

export async function scanWifiNetworksFromHelper(options?: HelperCallOptions) {
  return callHelper<WifiAccessPoint[]>("network.scan", {}, options);
}

export async function connectWifiFromHelper(
  input: ConnectNetworkRequest,
  options?: HelperCallOptions,
) {
  return callHelper<NetworkStatus>("network.connect", input, options);
}

export async function disconnectWifiFromHelper(
  input: { iface?: string },
  options?: HelperCallOptions,
) {
  return callHelper<NetworkStatus>("network.disconnect", input, options);
}

export function subscribeToHelperEvents(
  onEvent: (event: NetworkEvent) => void,
  options?: HelperEventSubscriptionOptions,
) {
  const socket = net.createConnection({
    path: serverEnv.DBUS_HELPER_SOCKET_PATH,
  });
  let buffer = "";

  socket.on("data", (chunk) => {
    buffer += chunk.toString("utf8");
    let newline = buffer.indexOf("\n");

    while (newline >= 0) {
      const line = buffer.slice(0, newline).trim();
      buffer = buffer.slice(newline + 1);
      newline = buffer.indexOf("\n");

      if (!line) continue;
      const message = parseJsonLine(line);
      if (!message || message.type !== "event" || !message.event) continue;
      onEvent(message.event);
    }
  });

  socket.on("error", (error) => {
    if (options?.onError) {
      options.onError(mapSocketError(error));
    }
  });

  socket.on("close", () => {
    if (options?.onError) {
      options.onError(
        new NetworkHelperError("DBus helper event stream closed", {
          code: "helper_unavailable",
          statusCode: 503,
        }),
      );
    }
  });

  return () => {
    socket.destroy();
  };
}
byte
9.252
baris
367
Jenis
ts
Commit terakhir
16 Oktober 2026