Ctrl K
ComunidadGuías…

src/preload.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts

import { contextBridge, ipcRenderer, webFrame } from "electron";
import {
  VALID_INVOKE_CHANNELS,
  VALID_RECEIVE_CHANNELS,
  VALID_SEND_CHANNELS,
  type ValidInvokeChannel,
  type ValidReceiveChannel,
  type ValidSendChannel,
} from "./ipc/preload/channels";
import { isIpcInvokeEnvelope, unwrapIpcEnvelope } from "./ipc/contracts/core";
import { systemSendContracts } from "./ipc/types/system";

// Use the contract-derived channel arrays
const validInvokeChannels = VALID_INVOKE_CHANNELS;
const validReceiveChannels = VALID_RECEIVE_CHANNELS;

function isValidReceiveChannel(
  channel: string,
): channel is ValidReceiveChannel {
  return validReceiveChannels.includes(channel as ValidReceiveChannel);
}

function isValidDynamicReceiveChannel(channel: string): boolean {
  // Terminal stream suffixes must stay server-generated, unpredictable session
  // IDs. Do not extend this pattern to renderer-controlled channel names.
  return (
    channel.startsWith("terminal:data:") || channel.startsWith("terminal:exit:")
  );
}

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld("electron", {
  ipcRenderer: {
    invoke: (channel: ValidInvokeChannel | string, ...args: unknown[]) => {
      if ((validInvokeChannels as readonly string[]).includes(channel)) {
        return ipcRenderer.invoke(channel, ...args).then((response) => {
          if (isIpcInvokeEnvelope(response)) {
            return unwrapIpcEnvelope(response);
          }
          return response;
        });
      }
      throw new Error(`Invalid channel: ${channel}`);
    },
    invokeEnvelope: (
      channel: ValidInvokeChannel | string,
      ...args: unknown[]
    ) => {
      if ((validInvokeChannels as readonly string[]).includes(channel)) {
        return ipcRenderer.invoke(channel, ...args);
      }
      throw new Error(`Invalid channel: ${channel}`);
    },
    // One-way, fire-and-forget renderer -> main. No response is returned, so
    // this is safe to call while the frame is being torn down (e.g. from a
    // `pagehide` handler on app quit), where a reply-expecting `invoke` would
    // make the main process throw "Object has been destroyed".
    send: (channel: ValidSendChannel | string, ...args: unknown[]) => {
      if ((VALID_SEND_CHANNELS as readonly string[]).includes(channel)) {
        ipcRenderer.send(channel, ...args);
        return;
      }
      throw new Error(`Invalid channel: ${channel}`);
    },
    on: (
      channel: ValidReceiveChannel | string,
      listener: (...args: unknown[]) => void,
    ) => {
      if (
        isValidReceiveChannel(channel) ||
        isValidDynamicReceiveChannel(channel)
      ) {
        const subscription = (
          _event: Electron.IpcRendererEvent,
          ...args: unknown[]
        ) => listener(...args);
        ipcRenderer.on(channel, subscription);
        return () => {
          ipcRenderer.removeListener(channel, subscription);
        };
      }
      throw new Error(`Invalid channel: ${channel}`);
    },
    removeAllListeners: (channel: ValidReceiveChannel | string) => {
      if (
        isValidReceiveChannel(channel) ||
        isValidDynamicReceiveChannel(channel)
      ) {
        ipcRenderer.removeAllListeners(channel);
      }
    },
    removeListener: (
      channel: ValidReceiveChannel | string,
      listener: (...args: unknown[]) => void,
    ) => {
      if (
        isValidReceiveChannel(channel) ||
        isValidDynamicReceiveChannel(channel)
      ) {
        ipcRenderer.removeListener(channel, listener);
      }
    },
  },
  webFrame: {
    setZoomFactor: (factor: number) => {
      webFrame.setZoomFactor(factor);
    },
    getZoomFactor: () => webFrame.getZoomFactor(),
  },
});

// Push this renderer's V8 heap to the main-process performance heartbeat.
// The main process cannot read another process's heap, and this is the
// figure that says how close the UI is to Electron's ~4 GB renderer cap.
// Lives in the preload rather than React so a wedged UI still reports as
// long as its event loop turns. Electron's sandboxed `process` reports KB.
const RENDERER_HEAP_REPORT_INTERVAL_MS = 30_000;
const KB_PER_MB = 1024;

function reportRendererHeap(): void {
  try {
    const stats = process.getHeapStatistics();
    ipcRenderer.send(systemSendContracts.reportRendererHeap.channel, {
      heapUsedMB: Math.round(stats.usedHeapSize / KB_PER_MB),
      heapLimitMB: Math.round(stats.heapSizeLimit / KB_PER_MB),
    });
  } catch {
    // Best effort: the heartbeat simply lacks the renderer heap.
  }
}

// The first report waits for the document: sent from the top of the
// preload, it arrives before the frame passes the main process's trusted
// renderer check and is dropped.
window.addEventListener("DOMContentLoaded", reportRendererHeap, {
  once: true,
});
setInterval(reportRendererHeap, RENDERER_HEAP_REPORT_INTERVAL_MS);