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-effect-rpc

v0.8.0

Published

Typed IPC RPC for Electron, built on Effect and @effect/schema

Readme

electron-effect-rpc

Typed IPC RPC for Electron, built on Effect and @effect/schema.

The ergonomic default is now a single shared createIpcKit configuration that you reuse in main, preload, and renderer code. Low-level subpath APIs still exist and remain fully supported.

This package is ESM-only. It targets modern Electron runtimes and assumes an ESM-capable build pipeline.

Features

  • Single shared contract for methods, events, and streaming RPC.
  • Single shared kit config to eliminate cross-process prefix drift.
  • End-to-end schema validation at IPC boundaries.
  • Effect-first renderer RPC with typed domain and defect channels.
  • Streaming RPC: handlers return Stream.Stream, clients consume Stream.Stream.
  • Effect-native main handlers with explicit runtime injection.
  • Explicit lifecycle handles and bounded event queue backpressure.
  • Structured diagnostics hooks for decode/protocol/dispatch failures.

Requirements

  • Electron with context isolation enabled.
  • ESM-capable bundling.
  • Peer dependencies: effect, @effect/schema, electron.

Installation

bun add electron-effect-rpc effect @effect/schema

Quickstart (Kit-First)

1) Define contract and kit once

import * as S from "@effect/schema/Schema";
import { createIpcKit, defineContract, event, rpc, streamRpc } from "electron-effect-rpc";

export const GetAppVersion = rpc("GetAppVersion", S.Struct({}), S.Struct({ version: S.String }));

export const WorkUnitProgress = event(
  "WorkUnitProgress",
  S.Struct({
    requestId: S.String,
    chunk: S.String,
    done: S.Boolean,
  }),
);

export const StreamAiGeneration = streamRpc(
  "StreamAiGeneration",
  S.Struct({ prompt: S.String }),
  S.Struct({ delta: S.String }),
);

const contract = defineContract({
  methods: [GetAppVersion] as const,
  events: [WorkUnitProgress] as const,
  streamMethods: [StreamAiGeneration] as const,
});

export const ipc = createIpcKit({
  contract,
  channelPrefix: { rpc: "rpc/", event: "event/" },
  bridge: { global: "api" },
  decode: { rpc: "envelope", events: "safe" },
});

2) Main process

import { app, ipcMain } from "electron";
import { Effect, Stream } from "effect";
import * as Runtime from "effect/Runtime";
import { ipc, WorkUnitProgress } from "./shared-ipc.ts";

const mainRpc = ipc.main({
  ipcMain,
  handlers: {
    GetAppVersion: () => Effect.succeed({ version: app.getVersion() }),
  },
  streamHandlers: {
    StreamAiGeneration: ({ prompt }) =>
      Stream.fromIterable(prompt.split(" ")).pipe(Stream.map((word) => ({ delta: word + " " }))),
  },
  runtime: Runtime.defaultRuntime,
  getWindows: () => [mainWindow],
});

mainRpc.start();

void Effect.runPromise(
  mainRpc.publish(WorkUnitProgress, {
    requestId: "req-1",
    chunk: "starting",
    done: false,
  }),
);

3) Preload

import { ipc } from "./shared-ipc.ts";

const { expose } = ipc.preload();
expose();

This exposes one global by default: window.api.

If your preload runtime is ESM-only and does not expose synchronous require, pass the imported Electron module explicitly:

import * as electron from "electron";
import { ipc } from "./shared-ipc.ts";

const { expose } = ipc.preload({ electronModule: electron });
expose();

4) Renderer

import { Effect, Stream } from "effect";
import { ipc, WorkUnitProgress } from "./shared-ipc.ts";

const { client, events, streamClient, dispose } = ipc.renderer(window.api);
const { version } = await Effect.runPromise(client.GetAppVersion());

// Streaming RPC
await Effect.runPromise(
  streamClient
    .StreamAiGeneration({ prompt: "hello world" })
    .pipe(Stream.runForEach((chunk) => Effect.sync(() => console.log(chunk.delta)))),
);

const unsubscribe = events.subscribe(WorkUnitProgress, (payload) => {
  console.log(payload.chunk);
});

// later
unsubscribe();
dispose();

5) Window typing

declare global {
  interface Window {
    api: {
      invoke: (method: string, payload: unknown) => Promise<unknown>;
      subscribe: (name: string, handler: (payload: unknown) => void) => () => void;
      onStreamFrame?: (listener: (frame: unknown) => void) => () => void;
    };
  }
}

Error Model

Domain failures are modeled with tagged error schemas and are surfaced in the Effect error channel as those same tagged values. Unexpected failures, transport defects, and protocol mismatches are surfaced as RpcDefectError, which includes a stable code discriminator: request_encoding_failed, invoke_failed, success_payload_decoding_failed, failure_payload_decoding_failed, noerror_contract_violation, invalid_response_envelope, legacy_decode_failed, remote_defect, stream_invoke_failed, stream_handshake_invalid, stream_chunk_decode_failed, and stream_error_decode_failed.

Breaking Changes

getWindow was renamed to getWindows and now returns an array, enabling multi-window event fan-out. Empty array replaces null for "no windows."

Before:

getWindow: () => mainWindow,

After:

getWindows: () => [mainWindow],

Renderer RPC methods now return Effect.Effect instead of Promise, and IpcMainHandle.emit was removed in favor of publish.

Before:

const result = await client.GetAppVersion();
await mainRpc.emit(WorkUnitProgress, payload);

After:

const result = await Effect.runPromise(client.GetAppVersion());
await Effect.runPromise(mainRpc.publish(WorkUnitProgress, payload));

Low-Level APIs (Still Supported)

If you need direct control, keep using subpath entry points:

  • electron-effect-rpc/contract
  • electron-effect-rpc/main
  • electron-effect-rpc/renderer
  • electron-effect-rpc/preload
  • electron-effect-rpc/types
  • electron-effect-rpc/testing

Root API Surface

The root entry point exports:

  • createIpcKit
  • rpc, event, streamRpc, defineContract, NoError
  • Types: IpcKit, IpcKitOptions, IpcMainHandle, IpcBridge, IpcBridgeGlobal

Low-level factories like createRpcClient remain subpath-only by design.

Tutorials

For deeper walkthroughs and production guidance:

Conventions

  • Relative imports use .ts extensions.
  • Package imports are extensionless.
  • No index.ts barrel files in subpath modules.

License

MIT