diff --git a/packages/electron-trpc/package.json b/packages/electron-trpc/package.json index 109687da..159ebb42 100644 --- a/packages/electron-trpc/package.json +++ b/packages/electron-trpc/package.json @@ -34,8 +34,8 @@ }, "devDependencies": { "@tanstack/react-query": "^5.32.1", - "@trpc/client": "10.45.2", - "@trpc/server": "10.45.2", + "@trpc/client": "next", + "@trpc/server": "next", "@types/debug": "^4.1.12", "@types/node": "^20.12.8", "@vitest/coverage-v8": "^1.6.0", @@ -51,8 +51,8 @@ "zod": "^3.23.6" }, "peerDependencies": { - "@trpc/client": ">10.0.0", - "@trpc/server": ">10.0.0", + "@trpc/client": ">=11.0.0", + "@trpc/server": ">=11.0.0", "electron": ">19.0.0" }, "dependencies": { diff --git a/packages/electron-trpc/src/main/__tests__/utils.test.ts b/packages/electron-trpc/src/main/__tests__/utils.test.ts deleted file mode 100644 index e68d6acc..00000000 --- a/packages/electron-trpc/src/main/__tests__/utils.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { getTRPCErrorFromUnknown } from '../utils'; -import { TRPCError } from '@trpc/server'; - -describe('getTRPCErrorFromUnknown', () => { - test('should return a TRPCError when given a TRPCError', () => { - const error = new TRPCError({ - code: 'TIMEOUT', - cause: new Error('test'), - message: 'test', - }); - const result = getTRPCErrorFromUnknown(error); - - expect(result).toBe(error); - }); - - test('should return a TRPCError when given an Error', () => { - const error = new Error('test'); - const result = getTRPCErrorFromUnknown(error); - - expect(result).toBeInstanceOf(TRPCError); - expect(result).toMatchObject({ - code: 'INTERNAL_SERVER_ERROR', - cause: error, - message: error.message, - }); - }); - - test('should return a TRPCError when given a string', () => { - const error = 'test'; - const result = getTRPCErrorFromUnknown(error); - - expect(result).toBeInstanceOf(TRPCError); - expect(result).toMatchObject({ - code: 'INTERNAL_SERVER_ERROR', - cause: new Error(error), - message: error, - }); - }); - - test('should use the stack from the given error', () => { - const error = new Error('test'); - error.stack = 'test stack'; - const result = getTRPCErrorFromUnknown(error); - - expect(result.stack).toBe(error.stack); - }); - - test.each([{ test: 'test' }, undefined, null])( - 'should fallback to "Unknown error" when given an unknown type', - (error: unknown) => { - const result = getTRPCErrorFromUnknown(error); - - expect(result).toBeInstanceOf(TRPCError); - expect(result).toMatchObject({ - code: 'INTERNAL_SERVER_ERROR', - cause: new Error('Unknown error'), - message: 'Unknown error', - }); - } - ); -}); diff --git a/packages/electron-trpc/src/main/createIPCHandler.ts b/packages/electron-trpc/src/main/createIPCHandler.ts index 0ee086db..ac2869f4 100644 --- a/packages/electron-trpc/src/main/createIPCHandler.ts +++ b/packages/electron-trpc/src/main/createIPCHandler.ts @@ -1,23 +1,24 @@ -import type { AnyRouter, inferRouterContext } from '@trpc/server'; +import type { AnyTRPCRouter, inferRouterContext } from '@trpc/server'; +import { Unsubscribable } from '@trpc/server/observable'; import { ipcMain } from 'electron'; import type { BrowserWindow, IpcMainEvent } from 'electron'; -import { handleIPCMessage } from './handleIPCMessage'; -import { CreateContextOptions } from './types'; + import { ELECTRON_TRPC_CHANNEL } from '../constants'; import { ETRPCRequest } from '../types'; -import { Unsubscribable } from '@trpc/server/observable'; +import { handleIPCMessage } from './handleIPCMessage'; +import { CreateContextOptions } from './types'; import debugFactory from 'debug'; const debug = debugFactory('electron-trpc:main:IPCHandler'); -type Awaitable = T | Promise; +type MaybePromise = Promise | TType; const getInternalId = (event: IpcMainEvent, request: ETRPCRequest) => { const messageId = request.method === 'request' ? request.operation.id : request.id; return `${event.sender.id}-${event.senderFrame.routingId}:${messageId}`; }; -class IPCHandler { +class IPCHandler { #windows: BrowserWindow[] = []; #subscriptions: Map = new Map(); @@ -26,7 +27,7 @@ class IPCHandler { router, windows = [], }: { - createContext?: (opts: CreateContextOptions) => Awaitable>; + createContext?: (opts: CreateContextOptions) => MaybePromise>; router: TRouter; windows?: BrowserWindow[]; }) { @@ -97,7 +98,7 @@ class IPCHandler { } } -export const createIPCHandler = ({ +export const createIPCHandler = ({ createContext, router, windows = [], diff --git a/packages/electron-trpc/src/main/handleIPCMessage.ts b/packages/electron-trpc/src/main/handleIPCMessage.ts index bce69a57..0d1ddc0c 100644 --- a/packages/electron-trpc/src/main/handleIPCMessage.ts +++ b/packages/electron-trpc/src/main/handleIPCMessage.ts @@ -1,10 +1,9 @@ -import { callProcedure, TRPCError } from '@trpc/server'; -import type { AnyRouter, inferRouterContext } from '@trpc/server'; +import { callTRPCProcedure, TRPCError, getErrorShape, getTRPCErrorFromUnknown } from '@trpc/server'; +import type { AnyTRPCRouter, inferRouterContext } from '@trpc/server'; import type { TRPCResponseMessage } from '@trpc/server/rpc'; import type { IpcMainEvent } from 'electron'; import { isObservable, Unsubscribable } from '@trpc/server/observable'; -import { transformTRPCResponse } from '@trpc/server/shared'; -import { getTRPCErrorFromUnknown } from './utils'; +import { transformTRPCResponse } from '@trpc/server'; import { CreateContextOptions } from './types'; import { ELECTRON_TRPC_CHANNEL } from '../constants'; import { ETRPCRequest } from '../types'; @@ -12,7 +11,7 @@ import debugFactory from 'debug'; const debug = debugFactory('electron-trpc:main:handleIPCMessage'); -export async function handleIPCMessage({ +export async function handleIPCMessage({ router, createContext, internalId, @@ -51,11 +50,11 @@ export async function handleIPCMessage({ }; try { - const result = await callProcedure({ + const result = await callTRPCProcedure({ ctx, path, procedures: router._def.procedures, - rawInput: input, + getRawInput: async () => input, type, }); @@ -91,7 +90,8 @@ export async function handleIPCMessage({ const error = getTRPCErrorFromUnknown(err); respond({ id, - error: router.getErrorShape({ + error: getErrorShape({ + config: router._def._config, error, type, path, @@ -117,7 +117,8 @@ export async function handleIPCMessage({ return respond({ id, - error: router.getErrorShape({ + error: getErrorShape({ + config: router._def._config, error, type, path, diff --git a/packages/electron-trpc/src/main/utils.ts b/packages/electron-trpc/src/main/utils.ts deleted file mode 100644 index 395c4dff..00000000 --- a/packages/electron-trpc/src/main/utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { TRPCError } from '@trpc/server'; - -// modified from @trpc/server/src/error/utils -export function getTRPCErrorFromUnknown(cause: unknown): TRPCError { - if (cause instanceof TRPCError) { - return cause; - } - - const error = getErrorFromUnknown(cause); - const trpcError = new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - cause: error, - message: error.message, - }); - - // Inherit stack from error - trpcError.stack = error.stack; - - return trpcError; -} - -// modified from @trpc/server/src/error/utils -function getErrorFromUnknown(cause: unknown): Error { - if (cause instanceof Error) { - return cause; - } - - if (typeof cause === 'string') { - return new Error(cause); - } - - return new Error('Unknown error'); -} diff --git a/packages/electron-trpc/src/renderer/__tests__/ipcLink.test.ts b/packages/electron-trpc/src/renderer/__tests__/ipcLink.test.ts index 5aaf4b4e..07ed8c5e 100644 --- a/packages/electron-trpc/src/renderer/__tests__/ipcLink.test.ts +++ b/packages/electron-trpc/src/renderer/__tests__/ipcLink.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { createTRPCProxyClient } from '@trpc/client'; +import { createTRPCClient } from '@trpc/client'; import { initTRPC } from '@trpc/server'; import type { TRPCResponseMessage } from '@trpc/server/rpc'; import z from 'zod'; @@ -7,7 +7,7 @@ import type { RendererGlobalElectronTRPC } from '../../types'; import { ipcLink } from '../ipcLink'; import superjson from 'superjson'; -const t = initTRPC.create(); +const t = initTRPC.create({ transformer: superjson }); const router = t.router({ testQuery: t.procedure.query(() => 'query success'), testMutation: t.procedure.input(z.string()).mutation(() => 'mutation success'), @@ -18,7 +18,13 @@ const router = t.router({ }; }), testInputs: t.procedure - .input(z.object({ str: z.string(), date: z.date(), bigint: z.bigint() })) + .input( + z.object({ + str: z.string(), + date: z.date(), + bigint: z.bigint().optional(), + }) + ) .query((input) => { return input; }), @@ -40,15 +46,15 @@ vi.stubGlobal('electronTRPC', electronTRPC); describe('ipcLink', () => { test('can create ipcLink', () => { - expect(() => createTRPCProxyClient({ links: [ipcLink()] })).not.toThrow(); + expect(() => createTRPCClient({ links: [ipcLink()] })).not.toThrow(); }); describe('operations', () => { - let client: ReturnType>; + let client: ReturnType>; const mock = vi.mocked(electronTRPC); beforeEach(() => { - client = createTRPCProxyClient({ links: [ipcLink()] }); + client = createTRPCClient({ links: [ipcLink()] }); }); test('routes query to/from', async () => { @@ -225,9 +231,8 @@ describe('ipcLink', () => { }); test('serializes inputs/outputs', async () => { - const client = createTRPCProxyClient({ - transformer: superjson, - links: [ipcLink()], + const client = createTRPCClient({ + links: [ipcLink({ transformer: superjson })], }); const mock = vi.mocked(electronTRPC); @@ -270,12 +275,15 @@ describe('ipcLink', () => { }); test('serializes inputs with custom transformer', async () => { - const client = createTRPCProxyClient({ - transformer: { - serialize: (input) => JSON.stringify(input), - deserialize: (input) => JSON.parse(input), - }, - links: [ipcLink()], + const client = createTRPCClient({ + links: [ + ipcLink({ + transformer: { + serialize: (input) => JSON.stringify(input), + deserialize: (input) => JSON.parse(input), + }, + }), + ], }); const mock = vi.mocked(electronTRPC); diff --git a/packages/electron-trpc/src/renderer/ipcLink.ts b/packages/electron-trpc/src/renderer/ipcLink.ts index 0f983f52..55b81595 100644 --- a/packages/electron-trpc/src/renderer/ipcLink.ts +++ b/packages/electron-trpc/src/renderer/ipcLink.ts @@ -1,25 +1,31 @@ import { Operation, TRPCClientError, TRPCLink } from '@trpc/client'; -import type { AnyRouter, inferRouterContext, ProcedureType } from '@trpc/server'; +import type { + AnyTRPCRouter, + inferRouterContext, + TRPCProcedureType, + inferTRPCClientTypes, +} from '@trpc/server'; import type { TRPCResponseMessage } from '@trpc/server/rpc'; -import type { RendererGlobalElectronTRPC } from '../types'; import { observable, Observer } from '@trpc/server/observable'; -import { transformResult } from './utils'; import debugFactory from 'debug'; +import { type TransformerOptions, getTransformer } from '@trpc/client/unstable-internals'; +import type { RendererGlobalElectronTRPC } from '../types'; +import { transformResult } from './utils'; const debug = debugFactory('electron-trpc:renderer:ipcLink'); -type IPCCallbackResult = TRPCResponseMessage< +type IPCCallbackResult = TRPCResponseMessage< unknown, inferRouterContext >; -type IPCCallbacks = Observer< +type IPCCallbacks = Observer< IPCCallbackResult, TRPCClientError >; type IPCRequest = { - type: ProcedureType; + type: TRPCProcedureType; callbacks: IPCCallbacks; op: Operation; }; @@ -88,13 +94,20 @@ class IPCClient { } } -export function ipcLink(): TRPCLink { - return (runtime) => { +export type IPCLinkOptions = TransformerOptions< + inferTRPCClientTypes +>; + +export function ipcLink( + opts?: IPCLinkOptions +): TRPCLink { + return () => { const client = new IPCClient(); + const transformer = getTransformer(opts?.transformer); return ({ op }) => { return observable((observer) => { - op.input = runtime.transformer.serialize(op.input); + op.input = transformer.input.serialize(op.input); const unsubscribe = client.request(op, { error(err) { @@ -105,7 +118,7 @@ export function ipcLink(): TRPCLink { observer.complete(); }, next(response) { - const transformed = transformResult(response, runtime); + const transformed = transformResult(response, transformer.output); if (!transformed.ok) { observer.error(TRPCClientError.from(transformed.error)); diff --git a/packages/electron-trpc/src/renderer/utils.ts b/packages/electron-trpc/src/renderer/utils.ts index 73c2f353..0cbf5cdf 100644 --- a/packages/electron-trpc/src/renderer/utils.ts +++ b/packages/electron-trpc/src/renderer/utils.ts @@ -1,20 +1,19 @@ -import type { AnyRouter, inferRouterError } from '@trpc/server'; +import type { AnyTRPCRouter, inferRouterError, TRPCCombinedDataTransformer } from '@trpc/server'; import type { TRPCResponse, TRPCResponseMessage, TRPCResultMessage } from '@trpc/server/rpc'; -import type { TRPCClientRuntime } from '@trpc/client'; // from @trpc/client/src/links/internals/transformResult // FIXME: // - the generics here are probably unnecessary // - the RPC-spec could probably be simplified to combine HTTP + WS /** @internal */ -export function transformResult( +export function transformResult( response: | TRPCResponseMessage> | TRPCResponse>, - runtime: TRPCClientRuntime + transformer: TRPCCombinedDataTransformer['output'] ) { if ('error' in response) { - const error = runtime.transformer.deserialize(response.error) as inferRouterError; + const error = transformer.deserialize(response.error) as inferRouterError; return { ok: false, error: { @@ -28,7 +27,7 @@ export function transformResult( ...response.result, ...((!response.result.type || response.result.type === 'data') && { type: 'data', - data: runtime.transformer.deserialize(response.result.data) as unknown, + data: transformer.deserialize(response.result.data) as unknown, }), } as TRPCResultMessage['result']; return { ok: true, result } as const; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 024eb25d..549c15f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,11 +147,11 @@ importers: specifier: ^5.32.1 version: 5.32.1(react@18.3.1) '@trpc/client': - specifier: 10.45.2 - version: 10.45.2(@trpc/server@10.45.2) + specifier: next + version: 11.0.0-rc.364(@trpc/server@11.0.0-rc.364) '@trpc/server': - specifier: 10.45.2 - version: 10.45.2 + specifier: next + version: 11.0.0-rc.364 '@types/debug': specifier: ^4.1.12 version: 4.1.12 @@ -852,10 +852,10 @@ packages: peerDependencies: '@trpc/server': 10.33.1 - '@trpc/client@10.45.2': - resolution: {integrity: sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==} + '@trpc/client@11.0.0-rc.364': + resolution: {integrity: sha512-k9ZH7I0OOBFME9fZJ1JI0ZjvPEV64bvoT20kWHkFeMH5trJmK1tzcc/q7c7cLBY4pziLPqBDPFw2qax09vyRTg==} peerDependencies: - '@trpc/server': 10.45.2 + '@trpc/server': 11.0.0-rc.364+d95fb467b '@trpc/react-query@10.33.1': resolution: {integrity: sha512-Jpdg9xOyVJ4k+0wg28CxEEU1KcUVP+nMzpPbhISWOKaP1WZTGaQxphsCwx8KEFVaRiutKGBxhC24D+WhX7tMpA==} @@ -869,8 +869,8 @@ packages: '@trpc/server@10.33.1': resolution: {integrity: sha512-ObMzyEALXhQ7QTVQdSJ+IL1wYB7UdLLoIhqHraYzqKb9sTTsL3+U64kK084KPsRd4b8R3yH/bzBOnCn8Hbrxkw==} - '@trpc/server@10.45.2': - resolution: {integrity: sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg==} + '@trpc/server@11.0.0-rc.364': + resolution: {integrity: sha512-sGlrb2jSCiU74TrJ+N+WxIcJIUDyo0WOy0y30vIArw6oHFXjeithgxjvIu+PbN+VWt/Gt4ee8kVhnmxCW4X/Qw==} '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -3742,9 +3742,9 @@ snapshots: dependencies: '@trpc/server': 10.33.1 - '@trpc/client@10.45.2(@trpc/server@10.45.2)': + '@trpc/client@11.0.0-rc.364(@trpc/server@11.0.0-rc.364)': dependencies: - '@trpc/server': 10.45.2 + '@trpc/server': 11.0.0-rc.364 '@trpc/react-query@10.33.1(@tanstack/react-query@4.29.14(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.33.1(@trpc/server@10.33.1))(@trpc/server@10.33.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: @@ -3756,7 +3756,7 @@ snapshots: '@trpc/server@10.33.1': {} - '@trpc/server@10.45.2': {} + '@trpc/server@11.0.0-rc.364': {} '@types/babel__core@7.20.5': dependencies: