# How IPC Communication Works Between the Electron Main and Renderer Processes in 5ire

> Explore how 5ire's typed IPC layer enables seamless communication between Electron main and renderer processes using service-oriented proxies and supports async RPC and streaming data.

- Repository: [Ironben/5ire](https://github.com/nanbingxyz/5ire)
- Tags: internals
- Published: 2026-03-07

---

**The 5ire application implements a typed, bidirectional IPC layer that abstracts Electron's raw `ipcMain` and `ipcRenderer` into service-oriented proxies with support for both async RPC and streaming data.**

The 5ire repository builds a sophisticated abstraction over standard Electron IPC patterns. Instead of manually managing channel strings and event listeners, the codebase uses a **Bridge pattern** that provides type-safe communication between the main and renderer processes. This architecture handles everything from simple method invocations to complex streaming operations with backpressure management.

## The Architecture of IPC Communication in 5ire

The IPC system relies on three core components working together: the preload bridge that runs in an isolated context, the main process bridge that registers handlers, and the connector that builds proxy objects.

### Preload Bridge Setup

In [`src/main/preload.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/preload.ts), the application establishes the communication channel during the preload phase. This file creates a `BridgeConnector` instance that receives the `ipcRenderer` object and constructs proxy services for each exposed capability:

```typescript
// src/main/preload.ts
const connector = new BridgeConnector(ipcRenderer);
const bridge = {
  encryptor: connector.createProxy('encryptor'),
  updater: connector.createProxy('updater'),
  documentManager: connector.createProxy('documentManager'),
};

```

These proxies expose `async` and `stream` methods that internally translate method calls into `ipcRenderer.invoke` for request/response patterns or `ipcRenderer.send` for fire-and-forget operations.

### Main Process Bridge Implementation

On the main side, [`src/main/internal/bridge.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/internal/bridge.ts) defines the generic `Bridge<T>` class that services extend. Each service implements a subclass returning an object describing its actions—distinguishing between async operations and streaming channels:

```typescript
// src/main/internal/bridge.ts
class Bridge<T> {
  expose(ipcMain: IpcMain) {
    // Registers handlers under bridge::<namespace>::<action>
    // Handles ReadableStream by creating unique reader IDs
    // Manages stream lifecycle via bridge:stream:next and bridge:stream:stop
  }
}

```

When a handler returns a `ReadableStream`, the bridge generates a unique **stream reader ID**, stores the `ReadableStreamDefaultReader`, and enables the renderer to request subsequent chunks or terminate the stream through dedicated IPC channels.

## How the Preload Context Establishes IPC Communication

The `BridgeConnector` class in [`src/main/internal/bridge-connector.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/internal/bridge-connector.ts) acts as the factory for renderer-side proxies. When you call `connector.createProxy('updater')`, it returns an object where every method is wrapped to handle IPC serialization:

```typescript
// Conceptual representation from bridge-connector.ts
createProxy(namespace: string) {
  return new Proxy({}, {
    get: (target, action: string) => {
      return (...args: any[]) => {
        return ipcRenderer.invoke(`bridge::${namespace}::${action}`, ...args);
      };
    }
  });
}

```

For streaming methods, the proxy detects the stream configuration and returns an object with `next()` and `stop()` methods that communicate with the main process via `bridge:stream:next::<namespace>` and `bridge:stream:stop::<namespace>` channels.

## Main Process Handler Registration for IPC Communication

During application bootstrap in [`src/main/main.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/main.ts), each service bridge is instantiated and exposed to `ipcMain` through the dependency injection container:

```typescript
// src/main/main.ts
Container.inject(EncryptorBridge).expose(ipcMain);
Container.inject(UpdaterBridge).expose(ipcMain);
Container.inject(DocumentManagerBridge).expose(ipcMain);

```

This registration pattern ensures that all IPC handlers are set up before the renderer loads, preventing race conditions where the renderer might invoke channels before handlers exist.

### Streaming Implementation Details

When the main process returns a stream, the bridge creates a managed reader:

```typescript
// From bridge.ts stream handling logic
const readerId = generateReaderId();
const reader = stream.getReader();
streamReaders.set(readerId, reader);

// Return readerId to renderer
return { __streamId: readerId };

```

The renderer then uses this ID to request chunks:

```typescript
// Renderer side stream consumption
const { done, value } = await ipcRenderer.invoke(
  `bridge:stream:next::${namespace}`, 
  readerId
);

```

## Practical Examples of IPC Communication

### Async Method Invocation

To check for updates from the renderer:

```typescript
// Renderer process
async function checkUpdates() {
  const result = await window.bridge.updater.checkForUpdates();
  console.log('Update info:', result);
}

```

### Streaming Data from Main to Renderer

For monitoring download progress or other continuous data flows:

```typescript
// Renderer process
async function monitorDownload() {
  const stream = await window.bridge.downloader.download();
  
  while (true) {
    const { done, value } = await stream.next();
    if (done) break;
    console.log('Progress:', value);
  }
  
  // Or stop early if needed
  // await stream.stop();
}

```

### Low-Level Electron Helpers

For direct IPC operations outside the bridge system:

```typescript
// Using the exposed electron helper
window.electron.openExternal('https://example.com');

// Subscribing to custom events
const unsubscribe = window.electron.ipcRenderer.on('mcp-server-loaded', (servers) => {
  console.log('MCP servers loaded:', servers);
});

// Cleanup
unsubscribe();

```

## Summary

- **5ire implements a typed Bridge pattern** over Electron's raw IPC to provide type-safe communication between main and renderer processes.
- **The preload script** ([`src/main/preload.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/preload.ts)) creates proxy objects via `BridgeConnector` that translate method calls into `ipcRenderer.invoke` and `ipcRenderer.send`.
- **The main process** ([`src/main/internal/bridge.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/internal/bridge.ts)) exposes service methods through the `Bridge.expose(ipcMain)` pattern, handling both async RPC and `ReadableStream` streaming with unique reader IDs.
- **Streaming support** uses dedicated channels (`bridge:stream:next` and `bridge:stream:stop`) to manage backpressure and allow the renderer to consume chunks on demand.
- **Service registration** occurs during app bootstrap in [`src/main/main.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/main.ts) through a dependency injection container, ensuring handlers exist before the renderer loads.

## Frequently Asked Questions

### How does 5ire maintain type safety across the IPC boundary?

5ire uses TypeScript interfaces to define the shape of each service (e.g., `UpdaterBridge`, `EncryptorBridge`). The `BridgeConnector` in [`src/main/internal/bridge-connector.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/internal/bridge-connector.ts) creates proxies that enforce these types at compile time, while the `Bridge` class in [`src/main/internal/bridge.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/internal/bridge.ts) ensures the main-side implementation matches the expected interface. This eliminates string-based channel names from application code.

### What is the difference between async and stream methods in the 5ire IPC system?

**Async methods** return a single Promise that resolves with the result of `ipcRenderer.invoke`, suitable for one-off operations like `checkForUpdates()`. **Stream methods** return a `ReadableStream` wrapper object with `next()` and `stop()` methods, designed for continuous data flows like download progress. The bridge automatically detects stream returns and manages reader IDs via the `bridge:stream:next` and `bridge:stream:stop` channels.

### How does 5ire handle cleanup when a renderer stream is no longer needed?

When the renderer calls `stream.stop()` or the stream naturally ends, the main process receives a message on the `bridge:stream:stop::<namespace>` channel. The `Bridge` class looks up the stored `ReadableStreamDefaultReader` by its unique ID in [`src/main/internal/bridge.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/internal/bridge.ts) and calls `reader.cancel()` to release resources. This prevents memory leaks from abandoned stream readers in the main process.

### Can I use the low-level `window.electron` API instead of the Bridge system?

Yes, 5ire exposes a low-level `electronHandler` object as `window.electron` in [`src/main/preload.ts`](https://github.com/nanbingxyz/5ire/blob/main/src/main/preload.ts) for operations outside the typed Bridge architecture. This provides direct access to `ipcRenderer.invoke` and `ipcRenderer.send` through methods like `request`, `store`, and `openExternal`, plus an event subscription API (`on`, `once`, `unsubscribe`). Use this for one-off IPC calls that don't warrant a full Bridge service implementation.