GithubHelp home page GithubHelp logo

jsonnull / electron-trpc Goto Github PK

View Code? Open in Web Editor NEW
216.0 2.0 21.0 598 KB

Build type-safe Electron inter-process communication using tRPC

License: MIT License

TypeScript 98.24% Nix 1.06% JavaScript 0.70%
electron ipc trpc typescript hacktoberfest

electron-trpc's Introduction

electron-trpc

NPM MIT

Build IPC for Electron with tRPC

  • Expose APIs from Electron's main process to one or more render processes.
  • Build fully type-safe IPC.
  • Secure alternative to opening servers on localhost.
  • Full support for queries, mutations, and subscriptions.

Installation

# Using pnpm
pnpm add electron-trpc

# Using yarn
yarn add electron-trpc

# Using npm
npm install --save electron-trpc

Basic Setup

  1. Add your tRPC router to the Electron main process using createIPCHandler:

    import { app } from 'electron';
    import { createIPCHandler } from 'electron-trpc/main';
    import { router } from './api';
    
    app.on('ready', () => {
      const win = new BrowserWindow({
        webPreferences: {
          // Replace this path with the path to your preload file (see next step)
          preload: 'path/to/preload.js',
        },
      });
    
      createIPCHandler({ router, windows: [win] });
    });
  2. Expose the IPC to the render process from the preload file:

    import { exposeElectronTRPC } from 'electron-trpc/main';
    
    process.once('loaded', async () => {
      exposeElectronTRPC();
    });

    Note: electron-trpc depends on contextIsolation being enabled, which is the default.

  3. When creating the client in the render process, use the ipcLink (instead of the HTTP or batch HTTP links):

    import { createTRPCProxyClient } from '@trpc/client';
    import { ipcLink } from 'electron-trpc/renderer';
    
    export const client = createTRPCProxyClient({
      links: [ipcLink()],
    });
  4. Now you can use the client in your render process as you normally would (e.g. using @trpc/react).

electron-trpc's People

Contributors

allanhvam avatar beeequeue avatar eslym avatar github-actions[bot] avatar joehartzell avatar jsonnull avatar nickymeuleman avatar renovate[bot] avatar skyrpex avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

electron-trpc's Issues

TRPC 11.x.x (next) introduces breaking changes that will break electron-trpc

One of the breaking changes involves transformers, the error manifests itself on client side as:

Uncaught TypeError: Cannot read properties of undefined (reading 'serialize')

With this line being at fault:

op.input = runtime.transformer.serialize(op.input);

I've forked the repository and resolved the compatibility issues with the next version of TRPC: mat-sz@994bd41

Since this is an unstable version of TRPC, I'm not submitting this as a pull request. If you'd like me to submit that in the future when 11.x.x becomes stable, please let me know; otherwise I'll continue maintaining this as a fork.

ESM Preload Script fails to import `electron-trpc/main`

Problem

If we write

import { exposeElectronTRPC } from 'electron-trpc/main';

in ESM Preload Script preload.mjs, following error occurs in renderer process:

file:/my/workspace/try-electron-trpc/node_modules/electron-trpc/dist/main.mjs:11
import { ipcMain as ee, contextBridge as re, ipcRenderer as U } from "electron";
         ^^^^^^^
SyntaxError: The requested module 'electron' does not provide an export named 'ipcMain'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:132:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:214:5)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async node:electron/js2c/renderer_init:2:33310
    at async loadESM (node:internal/process/esm_loader:28:7)
(anonymous) @ VM111 renderer_init:2

(NB: Here sandboxing has to be disabled, since sandboxed preload scripts cannot use ESM imports.)

This means we cannot expose IPC following README

Workaround

A workaround is to copy/paste the implementation of exposeElectronTRPC() like this:

// preload.mjs
const ELECTRON_TRPC_CHANNEL = 'electron-trpc';
const electronTRPC = {
  sendMessage: (operation) =>
    ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
  onMessage: (callback) =>
    ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args)),
};
contextBridge.exposeInMainWorld('electronTRPC', electronTRPC);

