@noxfly/noxus
Version:
Simulate lightweight HTTP-like requests between renderer and main process in Electron applications with MessagePort, with structured and modular design.
192 lines (160 loc) • 6.39 kB
text/typescript
/**
* @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 { IRequest, IResponse, Request } from "src/request";
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(): Promise<void>;
onActivated(): Promise<void>;
}
/**
* NoxApp is the main application class that manages the application lifecycle,
* handles IPC communication, and integrates with the Router.
*/
export class NoxApp {
private readonly messagePorts = new Map<number, Electron.MessageChannelMain>();
private app: IApp | undefined;
constructor(
private readonly router: Router,
) {}
/**
* 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.messagePorts.has(senderId)) {
this.shutdownChannel(senderId);
}
const channel = new MessageChannelMain();
this.messagePorts.set(senderId, channel);
channel.port1.on('message', this.onRendererMessage.bind(this));
channel.port1.start();
event.sender.postMessage('port', { senderId }, [channel.port2]);
}
/**
* Electron specific message handling.
* Replaces HTTP calls by using Electron's IPC mechanism.
*/
private async onRendererMessage(event: Electron.MessageEvent): Promise<void> {
const { senderId, requestId, path, method, body }: IRequest = event.data;
const channel = this.messagePorts.get(senderId);
if(!channel) {
Logger.error(`No message channel found for sender ID: ${senderId}`);
return;
}
try {
const request = new Request(event, requestId, method, path, body);
const response = await this.router.handle(request);
channel.port1.postMessage(response);
}
catch(err: any) {
const response: IResponse = {
requestId,
status: 500,
body: null,
error: err.message || 'Internal Server Error',
};
channel.port1.postMessage(response);
}
}
/**
* 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 channel = this.messagePorts.get(channelSenderId);
if(!channel) {
Logger.warn(`No message channel found for sender ID: ${channelSenderId}`);
return;
}
channel.port1.off('message', this.onRendererMessage.bind(this));
channel.port1.close();
channel.port2.close();
this.messagePorts.delete(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> {
this.messagePorts.forEach((channel, senderId) => {
this.shutdownChannel(senderId);
});
this.messagePorts.clear();
Logger.info('All windows closed, shutting down application...');
await this.app?.dispose();
if(process.platform !== 'darwin') {
app.quit();
}
}
// ---
/**
* 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.
* @returns NoxApp instance for method chaining.
*/
public start(): NoxApp {
this.app?.onReady();
return this;
}
}