Add better type safety around Channels (#6133)

* Add better type safety around Channels

- Reallow non-JSON serializable complex types

- Remove unnecessary JSON serialization step to IPC

- Remove unnecesary injectable-ization of MessageChannel and
  RequestChannel declarations

- Add channel listener injectable getter function to remove general need
  for type casting and improve type safety

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add check to verify data in IPC related tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix implementation in light of new safety check in tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix listener ID clash potential

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch to not forcing split of request channel handlers being seperate from the listener decl

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move message channels to changed signature too

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix not registering listener

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test imports

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Refactor listening of different channel kinds

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add test covering new checking code for request channel handlers

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-10-13 07:50:19 -04:00
1 parent d96918c966
commit 5a5eda935b
115 files changed
+949 -1568

No files matched your search

@@ -6,8 +6,6 @@ import ipcRendererInjectable from "../ipc-renderer.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { IpcRendererEvent } from "electron";
import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
import { tentativeParseJson } from "../../../../common/utils/tentative-parse-json";
import { pipeline } from "@ogre-tools/fp";
const enlistMessageChannelListenerInjectable = getInjectable({
id: "enlist-message-channel-listener-for-renderer",
@@ -17,11 +15,7 @@ const enlistMessageChannelListenerInjectable = getInjectable({
return ({ channel, handler }) => {
const nativeCallback = (_: IpcRendererEvent, message: unknown) => {
pipeline(
message,
tentativeParseJson,
handler,
);
handler(message);
};
ipcRenderer.on(channel.id, nativeCallback);
@@ -88,8 +88,8 @@ describe("enlist message channel listener in renderer", () => {
expect(handlerMock).toHaveBeenCalledWith(true);
});
it("given stringified object as message, when message arrives, calls the handler with the message", () => {
onMock.mock.calls[0][1]({} as IpcRendererEvent, JSON.stringify({ some: "object" }));
it("given object as message, when message arrives, calls the handler with the message", () => {
onMock.mock.calls[0][1]({} as IpcRendererEvent, { some: "object" });
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
});
@@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { enlistRequestChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token";
const enlistRequestChannelListenerInjectable = getInjectable({
id: "enlist-request-channel-listener-for-renderer",
instantiate: () => {
// Requests from main to renderer are not implemented yet.
return () => () => {};
},
injectionToken: enlistRequestChannelListenerInjectionToken,
});
export default enlistRequestChannelListenerInjectable;
@@ -4,13 +4,13 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { beforeFrameStartsInjectionToken } from "../../../before-frame-starts/before-frame-starts-injection-token";
import listeningOfChannelsInjectable from "../../../../common/utils/channel/listening-of-channels.injectable";
import listeningOnMessageChannelsInjectable from "../../../../common/utils/channel/listening-on-message-channels.injectable";
const startListeningOfChannelsInjectable = getInjectable({
id: "start-listening-of-channels-renderer",
instantiate: (di) => {
const listeningOfChannels = di.inject(listeningOfChannelsInjectable);
const listeningOfChannels = di.inject(listeningOnMessageChannelsInjectable);
return {
id: "start-listening-of-channels-renderer",
@@ -3,9 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
import type { SendMessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token";
import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
import sendToMainInjectable from "./send-to-main.injectable";
import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token";
const messageToChannelInjectable = getInjectable({
id: "message-to-channel",
@@ -13,14 +13,12 @@ const messageToChannelInjectable = getInjectable({
instantiate: (di) => {
const sendToMain = di.inject(sendToMainInjectable);
// TODO: Figure out way to improve typing in internals
// Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly.
return (channel: MessageChannel<any>, message?: unknown) => {
return ((channel, message) => {
sendToMain(channel.id, message);
};
}) as SendMessageToChannel;
},
injectionToken: messageToChannelInjectionToken,
injectionToken: sendMessageToChannelInjectionToken,
});
export default messageToChannelInjectable;
@@ -1,57 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token";
import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
import ipcRendererInjectable from "./ipc-renderer.injectable";
import type { IpcRenderer } from "electron";
describe("message to channel from renderer", () => {
let messageToChannel: MessageToChannel;
let sendMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
sendMock = jest.fn();
di.override(ipcRendererInjectable, () => ({
send: sendMock,
}) as unknown as IpcRenderer);
messageToChannel = di.inject(messageToChannelInjectionToken);
});
it("given string as message, when messaging to channel, sends stringified message", () => {
messageToChannel(someChannel, "some-message");
expect(sendMock).toHaveBeenCalledWith("some-channel-id", '"some-message"');
});
it("given boolean as message, when messaging to channel, sends stringified message", () => {
messageToChannel(someChannel, true);
expect(sendMock).toHaveBeenCalledWith("some-channel-id", "true");
});
it("given number as message, when messaging to channel, sends stringified message", () => {
messageToChannel(someChannel, 42);
expect(sendMock).toHaveBeenCalledWith("some-channel-id", "42");
});
it("given object as message, when messaging to channel, sends stringified message", () => {
messageToChannel(someChannel, { some: "object" });
expect(sendMock).toHaveBeenCalledWith(
"some-channel-id",
JSON.stringify({ some: "object" }),
);
});
});
const someChannel: MessageChannel<any> = { id: "some-channel-id" };
@@ -4,10 +4,8 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import ipcRendererInjectable from "./ipc-renderer.injectable";
import type { RequestFromChannel } from "../../../common/utils/channel/request-from-channel-injection-token";
import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json";
import { tentativeParseJson } from "../../../common/utils/tentative-parse-json";
const requestFromChannelInjectable = getInjectable({
id: "request-from-channel",
@@ -15,13 +13,7 @@ const requestFromChannelInjectable = getInjectable({
instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable);
return async (channel, ...[request]) =>
await pipeline(
request,
tentativeStringifyJson,
(req) => ipcRenderer.invoke(channel.id, req),
tentativeParseJson,
);
return ((channel, request) => ipcRenderer.invoke(channel.id, request)) as RequestFromChannel;
},
injectionToken: requestFromChannelInjectionToken,
@@ -1,121 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import ipcRendererInjectable from "./ipc-renderer.injectable";
import type { IpcRenderer } from "electron";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { RequestFromChannel } from "../../../common/utils/channel/request-from-channel-injection-token";
import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token";
import requestFromChannelInjectable from "./request-from-channel.injectable";
import { getPromiseStatus } from "../../../common/test-utils/get-promise-status";
describe("request from channel in renderer", () => {
let requestFromChannel: RequestFromChannel;
let invokeMock: AsyncFnMock<(channelId: string, request: any) => any>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.unoverride(requestFromChannelInjectable);
invokeMock = asyncFn();
di.override(ipcRendererInjectable, () => ({
invoke: invokeMock,
}) as unknown as IpcRenderer);
requestFromChannel = di.inject(requestFromChannelInjectionToken);
});
describe("when messaging to channel", () => {
let actualPromise: Promise<any>;
beforeEach(() => {
actualPromise = requestFromChannel(someChannel, "some-message");
});
it("sends stringified message", () => {
expect(invokeMock).toHaveBeenCalledWith("some-channel-id", '"some-message"');
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("when invoking resolves, resolves", async () => {
await invokeMock.resolve("some-response");
const actual = await actualPromise;
expect(actual).toBe("some-response");
});
it("when invoking resolves with stringified string, resolves with string", async () => {
await invokeMock.resolve('"some-response"');
const actual = await actualPromise;
expect(actual).toBe("some-response");
});
it("when invoking resolves with stringified boolean, resolves with boolean", async () => {
await invokeMock.resolve("true");
const actual = await actualPromise;
expect(actual).toBe(true);
});
it("when invoking resolves with stringified number, resolves with number", async () => {
await invokeMock.resolve("42");
const actual = await actualPromise;
expect(actual).toBe(42);
});
it("when invoking resolves with stringified object, resolves with object", async () => {
await invokeMock.resolve(JSON.stringify({ some: "object" }));
const actual = await actualPromise;
expect(actual).toEqual({ some: "object" });
});
});
it("given string as message, when messaging to channel, sends stringified message", () => {
requestFromChannel(someChannel, "some-message");
expect(invokeMock).toHaveBeenCalledWith("some-channel-id", '"some-message"');
});
it("given boolean as message, when messaging to channel, sends stringified message", () => {
requestFromChannel(someChannel, true);
expect(invokeMock).toHaveBeenCalledWith("some-channel-id", "true");
});
it("given number as message, when messaging to channel, sends stringified message", () => {
requestFromChannel(someChannel, 42);
expect(invokeMock).toHaveBeenCalledWith("some-channel-id", "42");
});
it("given object as message, when messaging to channel, sends stringified message", () => {
requestFromChannel(someChannel, { some: "object" });
expect(invokeMock).toHaveBeenCalledWith(
"some-channel-id",
JSON.stringify({ some: "object" }),
);
});
});
const someChannel: MessageChannel<any> = { id: "some-channel-id" };
@@ -3,9 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { JsonValue } from "type-fest";
import ipcRendererInjectable from "./ipc-renderer.injectable";
import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json";
const sendToMainInjectable = getInjectable({
id: "send-to-main",
@@ -13,11 +11,8 @@ const sendToMainInjectable = getInjectable({
instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable);
// TODO: Figure out way to improve typing in internals
return <T>(channelId: string, message: JsonValue extends T ? T : never ) => {
const stringifiedMessage = tentativeStringifyJson(message);
ipcRenderer.send(channelId, ...(stringifiedMessage ? [stringifiedMessage] : []));
return (channelId: string, message: any) => {
ipcRenderer.send(channelId, message);
};
},
});
@@ -5,14 +5,13 @@
import { getInjectable } from "@ogre-tools/injectable";
import { resolveSystemProxyInjectionToken } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-injection-token";
import requestFromChannelInjectable from "../channel/request-from-channel.injectable";
import resolveSystemProxyChannelInjectable from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel.injectable";
import { resolveSystemProxyChannel } from "../../../common/utils/resolve-system-proxy/resolve-system-proxy-channel";
const resolveSystemProxyInjectable = getInjectable({
id: "resolve-system-proxy-for-renderer",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectable);
const resolveSystemProxyChannel = di.inject(resolveSystemProxyChannelInjectable);
return async (url) => requestFromChannel(resolveSystemProxyChannel, url);
},
@@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { beforeFrameStartsInjectionToken } from "../../before-frame-starts/before-frame-starts-injection-token";
import syncBoxInitialValueChannelInjectable from "../../../common/utils/sync-box/sync-box-initial-value-channel.injectable";
import { syncBoxInitialValueChannel } from "../../../common/utils/sync-box/channels";
import createSyncBoxStateInjectable from "../../../common/utils/sync-box/sync-box-state.injectable";
import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token";
import { runInAction } from "mobx";
@@ -17,7 +17,6 @@ const provideInitialValuesForSyncBoxesInjectable = getInjectable({
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const syncBoxInitialValueChannel = di.inject(syncBoxInitialValueChannelInjectable);
const syncBoxes = di.injectMany(syncBoxInjectionToken);