aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-27 17:41:48 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-27 17:41:48 +0100
commit0242ba240e16355e27746ca98fd5df65e5241102 (patch)
tree7700b2505b50b1df28731bed3f6040c31cc05535
parentchore: Bump dependency versions (diff)
downloadsophie-0242ba240e16355e27746ca98fd5df65e5241102.tar.gz
sophie-0242ba240e16355e27746ca98fd5df65e5241102.tar.zst
sophie-0242ba240e16355e27746ca98fd5df65e5241102.zip
fix: Allow the shared store listener to re-register in dev mode
This way the shared store will be able to stay connected even if vite HMR replaces the renderer code.
-rw-r--r--packages/preload/src/contextBridge/SophieRendererImpl.ts8
-rw-r--r--packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts51
-rw-r--r--packages/preload/src/index.ts4
-rw-r--r--packages/preload/tsconfig.json3
4 files changed, 45 insertions, 21 deletions
diff --git a/packages/preload/src/contextBridge/SophieRendererImpl.ts b/packages/preload/src/contextBridge/SophieRendererImpl.ts
index 18ab07e..4c24b74 100644
--- a/packages/preload/src/contextBridge/SophieRendererImpl.ts
+++ b/packages/preload/src/contextBridge/SophieRendererImpl.ts
@@ -35,7 +35,7 @@ class SophieRendererImpl implements SophieRenderer {
35 35
36 private listener: SharedStoreListener | null = null; 36 private listener: SharedStoreListener | null = null;
37 37
38 constructor() { 38 constructor(private readonly allowReplaceListener: boolean) {
39 ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => { 39 ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => {
40 try { 40 try {
41 // `mobx-state-tree` will validate the patch, so we can safely cast here. 41 // `mobx-state-tree` will validate the patch, so we can safely cast here.
@@ -48,7 +48,7 @@ class SophieRendererImpl implements SophieRenderer {
48 } 48 }
49 49
50 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> { 50 async onSharedStoreChange(listener: SharedStoreListener): Promise<void> {
51 if (this.onSharedStoreChangeCalled) { 51 if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) {
52 throw new Error('Shared store change listener was already set'); 52 throw new Error('Shared store change listener was already set');
53 } 53 }
54 this.onSharedStoreChangeCalled = true; 54 this.onSharedStoreChangeCalled = true;
@@ -86,8 +86,8 @@ class SophieRendererImpl implements SophieRenderer {
86 } 86 }
87} 87}
88 88
89export function createSophieRenderer(): SophieRenderer { 89export function createSophieRenderer(allowReplaceListener: boolean): SophieRenderer {
90 const impl = new SophieRendererImpl(); 90 const impl = new SophieRendererImpl(allowReplaceListener);
91 return { 91 return {
92 onSharedStoreChange: impl.onSharedStoreChange.bind(impl), 92 onSharedStoreChange: impl.onSharedStoreChange.bind(impl),
93 dispatchAction: impl.dispatchAction.bind(impl), 93 dispatchAction: impl.dispatchAction.bind(impl),
diff --git a/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts b/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts
index 2f295e6..71ac2a1 100644
--- a/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts
+++ b/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts
@@ -60,7 +60,7 @@ const invalidAction = {
60 60
61describe('createSophieRenderer', () => { 61describe('createSophieRenderer', () => {
62 it('registers a shared store patch listener', () => { 62 it('registers a shared store patch listener', () => {
63 createSophieRenderer(); 63 createSophieRenderer(false);
64 expect(ipcRenderer.on).toHaveBeenCalledWith( 64 expect(ipcRenderer.on).toHaveBeenCalledWith(
65 MainToRendererIpcMessage.SharedStorePatch, 65 MainToRendererIpcMessage.SharedStorePatch,
66 expect.anything(), 66 expect.anything(),
@@ -77,26 +77,21 @@ describe('SophieRendererImpl', () => {
77 }; 77 };
78 78
79 beforeEach(() => { 79 beforeEach(() => {
80 sut = createSophieRenderer(); 80 sut = createSophieRenderer(false);
81 onSharedStorePatch = mocked(ipcRenderer.on).mock.calls.find(([channel]) => { 81 onSharedStorePatch = mocked(ipcRenderer.on).mock.calls.find(([channel]) => {
82 return channel === MainToRendererIpcMessage.SharedStorePatch; 82 return channel === MainToRendererIpcMessage.SharedStorePatch;
83 })?.[1]!; 83 })?.[1]!;
84 }); 84 });
85 85
86 describe('onSharedStoreChange', () => { 86 describe('onSharedStoreChange', () => {
87 it('requests a snapshot from the service', async () => { 87 it('requests a snapshot from the main process', async () => {
88 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); 88 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot);
89 await sut.onSharedStoreChange(listener); 89 await sut.onSharedStoreChange(listener);
90 expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); 90 expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot);
91 });
92
93 it('passes the snapshot to the listener', async () => {
94 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot);
95 await sut.onSharedStoreChange(listener);
96 expect(listener.onSnapshot).toBeCalledWith(snapshot); 91 expect(listener.onSnapshot).toBeCalledWith(snapshot);
97 }); 92 });
98 93
99 it('catches service errors without exposing them', async () => { 94 it('catches IPC errors without exposing them', async () => {
100 mocked(ipcRenderer.invoke).mockRejectedValue(new Error('s3cr3t')); 95 mocked(ipcRenderer.invoke).mockRejectedValue(new Error('s3cr3t'));
101 await expect(sut.onSharedStoreChange(listener)).rejects.not.toHaveProperty( 96 await expect(sut.onSharedStoreChange(listener)).rejects.not.toHaveProperty(
102 'message', 97 'message',
@@ -125,7 +120,7 @@ describe('SophieRendererImpl', () => {
125 }); 120 });
126 121
127 describe('when no listener is registered', () => { 122 describe('when no listener is registered', () => {
128 it('discards the received patch', () => { 123 it('discards the received patch without any error', () => {
129 onSharedStorePatch(event, patch); 124 onSharedStorePatch(event, patch);
130 }); 125 });
131 }); 126 });
@@ -136,8 +131,10 @@ describe('SophieRendererImpl', () => {
136 }); 131 });
137 } 132 }
138 133
139 function itDoesNotPassPatchesToTheListener() { 134 function itDoesNotPassPatchesToTheListener(
140 it('does not pass patches to the listener', () => { 135 name: string = 'does not pass patches to the listener',
136 ) {
137 it(name, () => {
141 onSharedStorePatch(event, patch); 138 onSharedStorePatch(event, patch);
142 expect(listener.onPatch).toBeCalledTimes(0); 139 expect(listener.onPatch).toBeCalledTimes(0);
143 }); 140 });
@@ -161,18 +158,18 @@ describe('SophieRendererImpl', () => {
161 158
162 itRefusesToRegisterAnotherListener(); 159 itRefusesToRegisterAnotherListener();
163 160
164 describe('that later threw an error', () => { 161 describe('after the listener threw in onPatch', () => {
165 beforeEach(() => { 162 beforeEach(() => {
166 mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); 163 mocked(listener.onPatch).mockImplementation(() => { throw new Error(); });
167 onSharedStorePatch(event, patch); 164 onSharedStorePatch(event, patch);
168 listener.onPatch.mockRestore(); 165 listener.onPatch.mockRestore();
169 }); 166 });
170 167
171 itDoesNotPassPatchesToTheListener(); 168 itDoesNotPassPatchesToTheListener('does not pass on patches any more');
172 }); 169 });
173 }); 170 });
174 171
175 describe('when a listener failed to register due to service error', () => { 172 describe('when a listener failed to register due to IPC error', () => {
176 beforeEach(async () => { 173 beforeEach(async () => {
177 mocked(ipcRenderer.invoke).mockRejectedValue(new Error()); 174 mocked(ipcRenderer.invoke).mockRejectedValue(new Error());
178 try { 175 try {
@@ -217,4 +214,28 @@ describe('SophieRendererImpl', () => {
217 214
218 itDoesNotPassPatchesToTheListener(); 215 itDoesNotPassPatchesToTheListener();
219 }); 216 });
217
218 describe('when it is allowed to replace listeners', () => {
219 const snapshot2 = {
220 shouldUseDarkColors: false,
221 }
222 const listener2 = {
223 onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => { }),
224 onPatch: jest.fn((_patch: IJsonPatch) => { }),
225 };
226
227 it('should fetch a second snapshot', async () => {
228 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2);
229 await sut.onSharedStoreChange(listener2);
230 expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot);
231 expect(listener2.onSnapshot).toBeCalledWith(snapshot2);
232 });
233
234 it('should pass the second snapshot to the new listener', async () => {
235 mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2);
236 await sut.onSharedStoreChange(listener2);
237 onSharedStorePatch(event, patch);
238 expect(listener2.onPatch).toBeCalledWith(patch);
239 });
240 });
220}); 241});
diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts
index ef466b4..de91742 100644
--- a/packages/preload/src/index.ts
+++ b/packages/preload/src/index.ts
@@ -22,6 +22,8 @@ import { contextBridge } from 'electron';
22 22
23import { createSophieRenderer } from './contextBridge/SophieRendererImpl'; 23import { createSophieRenderer } from './contextBridge/SophieRendererImpl';
24 24
25const sophieRenderer = createSophieRenderer(); 25const isDevelopment = import.meta.env.MODE === 'development';
26
27const sophieRenderer = createSophieRenderer(isDevelopment);
26 28
27contextBridge.exposeInMainWorld('sophieRenderer', sophieRenderer); 29contextBridge.exposeInMainWorld('sophieRenderer', sophieRenderer);
diff --git a/packages/preload/tsconfig.json b/packages/preload/tsconfig.json
index ab274a1..49f223a 100644
--- a/packages/preload/tsconfig.json
+++ b/packages/preload/tsconfig.json
@@ -11,7 +11,8 @@
11 "esnext" 11 "esnext"
12 ], 12 ],
13 "types": [ 13 "types": [
14 "@types/jest" 14 "@types/jest",
15 "vite/client"
15 ] 16 ]
16 }, 17 },
17 "references": [ 18 "references": [