Skip to content

electron-trpc 1.0: tRPC 11, disposable IPC handler, automatic preload - #231

Merged
jsonnull merged 20 commits into
mainfrom
trpc-11-rewrite
Oct 30, 2026
Merged

jsonnull merged 20 commits into
mainfrom
trpc-11-rewrite

Conversation

@jsonnull

Copy link
Copy Markdown
Owner

electron-trpc 1.0 supports tRPC 11 and Electron 35 or later, and rebuilds the IPC handler around disposable scopes. It replaces the next branch's 1.0 alphas: this branch includes next's history and exits pre-release mode, so merging it and then the Version Packages PR releases 1.0.0 as latest.

Breaking changes

  • Requires tRPC 11 (^11.10.0) and Electron 35 or later. Transformers are passed to ipcLink({ transformer }) instead of the client, as with tRPC 11's other links.
  • createIPCHandler registers electron-trpc's preload script itself, so apps no longer need one. Apps with their own preload script pass preload: false and import exposeElectronTRPC from electron-trpc/preload.
  • createIPCHandler returns a Disposable handler that serves every window, replacing the windows option and attachWindow/detachWindow.

Architecture

The main process tracks each renderer's requests in a tree of disposable scopes: the handler, each webContents, each renderer-side client and each request. Every request gets an AbortController, which tRPC 11 passes to the procedure, so disposing a scope cancels everything below it. Requests end when their page navigates away, reloads, crashes or closes, or when the handler is disposed.

Also new

  • allow(event) rejects requests by sender with a FORBIDDEN error, for apps that load content they don't trust.
  • preload: { sessions } registers the preload script on sessions other than the default.
  • When a build bundles electron-trpc without its preload script, createIPCHandler throws an error explaining the fix. webpack builds, including electron-forge's, copy the script automatically.

Fixed

tRPC 11 support also covers #184 and #227.

Tooling

A nix flake (Node 24, pnpm 12) replaces shell.nix, the library builds with tsdown, and the toolchain moves to TypeScript 7, Vitest 5, Electron 45 and Playwright 1.64. CI runs on Node 24. The examples move to tRPC 11, React 19 and Vite 8, and a new webpack example joins them.

Testing

  • 36 unit tests run the real tRPC client and server against each other through a fake Electron, which clones and delays messages like real IPC.
  • 5 e2e tests run the examples on Electron 45. basic-react checks that a reload leaves exactly one subscription in the main process, and basic-webpack runs from a copy of only its webpack output, as a packaged app would.
  • An app from electron-forge 8's webpack template was packaged with asar and run against this build.

- Replace shell.nix with a flake (Node 24, pnpm 12) and `use flake`.
- Drop corepack from scripts and move pnpm settings into pnpm-workspace.yaml.
- Build with tsdown instead of four vite configs and dts-bundle-generator,
  emitting per-format type declarations listed first in `exports`.
- Upgrade to TypeScript 7, Vitest 5, Electron 45 and Playwright 1.64, and
  remove unused dev dependencies.
- Skip IPC messages and navigations whose frame is already gone, now that
  Electron types `senderFrame` and navigation `frame` as nullable.
- Run CI on Node 24 with current action versions, and set up the Electron
  sandbox for every example.
Electron 45 no longer downloads its binary on install, so fetch it with
`pnpm install:electron` before running e2e tests or configuring its sandbox
in CI, instead of letting parallel test workers race to download it.

The dev shell gives nix-ld the libraries that binary loads, along with a
matching dynamic linker, so e2e tests run the same Electron locally as in CI.
The main process tracks everything a renderer has in flight in a tree of
disposable scopes: the handler, each webContents, each renderer-side client
and each operation. Disposing a scope aborts what is below it, so requests
end when their page navigates away, reloads, crashes or closes, or when the
handler itself is disposed.

- Support tRPC 11, including async generator subscriptions, tracked events
  and cancellation through the procedure's signal, and require Electron 35.
- Identify renderer-side clients, so pages with several clients get the
  right responses (#201).
- Register operations before creating context, so a request stopped early
  can't leave a subscription running.
- Clean up on committed cross-document navigations only, matching frames by
  frame tree node instead of by a routing id prefix.
- Only listen for responses in the renderer while requests are in flight.
- Register the preload script with `registerPreloadScript`, on configurable
  sessions, and unregister it when the handler is disposed.
- Typecheck main, renderer and tests separately, and test both halves
  against each other through a fake Electron.
- Update the examples to tRPC 11, React 19 and Vite 8.
The preload script exposes electron-trpc to every page in its sessions, so
apps that load content they don't trust need a way to refuse its requests.
`allow` is checked for every request, and rejected requests fail with a
`FORBIDDEN` error rather than never settling.
Exit pre-release mode, and rewrite the changesets for people upgrading from
0.7.1, who never had the alpha's `autoRegisterPreload` option.
… missing.

Packaged webpack apps, like electron-forge's, ship only the webpack output,
so the preload script was never found and renderers silently had no
`electronTRPC`. A `webpack` export condition gives webpack the ES module
build, whose `new URL(..., import.meta.url)` reference makes webpack copy the
script into its output.

`createIPCHandler` now uses the first location where the script exists, and
otherwise throws an error explaining how to expose electron-trpc from the
app's own preload script, which the README documents along with when it's
needed. The basic-webpack example runs its e2e test from a copy of only the
webpack output, as a packaged app would.
A page could send anything over the electron-trpc channel, and reading fields
off a message like `null` threw in the main process, before `allow` could
reject the sender. Check a message's shape before handling it.
Electron has no event for an iframe being removed, so its requests kept
running. Stop a client once it has something to send to a frame that's
gone, and sweep up clients of gone frames whenever their window sends a
message or creates a frame.
`frameToken` only exists from Electron 38, so on 35-37 every frame in a
process compared equal, and a frame could stop a request from another frame
that used the same client id. A frame tree node id identifies one frame on
every supported version, so compare that instead.
@jsonnull
jsonnull merged commit bb5a2d9 into main Oct 30, 2026
3 checks passed
@jsonnull
jsonnull deleted the trpc-11-rewrite branch October 30, 2026 02:14
@github-actions github-actions Bot mentioned this pull request Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant