Skip to content

Commit 68ddf63

Browse files
authored
Rework how subscriptions are cleaned up. (#138)
* Rework how subscriptions are cleaned up. * Add e2e tests. * Fix tests. * Tweak e2e pipeline. * Fix e2e test. * Add changeset.
1 parent 0a4a01f commit 68ddf63

File tree

15 files changed

+284
-177
lines changed

15 files changed

+284
-177
lines changed

.changeset/swift-lies-carry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'electron-trpc': minor
3+
---
4+
5+
Rework how subscriptions are cleaned up.

.github/workflows/main.yml

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
name: CI
2+
23
on: [push]
4+
5+
env:
6+
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
7+
38
jobs:
49
test:
510
runs-on: ${{ matrix.os }}
611
strategy:
712
matrix:
8-
node: ['16.x']
13+
node: ['18.x']
914
os: [ubuntu-latest]
1015

1116
steps:
@@ -19,15 +24,35 @@ jobs:
1924
cache: 'pnpm'
2025
- run: pnpm install --frozen-lockfile
2126
- run: pnpm build
22-
- run: pnpm --filter=examples/basic build
2327
- run: pnpm test:coverage
2428
- uses: codecov/codecov-action@v3
2529

30+
test-e2e:
31+
runs-on: ${{ matrix.os }}
32+
strategy:
33+
matrix:
34+
node: ['18.x']
35+
os: [ubuntu-latest]
36+
37+
steps:
38+
- uses: actions/checkout@v3
39+
- uses: pnpm/action-setup@v2
40+
with:
41+
version: 7
42+
- uses: actions/setup-node@v3
43+
with:
44+
node-version: ${{ matrix.node }}
45+
cache: 'pnpm'
46+
- run: pnpm install --frozen-lockfile
47+
- run: pnpm build
48+
- run: pnpm --filter="examples/*" build
49+
- run: xvfb-run --auto-servernum -- pnpm test:e2e
50+
2651
release-main:
2752
runs-on: ${{ matrix.os }}
2853
strategy:
2954
matrix:
30-
node: ['16.x']
55+
node: ['18.x']
3156
os: [ubuntu-latest]
3257

3358
needs: test
@@ -55,7 +80,7 @@ jobs:
5580
runs-on: ${{ matrix.os }}
5681
strategy:
5782
matrix:
58-
node: ['16.x']
83+
node: ['18.x']
5984
os: [ubuntu-latest]
6085

6186
needs: test

examples/basic-react/index.e2e.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { _electron as electron } from 'playwright';
2+
import { test, expect } from '@playwright/test';
3+
4+
test('Hello Electron', async () => {
5+
const electronApp = await electron.launch({ args: [`${__dirname}`] });
6+
7+
const window = await electronApp.firstWindow();
8+
expect(await window.title()).toBe('Hello from Electron renderer!');
9+
10+
const response = await window.textContent('[data-testid="greeting"]');
11+
expect(response).toBe('Hello Electron');
12+
13+
await electronApp.close();
14+
});

examples/basic-react/src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function HelloElectron() {
3636
return null;
3737
}
3838

39-
return <div>{data.text}</div>;
39+
return <div data-testid="greeting">{data.text}</div>;
4040
}
4141

4242
ReactDom.render(<App />, document.getElementById('react-root'));

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"scripts": {
44
"build": "pnpm --filter=electron-trpc build",
55
"test": "pnpm --filter=electron-trpc test",
6+
"test:e2e": "playwright test",
67
"test:coverage": "pnpm --filter=electron-trpc test:coverage",
78
"prepublish": "yarn build",
89
"changeset": "changeset",
@@ -15,6 +16,8 @@
1516
"devDependencies": {
1617
"@changesets/changelog-github": "^0.4.8",
1718
"@changesets/cli": "^2.26.1",
19+
"@playwright/test": "^1.32.3",
20+
"playwright": "^1.32.3",
1821
"prettier": "^2.8.7",
1922
"typescript": "^5.0.4",
2023
"unocss": "^0.51.4",

packages/electron-trpc/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@tanstack/react-query": "^4.29.1",
3737
"@trpc/client": "10.20.0",
3838
"@trpc/server": "10.20.0",
39+
"@types/debug": "^4.1.7",
3940
"@types/node": "^18.15.11",
4041
"@vitest/coverage-c8": "^0.30.1",
4142
"builtin-modules": "^3.3.0",
@@ -53,5 +54,8 @@
5354
"@trpc/client": ">10.0.0",
5455
"@trpc/server": ">10.0.0",
5556
"electron": ">19.0.0"
57+
},
58+
"dependencies": {
59+
"debug": "^4.3.4"
5660
}
5761
}

packages/electron-trpc/src/main/__tests__/handleIPCOperation.test.ts renamed to packages/electron-trpc/src/main/__tests__/handleIPCMessage.test.ts

Lines changed: 53 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { z } from 'zod';
33
import * as trpc from '@trpc/server';
44
import { observable } from '@trpc/server/observable';
55
import { EventEmitter } from 'events';
6-
import { handleIPCOperation } from '../handleIPCOperation';
6+
import { handleIPCMessage } from '../handleIPCMessage';
77
import { IpcMainInvokeEvent } from 'electron';
88

99
interface MockEvent {
@@ -51,11 +51,22 @@ describe('api', () => {
5151
},
5252
});
5353

54-
await handleIPCOperation({
54+
await handleIPCMessage({
5555
createContext: async () => ({}),
56-
operation: { context: {}, id: 1, input: { id: 'test-id' }, path: 'testQuery', type: 'query' },
57-
router: testRouter,
5856
event,
57+
internalId: '1-1:1',
58+
message: {
59+
method: 'request',
60+
operation: {
61+
context: {},
62+
id: 1,
63+
input: { id: 'test-id' },
64+
path: 'testQuery',
65+
type: 'query',
66+
},
67+
},
68+
router: testRouter,
69+
subscriptions: new Map(),
5970
});
6071

6172
expect(event.sender.send).toHaveBeenCalledOnce();
@@ -79,11 +90,22 @@ describe('api', () => {
7990
},
8091
});
8192

82-
await handleIPCOperation({
93+
await handleIPCMessage({
8394
createContext: async () => ({}),
84-
operation: { context: {}, id: 1, input: { id: 'test-id' }, path: 'testQuery', type: 'query' },
85-
router: testRouter,
8695
event,
96+
internalId: '1-1:1',
97+
message: {
98+
method: 'request',
99+
operation: {
100+
context: {},
101+
id: 1,
102+
input: { id: 'test-id' },
103+
path: 'testQuery',
104+
type: 'query',
105+
},
106+
},
107+
router: testRouter,
108+
subscriptions: new Map(),
87109
});
88110

89111
expect(event.sender.send).not.toHaveBeenCalled();
@@ -98,15 +120,20 @@ describe('api', () => {
98120
},
99121
});
100122

101-
await handleIPCOperation({
123+
await handleIPCMessage({
102124
createContext: async () => ({}),
103-
operation: {
104-
context: {},
105-
id: 1,
106-
input: undefined,
107-
path: 'testSubscription',
108-
type: 'subscription',
125+
message: {
126+
method: 'request',
127+
operation: {
128+
context: {},
129+
id: 1,
130+
input: undefined,
131+
path: 'testSubscription',
132+
type: 'subscription',
133+
},
109134
},
135+
internalId: '1-1:1',
136+
subscriptions: new Map(),
110137
router: testRouter,
111138
event,
112139
});
@@ -124,41 +151,6 @@ describe('api', () => {
124151
});
125152
});
126153

127-
test('cancels subscriptions when webcontents are closed', async () => {
128-
let isDestroyed = false;
129-
let onDestroyed: () => void;
130-
const event = makeEvent({
131-
sender: {
132-
isDestroyed: () => isDestroyed,
133-
send: vi.fn(),
134-
on: (_event: string, cb: () => void) => {
135-
onDestroyed = cb;
136-
},
137-
},
138-
});
139-
140-
await handleIPCOperation({
141-
createContext: async () => ({}),
142-
operation: {
143-
context: {},
144-
id: 1,
145-
input: undefined,
146-
path: 'testSubscription',
147-
type: 'subscription',
148-
},
149-
router: testRouter,
150-
event,
151-
});
152-
153-
expect(event.sender.send).not.toHaveBeenCalled();
154-
155-
onDestroyed();
156-
157-
ee.emit('test');
158-
159-
expect(event.sender.send).not.toHaveBeenCalled();
160-
});
161-
162154
test('subscription responds using custom serializer', async () => {
163155
const event = makeEvent({
164156
sender: {
@@ -193,15 +185,20 @@ describe('api', () => {
193185
}),
194186
});
195187

196-
await handleIPCOperation({
188+
await handleIPCMessage({
197189
createContext: async () => ({}),
198-
operation: {
199-
context: {},
200-
id: 1,
201-
input: undefined,
202-
path: 'testSubscription',
203-
type: 'subscription',
190+
message: {
191+
method: 'request',
192+
operation: {
193+
context: {},
194+
id: 1,
195+
input: undefined,
196+
path: 'testSubscription',
197+
type: 'subscription',
198+
},
204199
},
200+
internalId: '1-1:1',
201+
subscriptions: new Map(),
205202
router: testRouter,
206203
event,
207204
});
@@ -219,64 +216,4 @@ describe('api', () => {
219216
},
220217
});
221218
});
222-
223-
test("doesn't crash when canceling subscriptions when custom deserializer doesn't allow undefined", async () => {
224-
const t = trpc.initTRPC.create({
225-
transformer: {
226-
deserialize: (input: unknown) => {
227-
if (!input) throw new Error("Can't parse empty input");
228-
return JSON.parse(input as string);
229-
},
230-
serialize: (input) => {
231-
return JSON.stringify(input);
232-
},
233-
},
234-
});
235-
236-
const testRouter = t.router({
237-
testSubscription: t.procedure.subscription(() => {
238-
return observable((emit) => {
239-
function testResponse() {
240-
emit.next('test response');
241-
}
242-
243-
ee.on('test', testResponse);
244-
return () => ee.off('test', testResponse);
245-
});
246-
}),
247-
});
248-
249-
let isDestroyed = false;
250-
let onDestroyed: () => void;
251-
const event = makeEvent({
252-
sender: {
253-
isDestroyed: () => isDestroyed,
254-
send: vi.fn(),
255-
on: (_event: string, cb: () => void) => {
256-
onDestroyed = cb;
257-
},
258-
},
259-
});
260-
261-
await handleIPCOperation({
262-
createContext: async () => ({}),
263-
operation: {
264-
context: {},
265-
id: 1,
266-
input: undefined,
267-
path: 'testSubscription',
268-
type: 'subscription',
269-
},
270-
router: testRouter,
271-
event,
272-
});
273-
274-
expect(event.sender.send).not.toHaveBeenCalled();
275-
276-
onDestroyed();
277-
278-
ee.emit('test');
279-
280-
expect(event.sender.send).not.toHaveBeenCalled();
281-
});
282219
});

0 commit comments

Comments
 (0)