Conversation
|
Hi @HoseinErfani, thanks for the effort on this refactor! After reviewing, we won't be merging this PR. Here's why:
If you're interested in contributing an architectural improvement, feel free to open an issue to discuss the direction first - we'd be happy to collaborate that way. Thanks again! |
There was a problem hiding this comment.
Pull request overview
This pull request restructures an Angular-Electron application to follow an Angular-like architecture with class-based components, decorators, and dependency injection using InversifyJS. The main goal is to modernize the Electron main process code to mirror Angular's programming patterns.
Changes:
- Introduced decorator-based event handling for Tray, Window, and IPC events using custom decorators (@TrayListener, @windowlistener, @IpcListener)
- Implemented dependency injection in the Electron main process using InversifyJS with
@injectableand@injectdecorators - Refactored the main.ts entry point to use a class-based architecture with a MainWindow class extending base classes for window management
Reviewed changes
Copilot reviewed 39 out of 43 changed files in this pull request and generated 25 comments.
Show a summary per file
| File | Description |
|---|---|
| src/assets/icons/electron.svg | Added SVG icon for Electron tray |
| src/assets/icons/electron.png | Added PNG icon for Electron tray |
| src/app/core/services/electron/electron.service.ts | Added updateTrayText method and ElectronWindow type, reformatted imports |
| src/app/core/services/electron/electron.service.spec.ts | Deleted test file |
| src/app/app.component.ts | Removed TranslateService, added interval-based tray text update demo |
| src/app/app.component.html | Commented out router-outlet, added counter display |
| shared/types/ipc-channel.type.ts | Added type definition for IPC channel payloads |
| shared/enums/ipc-channel.enum.ts | Added enum for IPC channel names |
| package.json | Added inversify and reflect-metadata dependencies, removed -o flag from ng:serve |
| package-lock.json | Updated lockfile with new dependencies |
| app/windows/main.window.ts | Created MainWindow class with decorator-based event handling |
| app/utils/services/file.service.ts | Created FileService for path resolution |
| app/utils/interfaces/on-app-ready.interface.ts | Created OnAppReady lifecycle interface |
| app/utils/hooks/provide.hook.ts | Created useProvide hook for DI container setup |
| app/utils/decorators/*.ts | Created decorators for tray, window, and IPC event listening |
| app/utils/base-classes/*.ts | Created base classes for window management with DI |
| app/utils/enums/*.ts | Created enums for event names |
| app/utils/types/*.ts | Created type definitions for event parameters |
| app/main.ts | Refactored to use new architecture with DI and MainWindow class |
| README.md | Completely rewritten to document new decorator-based architecture |
| LICENSE.md | Deleted MIT license |
| .editorconfig | Changed indent_size from 2 to 4 spaces |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <!--<router-outlet></router-outlet>--> | ||
|
|
||
| {{s}} |
There was a problem hiding this comment.
The router-outlet is commented out but not removed, and the component now displays a simple counter. This appears to be temporary debug/demo code that should either be properly implemented or removed before merging to ensure the application's routing functionality is preserved.
| updateTrayText(text: IPCChannelType[IPCChannelEnum.UPDATE_TRAY_TEXT]) { | ||
| this.ipcRenderer.send(IPCChannelEnum.UPDATE_TRAY_TEXT, text); | ||
| } |
There was a problem hiding this comment.
The method 'updateTrayText' uses camelCase, which is consistent with TypeScript conventions. However, the parameter 'text' is typed as a complex indexed type. Consider adding JSDoc documentation to clarify the purpose and expected values for this method, especially since it's a public API of the service.
| if (!target.__trayEvents) { | ||
| target.__trayEvents = []; | ||
| } | ||
|
|
||
| const eventKey = `${eventName}_${propertyKey}`; | ||
|
|
||
| // Prevent duplicate event binding | ||
| if (!target.__trayEvents.includes(eventKey)) { | ||
| target.__trayEvents.push(eventKey); | ||
|
|
||
| const originalOnAppReady = target.onAppReady; | ||
|
|
||
| target.onAppReady = function (...args: any[]) { | ||
| if (typeof originalOnAppReady === 'function') { | ||
| originalOnAppReady.apply(this, args); | ||
| } | ||
|
|
||
| const tray = this.tray as Tray; | ||
|
|
||
| if (tray) { | ||
| tray.on(eventName as any, (...eventArgs: any[]) => { | ||
| // Ensure the method signature matches the event's arguments | ||
| if (typeof this[propertyKey] === 'function') { | ||
| const expectedArgs = eventArgs as TrayEventType[T]; | ||
| this[propertyKey](expectedArgs); |
There was a problem hiding this comment.
The decorator modifies target.onAppReady by wrapping it, but this happens multiple times when multiple decorators are applied to different methods in the same class. Each decorator call wraps the previous onAppReady, creating a chain of wrapper functions. This could lead to the original onAppReady being called multiple times. Consider a different approach like storing listeners in a metadata array and registering them once in a single onAppReady wrapper.
| if (!target.__trayEvents) { | |
| target.__trayEvents = []; | |
| } | |
| const eventKey = `${eventName}_${propertyKey}`; | |
| // Prevent duplicate event binding | |
| if (!target.__trayEvents.includes(eventKey)) { | |
| target.__trayEvents.push(eventKey); | |
| const originalOnAppReady = target.onAppReady; | |
| target.onAppReady = function (...args: any[]) { | |
| if (typeof originalOnAppReady === 'function') { | |
| originalOnAppReady.apply(this, args); | |
| } | |
| const tray = this.tray as Tray; | |
| if (tray) { | |
| tray.on(eventName as any, (...eventArgs: any[]) => { | |
| // Ensure the method signature matches the event's arguments | |
| if (typeof this[propertyKey] === 'function') { | |
| const expectedArgs = eventArgs as TrayEventType[T]; | |
| this[propertyKey](expectedArgs); | |
| // Store metadata about tray listeners on the prototype | |
| if (!target.__trayListenersMetadata) { | |
| target.__trayListenersMetadata = []; | |
| } | |
| const listeners: Array<{ eventName: TrayEventEnum; propertyKey: string }> = | |
| target.__trayListenersMetadata; | |
| // Prevent duplicate metadata entries for the same event/method pair | |
| const alreadyRegistered = listeners.some( | |
| (listener) => listener.eventName === eventName && listener.propertyKey === propertyKey, | |
| ); | |
| if (!alreadyRegistered) { | |
| listeners.push({ eventName, propertyKey }); | |
| } | |
| // Wrap onAppReady only once per class/prototype | |
| if (!target.__trayOnAppReadyWrapped) { | |
| target.__trayOnAppReadyWrapped = true; | |
| const originalOnAppReady = target.onAppReady; | |
| target.onAppReady = function (...args: any[]) { | |
| // Call the original onAppReady implementation once | |
| if (typeof originalOnAppReady === 'function') { | |
| originalOnAppReady.apply(this, args); | |
| } | |
| const tray = this.tray as Tray; | |
| if (!tray) { | |
| return; | |
| } | |
| const meta: Array<{ eventName: TrayEventEnum; propertyKey: string }> = | |
| (this as any).__trayListenersMetadata; | |
| if (!Array.isArray(meta)) { | |
| return; | |
| } | |
| for (const { eventName, propertyKey } of meta) { | |
| tray.on(eventName as any, (...eventArgs: any[]) => { | |
| // Ensure the method signature matches the event's arguments | |
| const handler = (this as any)[propertyKey]; | |
| if (typeof handler === 'function') { | |
| const expectedArgs = eventArgs as TrayEventType[typeof eventName]; | |
| handler(expectedArgs); |
| constructor( | ||
| private electronService: ElectronService, | ||
| ) { |
There was a problem hiding this comment.
The TranslateService import and usage has been removed from AppComponent, but this appears to break existing functionality. The test file (app.component.spec.ts) still imports TranslateModule, suggesting that translation was a core feature. This is a breaking change that should be addressed.
| show: false, | ||
| webPreferences: { | ||
| nodeIntegration: true, | ||
| allowRunningInsecureContent: true, |
There was a problem hiding this comment.
Setting 'allowRunningInsecureContent: true' allows the renderer to load insecure (HTTP) content even when the page is loaded via HTTPS. This is a security vulnerability that could expose users to man-in-the-middle attacks. This setting should be removed or set to false.
| target.onAppReady = function (...args: any[]) { | ||
| if (typeof originalOnAppReady === 'function') { | ||
| originalOnAppReady.apply(this, args); | ||
| } | ||
|
|
||
| // Listen to the IPC event | ||
| ipcMain.on(channel, (event: IpcMainEvent, ...eventArgs: any[]) => { | ||
| if (typeof this[propertyKey] === 'function') { | ||
| this[propertyKey](event, ...eventArgs); | ||
| } | ||
| }); | ||
| }; |
There was a problem hiding this comment.
The IpcListener decorator registers event listeners on ipcMain but never removes them. If the MainWindow instance is destroyed and recreated, duplicate event listeners will accumulate, causing memory leaks and potential double-execution of handlers. The decorator should track listeners and provide cleanup when the instance is destroyed.
| target.onAppReady = function (...args: any[]) { | |
| if (typeof originalOnAppReady === 'function') { | |
| originalOnAppReady.apply(this, args); | |
| } | |
| // Listen to the IPC event | |
| ipcMain.on(channel, (event: IpcMainEvent, ...eventArgs: any[]) => { | |
| if (typeof this[propertyKey] === 'function') { | |
| this[propertyKey](event, ...eventArgs); | |
| } | |
| }); | |
| }; | |
| // Wrap onAppReady to register the IPC listener when the app is ready | |
| target.onAppReady = function (...args: any[]) { | |
| if (typeof originalOnAppReady === 'function') { | |
| originalOnAppReady.apply(this, args); | |
| } | |
| // Ensure a per-instance storage for IPC listeners | |
| if (!this.__ipcListeners) { | |
| this.__ipcListeners = []; | |
| } | |
| // Listener function that will be registered with ipcMain | |
| const listener = (event: IpcMainEvent, ...eventArgs: any[]) => { | |
| if (typeof this[propertyKey] === 'function') { | |
| this[propertyKey](event, ...eventArgs); | |
| } | |
| }; | |
| // Listen to the IPC event and keep track of the listener for cleanup | |
| ipcMain.on(channel, listener); | |
| this.__ipcListeners.push({channel, listener}); | |
| }; | |
| // Register a one-time cleanup hook to remove all IPC listeners on destroy | |
| if (!target.__ipcCleanupRegistered) { | |
| const originalOnDestroy = target.onDestroy; | |
| target.onDestroy = function (...destroyArgs: any[]) { | |
| if (Array.isArray(this.__ipcListeners)) { | |
| this.__ipcListeners.forEach((entry: any) => { | |
| if (entry && entry.channel && entry.listener) { | |
| ipcMain.removeListener(entry.channel, entry.listener); | |
| } | |
| }); | |
| this.__ipcListeners = []; | |
| } | |
| if (typeof originalOnDestroy === 'function') { | |
| originalOnDestroy.apply(this, destroyArgs); | |
| } | |
| }; | |
| target.__ipcCleanupRegistered = true; | |
| } |
| window.on(eventName as any, (...eventArgs: any[]) => { | ||
| // Ensure the method signature matches the event's arguments | ||
| if (typeof this[propertyKey] === 'function') { | ||
| const expectedArgs = eventArgs as WindowEventType[T]; | ||
| this[propertyKey](expectedArgs); | ||
| } |
There was a problem hiding this comment.
The WindowListener decorator registers event listeners without cleanup, which can cause memory leaks. When a window is created and destroyed multiple times, old event listeners will persist in memory. Consider implementing proper cleanup when the window is destroyed.
| window.on(eventName as any, (...eventArgs: any[]) => { | |
| // Ensure the method signature matches the event's arguments | |
| if (typeof this[propertyKey] === 'function') { | |
| const expectedArgs = eventArgs as WindowEventType[T]; | |
| this[propertyKey](expectedArgs); | |
| } | |
| const listener = (...eventArgs: any[]) => { | |
| // Ensure the method signature matches the event's arguments | |
| if (typeof this[propertyKey] === 'function') { | |
| const expectedArgs = eventArgs as WindowEventType[T]; | |
| this[propertyKey](expectedArgs); | |
| } | |
| }; | |
| window.on(eventName as any, listener); | |
| // Clean up the listener when the window is closed to avoid memory leaks | |
| window.on('closed', () => { | |
| window.removeListener(eventName as any, listener); |
| private _window$: ReplaySubject<BrowserWindow> = new ReplaySubject<BrowserWindow>(); | ||
|
|
||
| constructor() { | ||
| app.whenReady().then(() => this.onAppReady()); |
There was a problem hiding this comment.
The constructor calls app.whenReady() which sets up the ready handler, but if the app is already ready when this constructor is called, the onAppReady() method will still be called. However, there's a race condition: decorators modify onAppReady on the prototype, but if the instance is created after the app is ready, the decorated version might be called before all decorators have been applied. Consider using a different initialization pattern.
| app.whenReady().then(() => this.onAppReady()); | |
| if (this.app.isReady()) { | |
| this.onAppReady(); | |
| } else { | |
| this.app.once('ready', () => this.onAppReady()); | |
| } |
| interval(1000).pipe( | ||
| map((_: number) => { | ||
| this.s++; | ||
| return this.s.toString(); | ||
| }), | ||
| tap(time => { | ||
| this.electronService.updateTrayText(time); | ||
| }) | ||
| ).subscribe() |
There was a problem hiding this comment.
The subscription created by the interval observable is never unsubscribed. This will cause a memory leak as the interval will continue to run even after the component is destroyed. The subscription should be stored and unsubscribed in the ngOnDestroy lifecycle hook.
| } catch (e) { | ||
| // Catch Error | ||
| // throw e; | ||
|
|
||
| } |
There was a problem hiding this comment.
The empty catch block silently swallows all errors, making debugging impossible. At minimum, the error should be logged to the console, or better yet, implement proper error handling and reporting.
No description provided.