UNPKG

@noxfly/noxus

Version:

Simulate lightweight HTTP-like requests between renderer and main process in Electron applications with MessagePort, with structured and modular design.

245 lines (208 loc) 8.67 kB
/** * @copyright 2025 NoxFly * @license MIT * @author NoxFly */ import { app, BrowserWindow, ipcMain, MessageChannelMain } from "electron/main"; import { Injectable } from "src/decorators/injectable.decorator"; import { IMiddleware } from "src/decorators/middleware.decorator"; import { inject } from "src/DI/app-injector"; import { InjectorExplorer } from "src/DI/injector-explorer"; import { IRequest, IResponse, Request } from "src/request"; import { NoxSocket } from "src/socket"; import { Router } from "src/router"; import { Logger } from "src/utils/logger"; import { Type } from "src/utils/types"; /** * The application service should implement this interface, as * the NoxApp class instance will use it to notify the given service * about application lifecycle events. */ export interface IApp { dispose(): Promise<void>; onReady(mainWindow?: BrowserWindow): Promise<void>; onActivated(): Promise<void>; } /** * NoxApp is the main application class that manages the application lifecycle, * handles IPC communication, and integrates with the Router. */ @Injectable('singleton') export class NoxApp { private app: IApp | undefined; private mainWindow: BrowserWindow | undefined; /** * */ private readonly onRendererMessage = async (event: Electron.MessageEvent): Promise<void> => { const { senderId, requestId, path, method, body }: IRequest = event.data; const channels = this.socket.get(senderId); if(!channels) { Logger.error(`No message channel found for sender ID: ${senderId}`); return; } try { const request = new Request(event, senderId, requestId, method, path, body); const response = await this.router.handle(request); channels.request.port1.postMessage(response); } catch(err: any) { const response: IResponse = { requestId, status: 500, body: null, error: err.message || 'Internal Server Error', }; channels.request.port1.postMessage(response); } }; constructor( private readonly router: Router, private readonly socket: NoxSocket, ) {} /** * Initializes the NoxApp instance. * This method sets up the IPC communication, registers event listeners, * and prepares the application for use. */ public async init(): Promise<NoxApp> { ipcMain.on('gimme-my-port', this.giveTheRendererAPort.bind(this)); app.once('activate', this.onAppActivated.bind(this)); app.once('window-all-closed', this.onAllWindowsClosed.bind(this)); console.log(''); // create a new line in the console to separate setup logs from the future logs return this; } /** * Handles the request from the renderer process. * This method creates a Request object from the IPC event data, * processes it through the Router, and sends the response back * to the renderer process using the MessageChannel. */ private giveTheRendererAPort(event: Electron.IpcMainInvokeEvent): void { const senderId = event.sender.id; if(this.socket.get(senderId)) { this.shutdownChannel(senderId); } const requestChannel = new MessageChannelMain(); const socketChannel = new MessageChannelMain(); requestChannel.port1.on('message', this.onRendererMessage); requestChannel.port1.start(); socketChannel.port1.start(); this.socket.register(senderId, requestChannel, socketChannel); event.sender.postMessage('port', { senderId }, [requestChannel.port2, socketChannel.port2]); } /** * MacOS specific behavior. */ private onAppActivated(): void { if(process.platform === 'darwin' && BrowserWindow.getAllWindows().length === 0) { this.app?.onActivated(); } } /** * Shuts down the message channel for a specific sender ID. * This method closes the IPC channel for the specified sender ID and * removes it from the messagePorts map. * @param channelSenderId - The ID of the sender channel to shut down. * @param remove - Whether to remove the channel from the messagePorts map. */ private shutdownChannel(channelSenderId: number): void { const channels = this.socket.get(channelSenderId); if(!channels) { Logger.warn(`No message channel found for sender ID: ${channelSenderId}`); return; } channels.request.port1.off('message', this.onRendererMessage); channels.request.port1.close(); channels.request.port2.close(); channels.socket.port1.close(); channels.socket.port2.close(); this.socket.unregister(channelSenderId); } /** * Handles the application shutdown process. * This method is called when all windows are closed, and it cleans up the message channels */ private async onAllWindowsClosed(): Promise<void> { for(const senderId of this.socket.getSenderIds()) { this.shutdownChannel(senderId); } Logger.info('All windows closed, shutting down application...'); await this.app?.dispose(); if(process.platform !== 'darwin') { app.quit(); } } // --- /** * Sets the main BrowserWindow that was created early by bootstrapApplication. * This window will be passed to IApp.onReady when start() is called. * @param window - The BrowserWindow created during bootstrap. */ public setMainWindow(window: BrowserWindow): void { this.mainWindow = window; } /** * Registers a lazy-loaded route. The module behind this path prefix * will only be dynamically imported when the first IPC request * targets this prefix — like Angular's loadChildren. * * @example * ```ts * noxApp.lazy("auth", () => import("./modules/auth/auth.module.js")); * noxApp.lazy("printing", () => import("./modules/printing/printing.module.js")); * ``` * * @param pathPrefix - The route prefix (e.g. "auth", "cash-register"). * @param loadModule - A function returning a dynamic import promise. * @returns NoxApp instance for method chaining. */ public lazy(pathPrefix: string, loadModule: () => Promise<unknown>): NoxApp { this.router.registerLazyRoute(pathPrefix, loadModule); return this; } /** * Eagerly loads one or more modules with a two-phase DI guarantee. * Use this when a service needed at startup lives inside a module * (e.g. the Application service depends on LoaderService). * * All dynamic imports run in parallel; bindings are registered first, * then singletons are resolved — safe regardless of import ordering. * * @param importFns - Functions returning dynamic import promises. */ public async loadModules(importFns: Array<() => Promise<unknown>>): Promise<void> { InjectorExplorer.beginAccumulate(); await Promise.all(importFns.map(fn => fn())); InjectorExplorer.flushAccumulated(); } /** * Configures the NoxApp instance with the provided application class. * This method allows you to set the application class that will handle lifecycle events. * @param app - The application class to configure. * @returns NoxApp instance for method chaining. */ public configure(app: Type<IApp>): NoxApp { this.app = inject(app); return this; } /** * Registers a middleware for the root of the application. * This method allows you to define a middleware that will be applied to all requests * @param middleware - The middleware class to register. * @returns NoxApp instance for method chaining. */ public use(middleware: Type<IMiddleware>): NoxApp { this.router.defineRootMiddleware(middleware); return this; } /** * Should be called after the bootstrapApplication function is called. * Passes the early-created BrowserWindow (if any) to the configured IApp service. * @returns NoxApp instance for method chaining. */ public start(): NoxApp { this.app?.onReady(this.mainWindow); return this; } }