npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

electron-ipc-socket

v4.0.0

Published

Typed bidirectional MessagePort RPC sessions for Electron.

Downloads

151

Readme

electron-ipc-socket

Typed, bidirectional RPC and events over a dedicated Electron MessagePort.

npm version Node CI

Version 4 is a dual ESM/CommonJS redesign for Node.js 22.12 or newer and Electron 41, 42, or 43. It replaces the v3 Socket and Transport API with secure sessions built on MessageChannelMain.

npm install electron-ipc-socket

Define a contract

A contract describes what each side receives. main.requests and main.events are handled by the main process; renderer.requests and renderer.events are handled by the renderer.

// contract.ts
import type { Procedure } from 'electron-ipc-socket';

export type AppContract = {
    main: {
        requests: {
            readPreferences: Procedure<void, { theme: 'light' | 'dark' }>;
            savePreferences: Procedure<{ theme: 'light' | 'dark' }, void>;
        };
        events: {
            rendererReady: void;
        };
    };
    renderer: {
        requests: {
            confirm: Procedure<{ message: string }, boolean>;
        };
        events: {
            preferencesChanged: { theme: 'light' | 'dark' };
        };
    };
};

The type contract rejects non-Procedure request entries and non-string route keys, but types alone do not inspect runtime IPC data. Payloads and results must also be supported by the structured clone algorithm; functions, promises, symbols, and Electron objects cannot be sent.

Optional runtime validation

For untrusted payloads, provide a complete Standard Schema V1 validation tree. The package accepts schemas from Standard Schema-compatible libraries without depending on one. Every request needs input and output schemas, and every event needs a payload schema on both sides:

// validation.ts
import { defineContractValidation } from 'electron-ipc-socket';
import type { AppContract } from './contract.js';
import { schemas } from './schemas.js';

export const validation = defineContractValidation<AppContract>({
    main: {
        requests: {
            readPreferences: {
                input: schemas.void,
                output: schemas.preferences,
            },
            savePreferences: {
                input: schemas.preferences,
                output: schemas.void,
            },
        },
        events: { rendererReady: schemas.void },
    },
    renderer: {
        requests: {
            confirm: {
                input: schemas.confirmation,
                output: schemas.boolean,
            },
        },
        events: { preferencesChanged: schemas.preferences },
    },
});

schemas may contain synchronous or asynchronous Standard Schema V1 validators. Validation occurs only when data is received, and the parsed schema value is passed to the handler, listener, or caller. Pass the same definition to both sessions for validation in both directions; it is never sent through preload or IPC.

Main process

Create one session for each (WebContents, channel) pair. Authorization is mandatory and synchronous. The library also verifies that the handshake came from that exact live WebContents, its current main frame, and the URL and origin observed during authorization.

// main.ts
import { BrowserWindow } from 'electron';
import { createMainSession } from 'electron-ipc-socket/main';
import type { AppContract } from './contract.js';
import { validation } from './validation.js';

const window = new BrowserWindow({
    webPreferences: {
        contextIsolation: true,
        nodeIntegration: false,
        preload: '/absolute/path/to/preload.cjs',
        sandbox: true,
    },
});

const session = createMainSession<AppContract>({
    channel: 'application',
    webContents: window.webContents,
    authorize: ({ origin, url }) =>
        origin === 'app://local' && url === 'app://local/index.html',
    validation,
});

session.handle('readPreferences', () => ({ theme: 'dark' }));
session.handle('savePreferences', (payload, context) => {
    // payload is the parsed savePreferences input.
    context.signal.throwIfAborted();
});

session.on('rendererReady', () => {
    session.send('preferencesChanged', { theme: 'dark' });
});

await session.ready;
const accepted = await session.invoke('confirm', {
    message: 'Continue?',
});

Handlers registered on a main session survive renderer reloads. A disconnect rejects pending calls and aborts active handler contexts; a subsequent authorized handshake reconnects the same session. Events sent while disconnected throw immediately and are not queued or replayed.

Preload bridge

The preload owns the handshake. It exposes only a small bridge for the dedicated port and never exposes ipcRenderer.

// preload.ts
import { exposeBridge } from 'electron-ipc-socket/preload';

exposeBridge({
    channel: 'application',
    key: 'applicationIpc',
});

The package publishes a CommonJS preload export, but Electron's sandbox exposes only a restricted require that cannot load arbitrary package modules, and sandboxed preloads cannot use ESM. A sandboxed application must therefore bundle its preload even when the source uses CommonJS:

// preload.cts
import preload = require('electron-ipc-socket/preload');

preload.exposeBridge({
    channel: 'application',
    key: 'applicationIpc',
});

Bundle it to one CommonJS file and leave Electron external:

