メインコンテンツまでスキップ

IPC Bridge

The Electron adapter replaces HTTP sockets with Electron's IPC mechanism. Standard Request/Response objects are serialized into IPC-safe payloads, sent between processes, and deserialized on the other side — your controllers see the same Web Fetch API as any other adapter.

How It Works

Renderer                    Preload                     Main
────────                    ───────                     ────
ipcFetch(req)
  → toIpcRequest(req)
    → globalThis[channel](payload)
                            ipcRenderer.invoke(channel, payload)
                                                        ipcMain.handle(channel, payload)
                                                          → toRequest(payload)
                                                          → app.fetch(request)
                                                          → toIpcResponse(response)
                            ← IpcFetchResponse
  ← toResponse(payload)
  ← Response

Text content (JSON, HTML, XML) is serialized as strings; binary content as ArrayBuffer.

Configuration

All three layers must agree on the same channel string:

// main
const electronZelt = await onElectron(app, { ipcChannel: 'http://zelt-app' });

// preload
exposeIpc({ channel: 'http://zelt-app' });

// renderer
ipcFetch(input, init, { channel: 'http://zelt-app' });

The channel must start with http:// or https://. The default is 'http://zelt-ipc' if omitted.

Main Process: onElectron()

const electronZelt = await onElectron(app, {
  ipcChannel: 'http://zelt-app',
  warmup: true, // default: resolve all controllers at startup
});

Options

OptionTypeDefaultDescription
ipcChannel`http://${string}` | `https://${string}`'http://zelt-ipc'IPC channel identifier
warmupbooleantrueResolve all controllers at startup
ipcFeaturestring'http'Key of the HTTP feature to bind to the IPC bridge, for apps with multiple http() features

Return Value (OnElectronApp)

Each configured feature is namespaced under its key (http by default). HTTP features additionally expose listen() to serve over TCP instead of IPC:

PropertyTypeDescription
http.fetch(request: Request) => Promise<Response>Handle a request directly
http.request(input: string | Request, init?: RequestInit) => Promise<Response>Handle a request built from a URL/path and RequestInit
http.listen(portOrOptions?: number | ListenOptions) => Promise<ServerHandle>Serve this feature over TCP instead of IPC
shutdown() => Promise<void>Graceful shutdown
get<T>(Class) => Promise<T>Resolve a service from DI

Breaking change: the old electronZelt.fetch shorthand is gone — use electronZelt.http.fetch instead (or the key you configured via ipcFeature).

Warmup

By default, onElectron() eagerly resolves all controllers at startup (warmup: true). This ensures services are initialized before the first request. Set warmup: false for lazy initialization on first request.

Preload Script: exposeIpc()

exposeIpc({ channel: 'http://zelt-app' });

exposeIpc() registers an IPC sender function and exposes it to the renderer via contextBridge.exposeInMainWorld() (or globalThis if context isolation is disabled).

The exposed key is the channel string itself, so ipcFetch on the renderer side can look it up from globalThis[channel].

Renderer: ipcFetch()

const response = await ipcFetch('http://zelt-app/hello/world', undefined, {
  channel: 'http://zelt-app',
});
const data = await response.json();

ipcFetch() has the same signature as fetch(), plus an optional third argument for the channel. It converts the request into an IPC payload, sends it through the preload bridge, and returns a standard Response.

Creating a Wrapper

In practice, wrap ipcFetch so the channel is configured once:

const CHANNEL = 'http://zelt-app';

export const apiFetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
  ipcFetch(input, init, { channel: CHANNEL });

Using with Hono Client

For type-safe API calls, combine with @zeltjs/hono-client:

const zeltIpcFetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
  ipcFetch(input, init, { channel: 'http://zelt-app' });

export const client = hc<AppType>('http://zelt-app', {
  fetch: zeltIpcFetch,
});

AppType is generated from your app definition — either by the CLI plugin (zelt build) or, when your build is driven by electron-vite instead of the Zelt CLI, programmatically via GeneratorService.generateFromApp(). For renderer code that cannot import from the main process build output, generate with portable: true.

Accessing the IPC Event

In controllers, use ipcEvent() to access the underlying IpcMainInvokeEvent:

@Controller('/system')
export class SystemController {
  @Get('/sender')
  getSender() {
    const event = ipcEvent();
    return { processId: event?.processId };
  }
}

Shutdown

Connect Zelt shutdown to Electron's quit lifecycle:

const electronZelt = await onElectron(app, { ipcChannel: 'http://zelt-app' });

const electronApp = await electronZelt.get(ElectronAdaptor);
electronApp.ready.app.on('will-quit', () => {
  void electronZelt.shutdown();
});

Using will-quit keeps the HTTP bridge alive until Electron is actually quitting, so the renderer can send final IPC calls during teardown.