mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-13 12:38:39 -09:00
strongly typed rpc
This commit is contained in:
parent
e31d72cc31
commit
2b644e1fbb
8 changed files with 169 additions and 40 deletions
33
app/rpc.ts
33
app/rpc.ts
|
|
@ -1,15 +1,18 @@
|
|||
import {EventEmitter} from 'events';
|
||||
import {ipcMain, BrowserWindow} from 'electron';
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import {TypedEmitter, MainEvents, RendererEvents, FilterNever} from '../common';
|
||||
|
||||
export class Server extends EventEmitter {
|
||||
export class Server {
|
||||
emitter: TypedEmitter<MainEvents>;
|
||||
destroyed = false;
|
||||
win: BrowserWindow;
|
||||
id!: string;
|
||||
|
||||
constructor(win: BrowserWindow) {
|
||||
super();
|
||||
this.emitter = new EventEmitter();
|
||||
this.win = win;
|
||||
this.ipcListener = this.ipcListener.bind(this);
|
||||
this.emit = this.emit.bind(this);
|
||||
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
|
|
@ -18,7 +21,6 @@ export class Server extends EventEmitter {
|
|||
const uid = uuidv4();
|
||||
this.id = uid;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
ipcMain.on(uid, this.ipcListener);
|
||||
|
||||
// we intentionally subscribe to `on` instead of `once`
|
||||
|
|
@ -33,23 +35,34 @@ export class Server extends EventEmitter {
|
|||
return this.win.webContents;
|
||||
}
|
||||
|
||||
ipcListener(event: any, {ev, data}: {ev: string; data: any}) {
|
||||
super.emit(ev, data);
|
||||
}
|
||||
ipcListener = (event: any, {ev, data}: {ev: keyof MainEvents; data: any}) => this.emitter.emit(ev, data);
|
||||
|
||||
emit(ch: string, data: any = {}): any {
|
||||
on = <U extends keyof MainEvents>(ev: U, fn: (arg0: MainEvents[U]) => void) => {
|
||||
this.emitter.on(ev, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
once = <U extends keyof MainEvents>(ev: U, fn: (arg0: MainEvents[U]) => void) => {
|
||||
this.emitter.once(ev, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
emit<U extends Exclude<keyof RendererEvents, FilterNever<RendererEvents>>>(ch: U): boolean;
|
||||
emit<U extends FilterNever<RendererEvents>>(ch: U, data: RendererEvents[U]): boolean;
|
||||
emit<U extends keyof RendererEvents>(ch: U, data?: RendererEvents[U]) {
|
||||
// This check is needed because data-batching can cause extra data to be
|
||||
// emitted after the window has already closed
|
||||
if (!this.win.isDestroyed()) {
|
||||
this.wc.send(this.id, {ch, data});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.removeAllListeners();
|
||||
this.emitter.removeAllListeners();
|
||||
this.wc.removeAllListeners();
|
||||
if (this.id) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
ipcMain.removeListener(this.id, this.ipcListener);
|
||||
} else {
|
||||
// mark for `genUid` in constructor
|
||||
|
|
|
|||
|
|
@ -206,8 +206,8 @@ export function newWindow(
|
|||
session.resize({cols, rows});
|
||||
}
|
||||
});
|
||||
rpc.on('data', ({uid, data, escaped}: {uid: string; data: string; escaped: boolean}) => {
|
||||
const session = sessions.get(uid);
|
||||
rpc.on('data', ({uid, data, escaped}) => {
|
||||
const session = uid && sessions.get(uid);
|
||||
if (session) {
|
||||
if (escaped) {
|
||||
const escapedData = session.shell?.endsWith('cmd.exe')
|
||||
|
|
@ -255,10 +255,10 @@ export function newWindow(
|
|||
});
|
||||
// pass on the full screen events from the window to react
|
||||
rpc.win.on('enter-full-screen', () => {
|
||||
rpc.emit('enter full screen', {});
|
||||
rpc.emit('enter full screen');
|
||||
});
|
||||
rpc.win.on('leave-full-screen', () => {
|
||||
rpc.emit('leave full screen', {});
|
||||
rpc.emit('leave full screen');
|
||||
});
|
||||
const deleteSessions = () => {
|
||||
sessions.forEach((session, key) => {
|
||||
|
|
@ -280,9 +280,9 @@ export function newWindow(
|
|||
const protocol = typeof url === 'string' && new URL(url).protocol;
|
||||
if (protocol === 'file:') {
|
||||
const path = fileURLToPath(url);
|
||||
return {data: path, escaped: true};
|
||||
return {uid: null, data: path, escaped: true};
|
||||
} else if (protocol === 'http:' || protocol === 'https:') {
|
||||
return {data: url};
|
||||
return {uid: null, data: url};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
96
common.d.ts
vendored
Normal file
96
common.d.ts
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import type parseUrl from 'parse-url';
|
||||
|
||||
export type Session = {
|
||||
uid: string;
|
||||
rows: number | null;
|
||||
cols: number | null;
|
||||
splitDirection?: 'HORIZONTAL' | 'VERTICAL';
|
||||
shell: string | null;
|
||||
pid: number | null;
|
||||
activeUid?: string;
|
||||
};
|
||||
|
||||
export type sessionExtraOptions = {
|
||||
cwd: string | undefined;
|
||||
splitDirection?: 'HORIZONTAL' | 'VERTICAL';
|
||||
activeUid?: string | null;
|
||||
isNewGroup?: boolean;
|
||||
};
|
||||
|
||||
export type MainEvents = {
|
||||
close: never;
|
||||
command: string;
|
||||
data: {uid: string | null; data: string; escaped?: boolean | null};
|
||||
exit: {uid: string};
|
||||
'info renderer': {uid: string; type: string};
|
||||
init: null;
|
||||
maximize: never;
|
||||
minimize: never;
|
||||
new: sessionExtraOptions;
|
||||
'open context menu': string;
|
||||
'open external': {url: string};
|
||||
'open hamburger menu': {x: number; y: number};
|
||||
'quit and install': never;
|
||||
resize: {uid: string; cols: number; rows: number};
|
||||
unmaximize: never;
|
||||
};
|
||||
|
||||
export type RendererEvents = {
|
||||
ready: never;
|
||||
'add notification': {text: string; url: string; dismissable: boolean};
|
||||
'update available': {releaseNotes: string; releaseName: string; releaseUrl: string; canInstall: boolean};
|
||||
'open ssh': ReturnType<typeof parseUrl>;
|
||||
'open file': {path: string};
|
||||
'move jump req': number | 'last';
|
||||
'reset fontSize req': never;
|
||||
'move left req': never;
|
||||
'move right req': never;
|
||||
'prev pane req': never;
|
||||
'decrease fontSize req': never;
|
||||
'increase fontSize req': never;
|
||||
'next pane req': never;
|
||||
'session break req': never;
|
||||
'session quit req': never;
|
||||
'session search close': never;
|
||||
'session search': never;
|
||||
'session stop req': never;
|
||||
'session tmux req': never;
|
||||
'session del line beginning req': never;
|
||||
'session del line end req': never;
|
||||
'session del word left req': never;
|
||||
'session del word right req': never;
|
||||
'session move line beginning req': never;
|
||||
'session move line end req': never;
|
||||
'session move word left req': never;
|
||||
'session move word right req': never;
|
||||
'term selectAll': never;
|
||||
reload: never;
|
||||
'session clear req': never;
|
||||
'split request horizontal': {activeUid?: string};
|
||||
'split request vertical': {activeUid?: string};
|
||||
'termgroup add req': {activeUid?: string};
|
||||
'termgroup close req': never;
|
||||
'session add': Session;
|
||||
'session data': string;
|
||||
'session exit': {uid: string};
|
||||
'windowGeometry change': {isMaximized: boolean};
|
||||
move: {bounds: {x: number; y: number}};
|
||||
'enter full screen': never;
|
||||
'leave full screen': never;
|
||||
'session data send': {uid: string | null; data: string; escaped?: boolean};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get keys of T where the value is not never
|
||||
*/
|
||||
export type FilterNever<T> = {[K in keyof T]: T[K] extends never ? never : K}[keyof T];
|
||||
|
||||
export interface TypedEmitter<Events> {
|
||||
on<E extends keyof Events>(event: E, listener: (args: Events[E]) => void): this;
|
||||
once<E extends keyof Events>(event: E, listener: (args: Events[E]) => void): this;
|
||||
emit<E extends Exclude<keyof Events, FilterNever<Events>>>(event: E): boolean;
|
||||
emit<E extends FilterNever<Events>>(event: E, data: Events[E]): boolean;
|
||||
emit<E extends keyof Events>(event: E, data?: Events[E]): boolean;
|
||||
removeListener<E extends keyof Events>(event: E, listener: (args: Events[E]) => void): this;
|
||||
removeAllListeners<E extends keyof Events>(event?: E): this;
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ export function maximize() {
|
|||
dispatch({
|
||||
type: UI_WINDOW_MAXIMIZE,
|
||||
effect() {
|
||||
rpc.emit('maximize', null);
|
||||
rpc.emit('maximize');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -50,7 +50,7 @@ export function unmaximize() {
|
|||
dispatch({
|
||||
type: UI_WINDOW_UNMAXIMIZE,
|
||||
effect() {
|
||||
rpc.emit('unmaximize', null);
|
||||
rpc.emit('unmaximize');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -72,7 +72,7 @@ export function minimize() {
|
|||
dispatch({
|
||||
type: UI_WINDOW_MINIMIZE,
|
||||
effect() {
|
||||
rpc.emit('minimize', null);
|
||||
rpc.emit('minimize');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -83,7 +83,7 @@ export function close() {
|
|||
dispatch({
|
||||
type: UI_WINDOW_CLOSE,
|
||||
effect() {
|
||||
rpc.emit('close', null);
|
||||
rpc.emit('close');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,15 @@ import {
|
|||
} from '../constants/sessions';
|
||||
import {HyperState, session, HyperDispatch, HyperActions} from '../hyper';
|
||||
|
||||
export function addSession({uid, shell, pid, cols, rows, splitDirection, activeUid}: session) {
|
||||
export function addSession({
|
||||
uid,
|
||||
shell,
|
||||
pid,
|
||||
cols,
|
||||
rows,
|
||||
splitDirection,
|
||||
activeUid
|
||||
}: Pick<session, 'uid' | 'shell' | 'pid' | 'cols' | 'rows' | 'splitDirection' | 'activeUid'>) {
|
||||
return (dispatch: HyperDispatch, getState: () => HyperState) => {
|
||||
const {sessions} = getState();
|
||||
const now = Date.now();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {setActiveSession, ptyExitSession, userExitSession} from './sessions';
|
|||
import {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../hyper';
|
||||
|
||||
function requestSplit(direction: 'VERTICAL' | 'HORIZONTAL') {
|
||||
return (activeUid: string) =>
|
||||
return (activeUid: string | undefined) =>
|
||||
(dispatch: HyperDispatch, getState: () => HyperState): void => {
|
||||
dispatch({
|
||||
type: SESSION_REQUEST,
|
||||
|
|
@ -40,7 +40,7 @@ export function resizeTermGroup(uid: string, sizes: number[]): HyperActions {
|
|||
};
|
||||
}
|
||||
|
||||
export function requestTermGroup(activeUid: string) {
|
||||
export function requestTermGroup(activeUid: string | undefined) {
|
||||
return (dispatch: HyperDispatch, getState: () => HyperState) => {
|
||||
dispatch({
|
||||
type: TERM_GROUP_REQUEST,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export function installUpdate(): HyperActions {
|
|||
return {
|
||||
type: UPDATE_INSTALL,
|
||||
effect: () => {
|
||||
rpc.emit('quit and install', null);
|
||||
rpc.emit('quit and install');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import {EventEmitter} from 'events';
|
||||
import {IpcRenderer, IpcRendererEvent} from 'electron';
|
||||
import electron from 'electron';
|
||||
import {FilterNever, MainEvents, RendererEvents, TypedEmitter} from '../../common';
|
||||
|
||||
export default class Client {
|
||||
emitter: EventEmitter;
|
||||
emitter: TypedEmitter<RendererEvents>;
|
||||
ipc: IpcRenderer;
|
||||
id!: string;
|
||||
|
||||
constructor() {
|
||||
this.emitter = new EventEmitter();
|
||||
this.ipc = electron.ipcRenderer;
|
||||
this.emit = this.emit.bind(this);
|
||||
if (window.__rpcId) {
|
||||
setTimeout(() => {
|
||||
this.id = window.__rpcId;
|
||||
|
|
@ -27,35 +31,43 @@ export default class Client {
|
|||
}
|
||||
}
|
||||
|
||||
ipcListener = (event: any, {ch, data}: {ch: string; data: any}) => {
|
||||
this.emitter.emit(ch, data);
|
||||
ipcListener = <U extends keyof RendererEvents>(
|
||||
event: IpcRendererEvent,
|
||||
{ch, data}: {ch: U; data: RendererEvents[U]}
|
||||
) => this.emitter.emit(ch, data);
|
||||
|
||||
on = <U extends keyof RendererEvents>(ev: U, fn: (arg0: RendererEvents[U]) => void) => {
|
||||
this.emitter.on(ev, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
on(ev: string, fn: (...args: any[]) => void) {
|
||||
this.emitter.on(ev, fn);
|
||||
}
|
||||
|
||||
once(ev: string, fn: (...args: any[]) => void) {
|
||||
once = <U extends keyof RendererEvents>(ev: U, fn: (arg0: RendererEvents[U]) => void) => {
|
||||
this.emitter.once(ev, fn);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
emit(ev: string, data: any) {
|
||||
emit<U extends Exclude<keyof MainEvents, FilterNever<MainEvents>>>(ch: U): boolean;
|
||||
emit<U extends FilterNever<MainEvents>>(ch: U, data: MainEvents[U]): boolean;
|
||||
emit<U extends keyof MainEvents>(ev: U, data?: MainEvents[U]) {
|
||||
if (!this.id) {
|
||||
throw new Error('Not ready');
|
||||
}
|
||||
this.ipc.send(this.id, {ev, data});
|
||||
return true;
|
||||
}
|
||||
|
||||
removeListener(ev: string, fn: (...args: any[]) => void) {
|
||||
removeListener = <U extends keyof RendererEvents>(ev: U, fn: (arg0: RendererEvents[U]) => void) => {
|
||||
this.emitter.removeListener(ev, fn);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
removeAllListeners() {
|
||||
removeAllListeners = () => {
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
destroy() {
|
||||
destroy = () => {
|
||||
this.removeAllListeners();
|
||||
this.ipc.removeAllListeners(this.id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue