Skip to content

Commit

Permalink
improve type-safety and enable optional custom logger for client
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed May 19, 2019
1 parent ad387d4 commit dcb9748
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 222 deletions.
2 changes: 1 addition & 1 deletion example/electron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"start:web": "cross-env NODE_ENV=development webpack-dev-server -c ./webpack.config.ts"
},
"dependencies": {
"electron-rpc-api": "5.0.0",
"electron-rpc-api": "5.1.0-beta3",
"rxjs": "6.5.2",
"sanitize-html": "1.20.1",
"tcp-ping": "0.1.1"
Expand Down
248 changes: 135 additions & 113 deletions example/electron-app/yarn.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "electron-rpc-api",
"version": "5.1.0-beta2",
"version": "5.1.0-beta3",
"description": "Wrapper around the Electron's IPC for building type-safe API based RPC-like and reactive interactions",
"author": "Vladimir Yakovlev <[email protected]> (https://github.com/vladimiry)",
"license": "MIT",
Expand Down Expand Up @@ -45,7 +45,7 @@
"rxjs": "^6.5.2"
},
"dependencies": {
"pubsub-to-rpc-api": "^5.1.0-beta3",
"pubsub-to-rpc-api": "^5.1.0-beta4",
"tslib": "^1.9.2",
"uuid-browser": "^3.1.0"
},
Expand All @@ -68,7 +68,9 @@
"ts-node": "^8.0.2",
"tsconfig-paths": "^3.8.0",
"tslint": "^5.13.1",
"tslint-consistent-codestyle": "^1.15.1",
"tslint-eslint-rules": "^5.4.0",
"tslint-rules-bunch": "^0.0.7",
"typescript": "^3.3.3333"
"typescript": "^3.4.5"
}
}
94 changes: 54 additions & 40 deletions src/lib/ipc-main-api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,36 @@ import * as Lib from "pubsub-to-rpc-api";
import {IpcMain, IpcMessageEvent, IpcRenderer} from "electron";

import * as PM from "./private/model";
import {curryOwnFunctionMembers} from "./private/util";
import {requireIpcMain, requireIpcRenderer} from "./private/electron-require";

// TODO infer from Electron.IpcMain["on"] listener arguments
type ACA = [IpcMessageEvent, ...PM.Any[]];
type DefACA = [IpcMessageEvent, ...PM.Any[]];

type IpcMainEventEmittersCache = Pick<IpcMain, "on" | "removeListener" | "emit">;
type IpcRendererEventEmittersCache = Pick<IpcRenderer, "on" | "removeListener" | "send">;

const ipcMainEventEmittersCache = new WeakMap<IpcMainEventEmittersCache, Lib.Model.CombinedEventEmitter>();
const ipcRendererEventEmittersCache = new WeakMap<IpcRendererEventEmittersCache, Lib.Model.CombinedEventEmitter>();

export const createIpcMainApiService: <AD extends Lib.Model.ApiDefinition<AD>>(
input: Lib.Model.CreateServiceInput<AD>,
) => {
export function createIpcMainApiService<AD extends Lib.Model.ApiDefinition<AD>, ACA2 extends DefACA = DefACA>(
createServiceArg: Lib.Model.CreateServiceInput<AD>,
): {
register: (
actions: PM.Arguments<Lib.Model.CreateServiceReturn<AD, ACA>["register"]>[0],
actions: PM.Arguments<Lib.Model.CreateServiceReturn<AD, ACA2>["register"]>[0],
options?: {
ipcMain?: IpcMainEventEmittersCache;
logger?: Lib.Model.Logger;
},
) => ReturnType<Lib.Model.CreateServiceReturn<AD, ACA>["register"]>;
) => ReturnType<Lib.Model.CreateServiceReturn<AD, ACA2>["register"]>;
client: (
arg?: {
clientOptions?: {
ipcRenderer?: IpcRendererEventEmittersCache;
options?: Lib.Model.CallOptions;
options?: PM.Omit<Partial<Lib.Model.CallOptions<AD, ACA2>>, "onEventResolver">;
},
) => ReturnType<Lib.Model.CreateServiceReturn<AD, ACA>["caller"]>;
} = (...createServiceArgs) => {
const baseService: Readonly<ReturnType<typeof Lib.createService>>
= Lib.createService<(typeof createServiceArgs[0])["apiDefinition"], ACA>(...createServiceArgs);

const clientOnEventResolver: Lib.Model.ClientOnEventResolver = (...[/* event */, payload]) => {
return {payload};
};
) => ReturnType<Lib.Model.CreateServiceReturn<AD, ACA2>["caller"]>;
} {
const baseService = Lib.createService<AD, ACA2>(createServiceArg);

return {
register(actions, options) {
Expand All @@ -58,36 +54,44 @@ export const createIpcMainApiService: <AD extends Lib.Model.ApiDefinition<AD>>(
return em;
})()
);
const onEventResolver: Lib.Model.ProviderOnEventResolver<[IpcMessageEvent, ...PM.Any[]]> = ({sender}, payload) => {
return {
payload,
emitter: {
emit: (...args) => {
if (!sender.isDestroyed()) {
return sender.send(...args);
}
if (logger) {
logger.warn(`[${PM.MODULE_NAME}]`, `Object has been destroyed: "sender"`);
}
},
},
};
};

return baseService.register(
actions as PM.Any, // TODO get rid of typecasting
actions,
cachedEm,
{
onEventResolver,
onEventResolver: (...[{sender}, payload]) => {
return {
payload,
emitter: {
emit: (...args) => {
if (!sender.isDestroyed()) {
return sender.send(...args);
}
if (logger) {
logger.warn(`[${PM.MODULE_NAME}]`, `Object has been destroyed: "sender"`);
}
},
},
};
},
logger,
},
);
},
client(arg) {
const {
client(
{
ipcRenderer = requireIpcRenderer(),
options = {timeoutMs: PM.ONE_SECOND_MS * 3},
} = arg || {} as Exclude<typeof arg, undefined>;
options: {
timeoutMs = PM.BASE_TIMEOUT_MS,
logger: _logger_ = createServiceArg.logger || PM.LOG_STUB, // tslint:disable-line:variable-name
...callOptions
} = {},
}: {
ipcRenderer?: IpcRendererEventEmittersCache;
options?: PM.Omit<Partial<Lib.Model.CallOptions<AD, ACA2>>, "onEventResolver">;
} = {},
) {
const logger = curryOwnFunctionMembers(_logger_, `[${PM.MODULE_NAME}]`, "createIpcMainApiService() [client]");
const cachedEm: Lib.Model.CombinedEventEmitter = (
ipcRendererEventEmittersCache.get(ipcRenderer)
||
Expand All @@ -105,9 +109,19 @@ export const createIpcMainApiService: <AD extends Lib.Model.ApiDefinition<AD>>(
);

return baseService.caller(
{emitter: cachedEm, listener: cachedEm},
{onEventResolver: clientOnEventResolver, ...options},
{
emitter: cachedEm,
listener: cachedEm,
},
{
...callOptions,
timeoutMs,
logger,
onEventResolver: (...[/* event */, payload]) => {
return {payload};
},
},
);
},
};
};
}
4 changes: 4 additions & 0 deletions src/lib/private/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ export type Unpacked<T> =
T extends Observable<infer U3> ? U3 :
T;

export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

export const MODULE_NAME = "electron-rpc-api";

export const ONE_SECOND_MS = 1000;

export const BASE_TIMEOUT_MS = ONE_SECOND_MS * 3;

export const EMPTY_FN: Lib.Model.LoggerFn = () => {}; // tslint:disable-line:no-empty

export const LOG_STUB: Readonly<Lib.Model.Logger> = {
Expand Down
106 changes: 65 additions & 41 deletions src/lib/webview-api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,43 @@ import * as PM from "./private/model";
import {curryOwnFunctionMembers} from "./private/util";
import {requireIpcRenderer} from "./private/electron-require";

// TODO infer as PM.Arguments<(PM.Arguments<Electron.WebviewTag["on"]>)[1]> (listener is currently defined by Electron as a raw function)
type ACA2 = [IpcMessageEvent, ...PM.Any[]]; // used by "pubsub-to-rpc-api" like Lib.Model.ActionContext<ACA2>

type RegisterApiIpcRenderer = Pick<IpcRenderer, "on" | "removeListener" | "sendToHost">;

const ipcRendererEventEmittersCache = new WeakMap<RegisterApiIpcRenderer, Lib.Model.CombinedEventEmitter>();
const webViewTagEventEmittersCache = new WeakMap<WebviewTag, Lib.Model.CombinedEventEmitter>();

const clientIpcMessageEventName = "ipc-message";
const clientIpcMessageListenerBundleProp = Symbol(`[${PM.MODULE_NAME}] clientIpcMessageListenerBundleProp symbol`);
const clientIpcMessageOnEventResolver: Lib.Model.ClientOnEventResolver<[IpcMessageEvent]> = ({args: [payload]}) => {
// first argument of the IpcMessageEvent.args is the needed payload
return {payload};
};

export const createWebViewApiService: <AD extends Lib.Model.ApiDefinition<AD>>(
createServiceInput: Lib.Model.CreateServiceInput<AD>,
) => {

export function createWebViewApiService<AD extends Lib.Model.ApiDefinition<AD>>(
createServiceArg: Lib.Model.CreateServiceInput<AD>,
): {
register: (
actions: PM.Arguments<Lib.Model.CreateServiceReturn<AD>["register"]>[0],
options?: {
ipcRenderer?: RegisterApiIpcRenderer;
logger?: Lib.Model.Logger;
},
) => ReturnType<Lib.Model.CreateServiceReturn<AD>["register"]>;
actions: PM.Arguments<Lib.Model.CreateServiceReturn<AD, ACA2>["register"]>[0],
options?: Lib.Model.CreateServiceRegisterOptions<AD, ACA2> & { ipcRenderer?: RegisterApiIpcRenderer; },
) => ReturnType<Lib.Model.CreateServiceReturn<AD, ACA2>["register"]>;
client: (
webView: WebviewTag,
arg?: { options?: Lib.Model.CallOptions },
) => ReturnType<Lib.Model.CreateServiceReturn<AD>["caller"]>;
} = (createServiceInput) => {
const clientLogger = createServiceInput.logger
? curryOwnFunctionMembers(createServiceInput.logger, `[${PM.MODULE_NAME}]`, "createWebViewApiService()", "client()")
: PM.LOG_STUB;
const baseService: Readonly<ReturnType<typeof Lib.createService>> = Lib.createService(createServiceInput);
params?: { options?: Partial<Lib.Model.CallOptions<AD, ACA2>> },
) => ReturnType<Lib.Model.CreateServiceReturn<AD, ACA2>["caller"]>;
} {
const baseService = Lib.createService<AD, ACA2>(createServiceArg);
const clientIpcMessageOnEventResolver: Lib.Model.ClientOnEventResolver<AD, ACA2> = (ipcMessageEvent) => {
const [payload] = ipcMessageEvent.args;
return {payload};
};

return {
register(actions, options) {
const {
ipcRenderer = requireIpcRenderer(),
register(
actions,
{
logger,
} = options || {} as Exclude<typeof options, undefined>;
ipcRenderer = requireIpcRenderer(),
}: Lib.Model.CreateServiceRegisterOptions<AD, ACA2> & { ipcRenderer?: RegisterApiIpcRenderer; } = {},
) {
const cachedEm: Lib.Model.CombinedEventEmitter = (
ipcRendererEventEmittersCache.get(ipcRenderer)
||
Expand Down Expand Up @@ -74,19 +73,30 @@ export const createWebViewApiService: <AD extends Lib.Model.ApiDefinition<AD>>(
},
);
},
client(webView, params) {
const {options} = params || {} as Exclude<typeof params, undefined>;
client(
webView,
{
options: {
timeoutMs = PM.BASE_TIMEOUT_MS,
logger: _logger_ = createServiceArg.logger || PM.LOG_STUB, // tslint:disable-line:variable-name
...callOptions
} = {},
}: {
options?: Partial<Lib.Model.CallOptions<AD, ACA2>>;
} = {},
) {
const logger = curryOwnFunctionMembers(_logger_, `[${PM.MODULE_NAME}]`, "createWebViewApiService() [client]");
const cachedEm: Lib.Model.CombinedEventEmitter = (
webViewTagEventEmittersCache.get(webView)
||
(() => {
type IpcMessageListener = (ipcMessageEvent: IpcMessageEvent) => void;

type IpcMessageListenerBundleProp = Readonly<{
uid: ReturnType<typeof uuid.v4>;
created: Date;
originalEventName: PM.Arguments<Lib.Model.CombinedEventEmitter["on"]>[0];
actual: [typeof clientIpcMessageEventName, IpcMessageListener];
actualListener: [
typeof clientIpcMessageEventName,
(...args: PM.Arguments<typeof clientIpcMessageOnEventResolver>) => void];
}>;

interface IpcMessageListenerBundlePropAware {
Expand All @@ -99,7 +109,7 @@ export const createWebViewApiService: <AD extends Lib.Model.ApiDefinition<AD>>(
uid: uuid.v4(),
created: new Date(),
originalEventName,
actual: [
actualListener: [
clientIpcMessageEventName,
(ipcMessageEvent) => {
if (ipcMessageEvent.channel !== originalEventName) {
Expand All @@ -110,15 +120,22 @@ export const createWebViewApiService: <AD extends Lib.Model.ApiDefinition<AD>>(
],
};

webView.addEventListener(...ipcMessageListenerBundle.actual);
webView.addEventListener(...ipcMessageListenerBundle.actualListener);

// TODO consider keeping actual listeners in a WeakMap<typeof originalListener, IpcMessageListenerBundleProp>
// link actual listener to the original listener, so we then could remove the actual listener
// we know that "listener" function is not locked for writing props as it's constructed by "pubsub-to-rpc-api"
(originalListener as IpcMessageListenerBundlePropAware)[clientIpcMessageListenerBundleProp]
= ipcMessageListenerBundle;

clientLogger.debug(`em: addEventListener(), uid=${ipcMessageListenerBundle.uid}`);
logger.debug(
`[cache] add event listener`,
JSON.stringify({
originalEventName,
uid: ipcMessageListenerBundle.uid,
created: ipcMessageListenerBundle.created,
}),
);

return em;
},
Expand All @@ -134,24 +151,28 @@ export const createWebViewApiService: <AD extends Lib.Model.ApiDefinition<AD>>(
return em;
}

const logData = JSON.stringify({
originalEventName,
uid: ipcMessageListenerBundle.uid,
created: ipcMessageListenerBundle.created,
});

if (webView.isConnected) {
webView.removeEventListener(...ipcMessageListenerBundle.actual);
webView.removeEventListener(...ipcMessageListenerBundle.actualListener);
logger.warn(`[cache] remove event listener`, logData);
} else {
clientLogger.warn(`em: skip "webView.removeEventListener()" since "webView" is not attached to the DOM`);
logger.warn(`[cache] remove event listener: skipped since "webView" is not attached to the DOM`, logData);
}

delete ipcMessageListenerBundlePropAware[clientIpcMessageListenerBundleProp];

const {uid, created} = ipcMessageListenerBundle;
clientLogger.debug(`em: removeEventListener(), uid=${uid}, created=${created}`);

return em;
},
emit: (...args) => {
if (webView.isConnected) {
webView.send(...args);
} else {
clientLogger.warn(`em: skip "webView.send()" since "webView" is not attached to the DOM`);
logger.warn(`"webView.send()" call skipped since "webView" is not attached to the DOM`);
}
},
};
Expand All @@ -164,8 +185,11 @@ export const createWebViewApiService: <AD extends Lib.Model.ApiDefinition<AD>>(

return baseService.caller(
{emitter: cachedEm, listener: cachedEm},
options,
{
...callOptions,
timeoutMs,
},
);
},
};
};
}
Loading

0 comments on commit dcb9748

Please sign in to comment.