(This workaround is almost same as the one described in #116 (comment).)
This works for me, but a downside is I have to care ELECTRON_TRPC_CHANNEL constant would be unchanged in future updates.

Possible solution

How about removing exposeElectronTRPC() from main.mjs and including it in another bundle like preload.mjs?
Then we can write in preload script like import { exposeElectronTRPC } from 'electron-trpc/preload'; without unnecessary reference to ipcMain.
Though I have not tried this yet, I guess this could also solve #180, where an error occurs in main process because main.mjs imports unnecessary contextBridge that comes from exposeElectronTRPC().

`process is not defined` after importing ipcLink in renderer

At first, that entire code works if I copy/paste it into my app. When I tried to use the npm package I faced with that issue:

CleanShot 2022-11-18 at 12 43 40@2x

I am using electron-react-boilerplate

I think it is caused because I am using import { ipcLink } from 'electron-trpc';. When I am trying to use import { ipcLink } from 'electron-trpc/renderer'; I am getting:

Module not found: Error: Package path ./renderer is not exported from package [...]/node_modules/electron-trpc

No way to access window from request

I would like to add a route that toggles DevTools and in order to do that I need access to the BrowserWindow that the request was made from. It doesn't look like there is a way for this, which is kind of strange as I'm seeing loads of use-cases where you would need the instance of the BrowserWindow. Or have I missed something?

EDIT: Found the createContext property :)

Cannot find module error

Hi,

For some reason all the imports in the README fail.

// All of the following give "Cannot find module..." error
import { createIPCHandler } from "electron-trpc/main";
import { exposeElectronTRPC } from "electron-trpc/main";
import { ipcLink } from "electron-trpc/renderer";

Importing from /dist or /src compiles, but fails at runtime with the same error.

I'm using version 0.2.1 of electron-trpc.

Query never resolves when the input is undefined

I'm consulting a query that does not have parameters this way:

trpc.endpoint.useQuery(undefined);

And it is never responding.

My current workaround is to send an empty object like this:

trpc.endpoint.useQuery({});

Subscriptions do not cleanup

Using your example repo, I noticed that subscriptions do not clean up. Eventually I get the following error:

[1] (node:22811) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 output listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit
[1] (Use `Electron --trace-warnings ...` to show where the warning was created)
[1] (node:22811) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 destroyed listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit
[1] subscribed

Subscription returns `undefined` with superjson

If is use superjson as transformer, this procedure:

open: publicProcedure.subscription(async () => {
  return observable<{ open: boolean }>((emit) => {
    const setFileopenVisible = (data: boolean) => {
      console.log("setFileopenVisible", data);
        emit.next({ open: data });
    };
    ee.on("setFileopenVisible", setFileopenVisible);
    return () => {
      ee.off("setFileopenVisible", setFileopenVisible);
    };
  })
}),

returns undefined when called here:

trpc.ui.openFileVisible.open.useSubscription(undefined, {
  onData: (data) => {
    console.log("data", data);
  },
});

The data is properly returned, if I remove the transformer from the api and the client.

Using electron-trpc version 0.4.2.

[Discussion] Roadmap to v1.0.0

This is a discussion around what the roadmap to v1.0.0 looks like. I really enjoy this library and believe it's the best way to work with ipc communication. I would like to contribute more to this project but don't know what the roadmap looks like.

Was hoping we could start a discussion specifically with @jsonnull around what work needs to be done to push this to v1.0.0. More than happy to help with planning, implementation, etc...

Thanks in advance for all the work on this project thus far and looking forward to future discussions!

Improved Stack Trace

Is there a way to get a more detailed stack trace for an error that occurs during an electron-trpc call? For example, a Zod error results in this:

image

Matches.tsx:267 Uncaught TRPCClientError: [
  {
    "code": "invalid_type",
    "expected": "object",
    "received": "null",
    "path": [],
    "message": "Expected object, received null"
  }
]
    at _y.from (renderer.mjs:60:10)
    at Object.next (renderer.mjs:487:23)
    at V.M (renderer.mjs:469:21)
    at renderer.mjs:447:21

There's no line numbers in the stack trace that indicate where the error actually came from.

How to use electron-trpc with Jest?

When I'm trying to run my jest tests I got this error:

Test suite failed to run

    Configuration error:

    Could not locate module electron-trpc/renderer mapped as:
    node_modules/electron-trpc/dist/renderer.

    Please check your configuration for these entries:
    {
      "moduleNameMapper": {
        "/^electron\-trpc\/renderer$/": "node_modules/electron-trpc/dist/renderer"
      },
      "resolver": undefined
    }

      2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
      3 | import { render, RenderOptions } from '@testing-library/react';
    > 4 | import { ipcLink } from 'electron-trpc/renderer';
        | ^
      5 | import { ReactElement, useState } from 'react';
      6 | import { MemoryRouter } from 'react-router-dom';
      7 |

      at createNoMappedModuleFoundError (../../node_modules/jest-resolve/build/resolver.js:759:17)
      at Object.<anonymous> (test-utils/TestWrapper.tsx:4:1)

Any ideas on how to resole this issue?

`subscription.stop` method has no `input`, causing crash with superjson transformer

I noticed a crash when using this lib with superjson. The stack trace indicated that superjson.deserialize was being called with the input being undefined.

A little digging and console logging led me to notice that this was happening for subscription.stop methods (which are being emitted here: https://github.com/jsonnull/electron-trpc/blob/main/packages/electron-trpc/src/renderer/ipcLink.ts#L77-L80) because they don't have an input property.

Thus when the handleIPCOperation function picks this up on the main thread, it is calling deserialize on an empty object (here: https://github.com/jsonnull/electron-trpc/blob/main/packages/electron-trpc/src/main/handleIPCOperation.ts#L23).

Seems like this function needs to look for the presence of operation.input before calling deserialize, or else filter by the operation method if it is a stable API.

Support tRPC v10

Hi there!

As discussed on Twitter, I'd like to help move this package to support v10 of tRPC.
I've gotten it to work on my starter that ties together vite, electron, tRPC, and Prisma.
Currently, that uses a bunch of copy-pasted internal tRPC source code.

I saw you PRd trpc/trpc#2397 for v9, so something similar for v10 is probably needed if we want to avoid rewriting a bunch of tRPCs sourcecode.

I can PR a proof-of-concept at the end of the week if you're interested!
Or wait until v10 is out of beta, let me know!

Add README and package meta to published package

I just noticed that the npm page doesn't have the necessary details for the electron-trpc package since the README is in the root and I haven't included additional meta in the published package's package.json.

Add support for different router types

Hi,

In my application I have two windows with different type of routers. For both windows I'm trying to register IPC handlers using createIPCHandler({router: router1, windows: [win1]}) and createIPCHandler({router: router2, windows: [win2]}).
When I try to invoke some query from win1, I'm getting the following error:

TRPCClientError: No "query"-procedure on path "getSomething"

I assume it happens because both routers replies to the same query simultaneously which brings to race condition. In my case, router2 is trying to handle a query that can be handled only by router1.
It would be nice to check event.source inside ipcMain.on message handler and process only those messages which come from BrowserWindow passed in windows property of createIPCHandler call.

Not compatible with default sandbox mode of Electron 20+

Since Electron 20, the sandbox option of window webPreferences has been set to true by default. This means that preload scripts can only use a small subset of the Electron and Node APIs. Specifically, require is polyfilled with a version that will only allow requiring a small subset of node core modules.

This causes a problem for this lib, since we need to require electron-trpc/main in the preload script, and then call exposeElectronTRPC().

Disabling the sandbox of course resolves this issue, but at the cost of losing the security benefits of sandboxing.

After a quick think about this I have two potential solutions:

  1. Advise users to process and bundle their preload script. This would inline the import of the constants, leaving only the allowed contextBridge code. This should be a documentation only change, perhaps based on a sample vite-plugin-electron config.
  2. Remove ELECTRON_TRPC_CHANNEL and make it up to the user to configure an appropriate and consistent channel across main, preload, and renderer. Then supply a copy/paste snippet for the preload file that only uses the contextBridge API. It would be helpful to export the RendererGlobalElectronTRPC type so consumers could at least match up with some sort of package API.

There may be other options I haven't considered.

Thanks for creating such a useful lib!

Remove `strict-peer-dependencies=false` when trpc v10 is released

In the beta versions of trpc v10, @trpc/react@next has a peer pin on beta 20 instead of beta 25 for the other @trpc packages. In order to stay on @next for all these packages, I'm leaving strict peer dependencies off for the project.

When trpc v10 releases, let's get back to strict peer deps.

handleIPCMessage only sends replies to electron main frame

Summary

I have a use case that involves using iframes and tRPC to communicate between each frame and the main thread. After spending some time debugging it seems that handleIPCMessage replies to the sender using event.sender.send which will always reply to the main frame.

References

  • Electron Docs

    • To send an asynchronous message back to the sender, you can use event.reply(...). This helper method will automatically handle messages coming from frames that aren't the main frame (e.g. iframes) whereas event.sender.send(...) will always send to the main frame.

  • handleIPCMessage code

Solution

I think the solution here is pretty simple. handleIPCMessage should be able to use event.reply to respond to incoming events as stated by the Electron docs. This would also involve updating the types on the event parameters to use ipcMainEvent instead of IpcMainInvokeEvent.

Example

Current implementation

import { IpcMainInvokeEvent } from 'electron'
export async function handleIPCMessage<TRouter extends AnyRouter>({
  router,
  createContext,
  internalId,
  message,
  event,
  subscriptions,
}: {
  router: TRouter;
  createContext?: (opts: CreateContextOptions) => Promise<inferRouterContext<TRouter>>;
  internalId: string;
  message: ETRPCRequest;
  event: IpcMainInvokeEvent; // OLD
  subscriptions: Map<string, Unsubscribable>;
}) { 
// ...
}

Suggested change

import { IpcMainEvent } from 'electron'
export async function handleIPCMessage<TRouter extends AnyRouter>({
  router,
  createContext,
  internalId,
  message,
  event,
  subscriptions,
}: {
  router: TRouter;
  createContext?: (opts: CreateContextOptions) => Promise<inferRouterContext<TRouter>>;
  internalId: string;
  message: ETRPCRequest;
  event: IpcMainEvent; // NEW
  subscriptions: Map<string, Unsubscribable>;
}) { 
// ...
}

Contributing

Would be more than happy to make a PR with the suggested changes above.

Versions

  • electron-trpc: 0.5.0
  • electron: 24.4.1
  • node: 16.14.0
  • pnpm: 8.6.1

Is this project still alive?

I noticed there isn't any recent activity on this repository.
Is there a chance this repository could be revived in the future, or are you perhaps looking for maintainers to help out?
Thanks!

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/lint.yml
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
.github/workflows/release.yml
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
  • changesets/action v1
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
  • changesets/action v1
.github/workflows/test.yml
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
  • codecov/codecov-action v4
  • actions/checkout v4
  • pnpm/action-setup v2
  • actions/setup-node v4
npm
examples/basic-react-superjson/package.json
  • @tanstack/react-query ^4.29.14
  • @trpc/client 10.33.1
  • @trpc/react-query 10.33.1
  • @trpc/server 10.33.1
  • electron 29.3.2
  • react ^18.2.0
  • react-dom ^18.2.0
  • superjson ^1.12.3
  • zod ^3.21.4
  • @types/node ^20.12.8
  • @types/react ^18.3.1
  • @types/react-dom ^18.3.0
  • @vitejs/plugin-react ^4.2.1
  • vite ^5.2.11
  • vite-plugin-electron ^0.28.7
examples/basic-react/package.json
  • @tanstack/react-query ^4.29.14
  • @trpc/client 10.33.1
  • @trpc/react-query 10.33.1
  • @trpc/server 10.33.1
  • electron 29.3.2
  • react ^18.2.0
  • react-dom ^18.2.0
  • zod ^3.21.4
  • @types/node ^20.12.8
  • @types/react ^18.3.1
  • @types/react-dom ^18.3.0
  • @vitejs/plugin-react ^4.2.1
  • vite ^5.2.11
  • vite-plugin-electron ^0.28.7
package.json
  • @changesets/changelog-github ^0.5.0
  • @changesets/cli ^2.27.1
  • @playwright/test ^1.43.1
  • prettier ^3.2.5
  • typescript ^5.4.5
  • unocss ^0.59.4
  • vite ^5.2.11
  • vitepress 1.1.4
  • vue ^3.4.26
  • node >=18
  • pnpm >=9
  • pnpm 9.0.6
packages/electron-trpc/package.json
  • debug ^4.3.4
  • @tanstack/react-query ^5.32.1
  • @trpc/client 10.45.2
  • @trpc/server 10.45.2
  • @types/debug ^4.1.12
  • @types/node ^20.12.8
  • @vitest/coverage-v8 ^1.6.0
  • builtin-modules ^4.0.0
  • dts-bundle-generator 9.5.1
  • electron 29.3.2
  • react ^18.3.1
  • react-dom ^18.3.1
  • superjson ^2.2.1
  • vite ^5.2.11
  • vite-plugin-commonjs-externals ^0.1.4
  • vitest ^1.6.0
  • zod ^3.23.6
  • @trpc/client >10.0.0
  • @trpc/server >10.0.0
  • electron >19.0.0

  • Check this box to trigger a request for Renovate to run again on this repository

Risk for memory leaks with subscriptions on page reload

Sometimes during development it's necessary to fully reload the page. I created this little subscription procedure to see whether there is a risk for memory leaks. And yes, there is. Check the code below:

let count = 0;

export const testSubscription = publicProcedure.subscription(async () => {
  return observable<string>(() => {
    count ++;
    console.log(count);

    return () => {
      count --;
    };
  });
});

Whenever this procedure is being subscribed to, count will increment or decrement to keep track of amount of active subscriptions. However, if you do a page reload, there is no cleanup process being made and count keeps increasing.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.