esbuild src/preload.cts \
    --bundle \
    --platform=node \
    --format=cjs \
    --external:electron \
    --outfile=dist/preload.cjs

Point webPreferences.preload at the resulting absolute preload.cjs path. Keep sandbox: true, contextIsolation: true, and nodeIntegration: false.

Renderer

The renderer wrapper owns RPC handlers, AbortSignal objects, and reconstructed errors in the renderer world rather than in preload.

// renderer.ts
import type { PreloadBridge } from 'electron-ipc-socket';
import { RemoteError } from 'electron-ipc-socket';
import { createRendererSession } from 'electron-ipc-socket/renderer';
import type { AppContract } from './contract.js';
import { validation } from './validation.js';

declare global {
    interface Window {
        applicationIpc: PreloadBridge;
    }
}

const session = createRendererSession<AppContract>(window.applicationIpc, {
    validation,
});

session.handle('confirm', ({ message }) => window.confirm(message));
await session.ready;
session.send('rendererReady');

try {
    const preferences = await session.invoke('readPreferences');
    console.log(preferences.theme);
} catch (error) {
    if (error instanceof RemoteError) {
        console.error(error.remoteName, error.code, error.message);
    }
}

Session API

Both main and renderer sessions provide the same directional API:

| Member | Purpose | | ----------------------------------- | ------------------------------------------------------------------------------------- | | ready | Promise for the current or next connection. | | isConnected | Whether a dedicated port is currently attached. | | invoke(route, payload?, options?) | Call a request handled by the other side. | | handle(route, handler) | Register one local request handler; returns an unsubscribe function. | | send(event, payload?) | Send a one-way event to the other side. | | on(event, listener) | Subscribe to a local event; returns an unsubscribe function. | | once(event, listener) | Subscribe for one event; returns an unsubscribe function. | | onStateChange(listener) | Observe connection state; immediately receives the current state. | | onError(listener) | Observe malformed messages and transport errors. | | dispose() | Close the session and deterministically clear handlers, calls, timers, and listeners. |

The default invocation timeout is 60 seconds. Override it for one call, disable it with false, and/or attach a cancellation signal:

const controller = new AbortController();

const result = session.invoke('readPreferences', undefined, {
    signal: controller.signal,
    timeout: 5_000,
});

controller.abort();
await result;

Cancellation aborts the remote handler's context.signal. Late replies are ignored, and every timeout and signal listener is removed when a call settles.

Handlers may throw RpcError to expose a stable error code:

import { RpcError } from 'electron-ipc-socket';

throw new RpcError('ERR_NOT_ALLOWED', 'This operation is not allowed');

Callers receive RemoteError containing remoteName, message, and code. Remote stack traces are never transmitted. Other thrown values are reduced to a safe name and message.

Failed request validation is reported locally through onError and returned to the caller as a sanitized RemoteError with code ERR_IPC_VALIDATION. Failed result validation rejects the local invocation with ValidationError. Failed event validation reports ValidationError through onError, drops the event, and does not consume a once listener. Standard Schema issue details remain in the receiving process and are never transmitted.

Module formats

Every entry point supports ESM import and CommonJS require through conditional exports:

// ESM
import { RpcError } from 'electron-ipc-socket';
import { createMainSession } from 'electron-ipc-socket/main';
// CommonJS
const { RpcError } = require('electron-ipc-socket');
const { createMainSession } = require('electron-ipc-socket/main');

The root package remains "type": "module"; require resolves to the separate CommonJS build. Both builds share public error branding and main-session channel ownership when they are loaded in the same process.

Migrate from v3

Version 4 intentionally has no v3 compatibility entry point.

| v3 | v4 | | ------------------------------------- | ------------------------------------------------------------------------------------------------- | | new Socket(new Transport(...)) | createMainSession, bundled exposeBridge, and createRendererSession | | socket.open(channel) | Pass the same channel to createMainSession and exposeBridge; await session.ready | | socket.request(name, data) | session.invoke(name, data) | | socket.onRequest(name, handler) | session.handle(name, handler); payload and cancellation context are separate arguments | | socket.send(name, data) | session.send(name, data) | | socket.onEvent(name, handler) | session.on(name, handler) or session.once(name, handler) | | Settings.timeout | defaultTimeout on session creation or timeout per invocation | | Settings.cleanup | Removed; v4 cleans each timer and listener deterministically and has no polling interval | | socket.close() / socket.dispose() | session.dispose(); main sessions reconnect automatically across renderer reloads until disposed |

Consumers that cannot migrate must remain on v3.

Compatibility

  • Node.js >=22.12.0
  • Electron >=41 <44
  • ESM and CommonJS application code
  • Bundled CommonJS sandbox preload

The release test matrix covers Electron 41.10.5, 42.9.0, and 43.4.0. Delivery is ordered and at most once while connected; there is no persistence or event replay.