Created
January 31, 2024 15:32
-
-
Save alza54/23e4ff00afd2dec06119f6c1f2fe92b2 to your computer and use it in GitHub Desktop.
Electron RPC communication service snippet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { spawn, ChildProcessWithoutNullStreams } from 'child_process'; | |
import { Observable } from 'rxjs'; | |
import { ServiceTemplate, ServiceClass, ServiceMethod, ServiceBase } from './service'; | |
@ServiceClass('emulator') | |
class EmulatorService extends ServiceBase implements ServiceTemplate { | |
private process!: ChildProcessWithoutNullStreams; | |
constructor () { | |
super(); | |
} | |
@ServiceMethod('list') | |
public async listEmulators (): Promise<string[]> { | |
return new Promise((resolve, reject) => { | |
const emulator = spawn('emulator', ['-list-avds']); | |
let emulators: string[] = []; | |
emulator.stdout.on('data', (data) => { | |
emulators = data.toString().split('\n'); | |
}); | |
emulator.stderr.on('data', (data) => { | |
reject(data.toString()); | |
}); | |
emulator.on('close', () => { | |
resolve(emulators); | |
}); | |
emulator.on('error', (err) => { | |
reject(err); | |
}); | |
}); | |
} | |
@ServiceMethod('start') | |
public startEmulator (event: Electron.IpcMainInvokeEvent, emulator: string): Observable<string> { | |
return new Observable((subscriber) => { | |
this.process = spawn('emulator', [ | |
'-avd', | |
emulator, | |
'-http-proxy', | |
'http://localhost:8080', | |
'-writable-system' | |
]); | |
this.process.stdout.on('data', (data) => { | |
subscriber.next(data.toString()); | |
}); | |
this.process.stderr.on('data', (data) => { | |
subscriber.next(data.toString()); | |
}); | |
this.process.on('close', (code: number) => { | |
if (code === 0) { | |
subscriber.complete(); | |
} else { | |
subscriber.error( | |
new Error(`Emulator exited with code ${code}`) | |
); | |
} | |
}); | |
this.process.on('error', (error: Error) => { | |
subscriber.error(error); | |
}); | |
return () => { | |
this.process.kill(); | |
}; | |
}); | |
} | |
} | |
export { EmulatorService }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { IpcRendererEvent, contextBridge, ipcRenderer } from 'electron'; | |
import { Observable, defer } from 'rxjs'; | |
import { buildChannelNames } from './services'; | |
// simple example snippet | |
contextBridge.exposeInMainWorld('API', { | |
listEmulators: () => ipcRenderer.invoke('emulator:list'), | |
startEmulator: async (emulator: string) => { | |
return new Promise((resolve, reject) => { | |
ipcRenderer.invoke('emulator:start', emulator); | |
const $ = createIpcSender<string>('emulator:start'); | |
$.subscribe({ | |
next: (data) => { | |
console.log(data); | |
}, | |
error: (err) => { | |
console.error(err); | |
reject(err); | |
}, | |
complete: () => { | |
console.log('Emulator closed'); | |
resolve(undefined); | |
} | |
}); | |
}); | |
} | |
}); | |
export function createIpcSender<R>(channel: string) { | |
return defer(() => { | |
const channels = buildChannelNames(channel); | |
return new Observable<R>((observer) => { | |
const onNext = (_: IpcRendererEvent, args: R) => { | |
observer.next(args); | |
}; | |
const onError = (_: IpcRendererEvent, args: R) => { | |
removeListeners(); | |
observer.error(args); | |
}; | |
const onComplete = () => { | |
removeListeners(); | |
observer.complete(); | |
}; | |
const removeListeners = () => { | |
ipcRenderer.removeListener(channels.next, onNext); | |
ipcRenderer.removeListener(channels.error, onError); | |
ipcRenderer.removeListener(channels.complete, onComplete); | |
}; | |
ipcRenderer.on(channels.next, onNext); | |
ipcRenderer.once(channels.error, onError); | |
ipcRenderer.once(channels.complete, onComplete); | |
return removeListeners; | |
}); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-explicit-any */ | |
import Electron from 'electron'; | |
import { Observable } from 'rxjs'; | |
export interface ServiceTemplate { | |
name: string; | |
} | |
export type HandlerType<T = unknown> = (event: Electron.IpcMainInvokeEvent, ...args: any[]) => Promise<T> | Observable<T>; | |
const ExposedMethods = Symbol('ExposedMethods'); | |
export class ServiceHandler { | |
private static _instance: ServiceHandler; | |
private _services: ServiceBase[] = []; | |
private _ipcMain: Electron.IpcMain; | |
public static getInstance (): ServiceHandler { | |
if (!ServiceHandler._instance) { | |
ServiceHandler._instance = new ServiceHandler(); | |
} | |
return ServiceHandler._instance; | |
} | |
public bindIpcMain (ipcMain: Electron.IpcMain): void { | |
this._ipcMain = ipcMain; | |
} | |
public registerService (service: ServiceBase): void { | |
this._services.push(service); | |
} | |
public registerRequestHandler (requestName: string, handler: HandlerType): void { | |
this._ipcMain.handle(requestName, handler); | |
} | |
} | |
export abstract class ServiceBase implements ServiceTemplate { | |
public name: string = ''; | |
constructor () { | |
ServiceHandler.getInstance().registerService(this); | |
} | |
public executeHandler ( | |
handlerName: string, | |
handler: HandlerType, | |
event: Electron.IpcMainInvokeEvent, | |
...args: any[] | |
): Promise<void> | any { | |
const result: unknown = handler.apply(this, [event, ...args]); | |
if (!(result instanceof Observable)) { | |
return result; | |
} | |
return new Promise((resolve, reject) => { | |
const channelNames = buildChannelNames(handlerName); | |
result.subscribe({ | |
next: (data: any) => { | |
event.sender.send(channelNames.next, data); | |
}, | |
error: (error: unknown) => { | |
const serializedError = serializeError(error); | |
event.sender.send(channelNames.error, serializedError); | |
reject(serializedError); | |
}, | |
complete: () => { | |
event.sender.send(channelNames.complete); | |
resolve(undefined); | |
} | |
}); | |
}); | |
} | |
} | |
export function ServiceClass< | |
T extends { new(...args: any[]): ServiceBase } | |
>(name: string) { | |
return function (Base: T) { | |
return class extends Base { | |
constructor(...args: any[]) { | |
super(...args); | |
this.name = name; | |
const exposedMethods = Base.prototype[ExposedMethods]; | |
if (exposedMethods) { | |
exposedMethods.forEach((handlerName: string, methodName: string) => { | |
ServiceHandler.getInstance().registerRequestHandler( | |
`${name}:${handlerName}`, | |
this.executeHandler.bind( | |
this, | |
`${name}:${handlerName}`, | |
(this as any)[methodName].bind(this) | |
) | |
); | |
}); | |
} | |
} | |
}; | |
}; | |
} | |
export function buildChannelNames(channel: string) { | |
return { | |
source: channel, | |
next: `${channel}/next`, | |
error: `${channel}/error`, | |
complete: `${channel}/complete`, | |
}; | |
} | |
export function serializeError (error: unknown): Record<string, string> { | |
if (error instanceof Error) { | |
return { | |
name: error.name, | |
message: error.message, | |
stack: error.stack, | |
}; | |
} | |
return { | |
message: String(error), | |
}; | |
} | |
export function ServiceMethod(handlerName: string) { | |
return function (target: any, propertyName: string) { | |
target[ExposedMethods] = target[ExposedMethods] || new Map(); | |
target[ExposedMethods].set(propertyName, handlerName); | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment