1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce competition for channel abstraction

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-05-16 13:54:02 +03:00
parent bb2e8fbf65
commit 18b1f6df35
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
14 changed files with 504 additions and 1 deletions

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export interface Channel {
id: string;
}
export const channelInjectionToken = getInjectionToken<Channel>({
id: "channel",
});

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export interface ChannelListener {
channel: any;
handler: (value: any) => void;
}
export const channelListenerInjectionToken = getInjectionToken<ChannelListener>(
{
id: "channel-listener",
},
);

View File

@ -0,0 +1,187 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/lens-window-injection-token";
import { lensWindowInjectionToken } from "../../../main/start-main-application/lens-window/application-window/lens-window-injection-token";
import { sendToAgnosticChannelInjectionToken } from "./send-to-agnostic-channel-injection-token";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { channelListenerInjectionToken } from "./channel-listener-injection-token";
import createLensWindowInjectable from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { Channel } from "./channel-injection-token";
import { channelInjectionToken } from "./channel-injection-token";
describe("channel", () => {
describe("messaging from main to renderer, given listener for channel in a window and application has started", () => {
let testChannel: Channel;
let testListenerInWindowMock: jest.Mock;
let mainDi: DiContainer;
let sendToAgnosticChannel: (channel: Channel, message: any) => void;
beforeEach(async () => {
const applicationBuilder = getApplicationBuilder();
mainDi = applicationBuilder.dis.mainDi;
const rendererDi = applicationBuilder.dis.rendererDi;
testListenerInWindowMock = jest.fn();
const testChannelListenerInTestWindowInjectable = getInjectable({
id: "test-channel-listener-in-test-window",
instantiate: (di) => ({
channel: di.inject(testChannelInjectable),
handler: testListenerInWindowMock,
}),
injectionToken: channelListenerInjectionToken,
});
rendererDi.register(testChannelListenerInTestWindowInjectable);
// Notice how test channel has presence in both DIs, being from common
mainDi.register(testChannelInjectable);
rendererDi.register(testChannelInjectable);
testChannel = mainDi.inject(testChannelInjectable);
sendToAgnosticChannel = mainDi.inject(
sendToAgnosticChannelInjectionToken,
);
await applicationBuilder.render();
});
describe("given window is shown", () => {
let someWindowFake: LensWindow;
beforeEach(async () => {
someWindowFake = createTestWindow(mainDi, "some-window");
await someWindowFake.show();
});
it("when sending message, triggers listener in window", () => {
sendToAgnosticChannel(testChannel, "some-message");
expect(testListenerInWindowMock).toHaveBeenCalledWith("some-message");
});
it("given window is hidden, when sending message, does not trigger listener in window", () => {
someWindowFake.close();
sendToAgnosticChannel(testChannel, "some-message");
expect(testListenerInWindowMock).not.toHaveBeenCalled();
});
});
it("given multiple shown windows, when sending message, triggers listeners in all windows", async () => {
const someWindowFake = createTestWindow(mainDi, "some-window");
const someOtherWindowFake = createTestWindow(mainDi, "some-other-window");
await someWindowFake.show();
await someOtherWindowFake.show();
sendToAgnosticChannel(testChannel, "some-message");
expect(testListenerInWindowMock.mock.calls).toEqual([
["some-message"],
["some-message"],
]);
});
});
describe("messaging from renderer to main, given listener for channel in a main and application has started", () => {
let testChannel: Channel;
let testListenerInMainMock: jest.Mock;
let rendererDi: DiContainer;
let mainDi: DiContainer;
let sendToAgnosticChannel: (channel: Channel, message: any) => void;
beforeEach(async () => {
const applicationBuilder = getApplicationBuilder();
mainDi = applicationBuilder.dis.mainDi;
rendererDi = applicationBuilder.dis.rendererDi;
testListenerInMainMock = jest.fn();
const testChannelListenerInMainInjectable = getInjectable({
id: "test-channel-listener-in-main",
instantiate: (di) => ({
channel: di.inject(testChannelInjectable),
handler: testListenerInMainMock,
}),
injectionToken: channelListenerInjectionToken,
});
mainDi.register(testChannelListenerInMainInjectable);
// Notice how test channel has presence in both DIs, being from common
mainDi.register(testChannelInjectable);
rendererDi.register(testChannelInjectable);
testChannel = rendererDi.inject(testChannelInjectable);
sendToAgnosticChannel = rendererDi.inject(
sendToAgnosticChannelInjectionToken,
);
await applicationBuilder.render();
});
it("when sending message, triggers listener in main", () => {
sendToAgnosticChannel(testChannel, "some-message");
expect(testListenerInMainMock).toHaveBeenCalledWith("some-message");
});
});
});
const testChannelInjectable = getInjectable({
id: "some-test-channel",
instantiate: () => {
const channelId = "some-channel-id";
return {
id: channelId,
};
},
injectionToken: channelInjectionToken,
});
const createTestWindow = (di: DiContainer, id: string) => {
const testWindowInjectable = getInjectable({
id,
instantiate: (di) => {
const createLensWindow = di.inject(createLensWindowInjectable);
return createLensWindow({
id,
title: "Some test window",
defaultHeight: 42,
defaultWidth: 42,
getContentUrl: () => "some-content-url",
resizable: true,
windowFrameUtilitiesAreShown: false,
centered: false,
});
},
injectionToken: lensWindowInjectionToken,
});
di.register(testWindowInjectable);
return di.inject(testWindowInjectable);
};

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export const enlistChannelListenerInjectionToken = getInjectionToken<
(channel: any, handler: any) => () => void
>({
id: "enlist-channel-listener",
});

View File

@ -0,0 +1,32 @@
/**
* 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 { getStartableStoppable } from "../../utils/get-startable-stoppable";
import { channelListenerInjectionToken } from "./channel-listener-injection-token";
import { enlistChannelListenerInjectionToken } from "./enlist-channel-listener-injection-token";
const listeningOfChannelsInjectable = getInjectable({
id: "listening-of-channels",
instantiate: (di) => {
const enlistChannelListener = di.inject(enlistChannelListenerInjectionToken);
const channelListeners = di.injectMany(channelListenerInjectionToken);
return getStartableStoppable("listening-of-channels", () => {
const disposers = channelListeners.map(({ channel, handler }) =>
enlistChannelListener(channel, handler),
);
return () => {
disposers.forEach((disposer) => {
disposer();
});
};
});
},
});
export default listeningOfChannelsInjectable;

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { Channel } from "./channel-injection-token";
export const sendToAgnosticChannelInjectionToken = getInjectionToken<(channel: Channel, message: any) => void>({
id: "send-to-agnostic-channel",
});

View File

@ -0,0 +1,31 @@
/**
* 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 type { IpcMainEvent } from "electron";
import ipcMainInjectable from "../../app-paths/register-channel/ipc-main/ipc-main.injectable";
import { enlistChannelListenerInjectionToken } from "../../../common/sync-box/channel/enlist-channel-listener-injection-token";
const enlistChannelListenerInjectable = getInjectable({
id: "enlist-channel-listener-for-main",
instantiate: (di) => {
const ipcMain = di.inject(ipcMainInjectable);
return (channel: any, handler: any) => {
const nativeCallback = (_: IpcMainEvent, message: unknown) =>
handler(message);
ipcMain.on(channel.id, nativeCallback);
return () => {
ipcMain.off(channel.id, nativeCallback);
};
};
},
injectionToken: enlistChannelListenerInjectionToken,
});
export default enlistChannelListenerInjectable;

View File

@ -0,0 +1,25 @@
/**
* 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 { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import listeningOfChannelsInjectable from "../../../common/sync-box/channel/listening-of-channels.injectable";
const startListeningOfChannelsInjectable = getInjectable({
id: "start-listening-of-channels-main",
instantiate: (di) => {
const listeningOfChannels = di.inject(listeningOfChannelsInjectable);
return {
run: async () => {
await listeningOfChannels.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startListeningOfChannelsInjectable;

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { lensWindowInjectionToken } from "../start-main-application/lens-window/application-window/lens-window-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { getInjectable } from "@ogre-tools/injectable";
import { filter } from "lodash/fp";
import { sendToAgnosticChannelInjectionToken } from "../../common/sync-box/channel/send-to-agnostic-channel-injection-token";
const sendToAgnosticChannelInjectable = getInjectable({
id: "send-to-agnostic-channel-main",
instantiate: (di) => {
const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken);
return (channel, message) => {
const visibleWindows = pipeline(
getAllLensWindows(),
filter((lensWindow) => !!lensWindow.visible),
);
visibleWindows.forEach((lensWindow) =>
lensWindow.send({ channel: channel.id, data: [message] }),
);
};
},
injectionToken: sendToAgnosticChannelInjectionToken,
});
export default sendToAgnosticChannelInjectable;

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import ipcRendererInjectable from "../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { IpcRendererEvent } from "electron";
import { enlistChannelListenerInjectionToken } from "../../../common/sync-box/channel/enlist-channel-listener-injection-token";
const enlistChannelListenerInjectable = getInjectable({
id: "enlist-channel-listener-for-renderer",
instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable);
return (channel: any, handler: any) => {
const nativeCallback = (_: IpcRendererEvent, message: unknown) =>
handler(message);
ipcRenderer.on(channel.id, nativeCallback);
return () => {
ipcRenderer.off(channel.id, nativeCallback);
};
};
},
injectionToken: enlistChannelListenerInjectionToken,
});
export default enlistChannelListenerInjectable;

View File

@ -0,0 +1,25 @@
/**
* 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 { beforeFrameStartsInjectionToken } from "../../before-frame-starts/before-frame-starts-injection-token";
import listeningOfChannelsInjectable from "../../../common/sync-box/channel/listening-of-channels.injectable";
const startListeningOfChannelsInjectable = getInjectable({
id: "start-listening-of-channels-renderer",
instantiate: (di) => {
const listeningOfChannels = di.inject(listeningOfChannelsInjectable);
return {
run: async () => {
await listeningOfChannels.start();
},
};
},
injectionToken: beforeFrameStartsInjectionToken,
});
export default startListeningOfChannelsInjectable;

View File

@ -0,0 +1,23 @@
/**
* 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 { sendToAgnosticChannelInjectionToken } from "../../common/sync-box/channel/send-to-agnostic-channel-injection-token";
import sendToMainInjectable from "./send-to-main.injectable";
const sendToAgnosticChannelInjectable = getInjectable({
id: "send-to-agnostic-channel-main",
instantiate: (di) => {
const sendToMain = di.inject(sendToMainInjectable);
return (channel, message) => {
sendToMain(channel.id, message);
};
},
injectionToken: sendToAgnosticChannelInjectionToken,
});
export default sendToAgnosticChannelInjectable;

View File

@ -0,0 +1,20 @@
/**
* 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 ipcRendererInjectable from "../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
const sendToMainInjectable = getInjectable({
id: "send-to-main",
instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable);
return (channelId: string, message: any) => {
ipcRenderer.send(channelId, message);
};
},
});
export default sendToMainInjectable;

View File

@ -11,7 +11,9 @@ import registerIpcChannelListenerInjectable from "../renderer/app-paths/get-valu
import type { SendToViewArgs } from "../main/start-main-application/lens-window/application-window/lens-window-injection-token";
import sendToChannelInElectronBrowserWindowInjectable from "../main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable";
import { isEmpty } from "lodash/fp";
import enlistChannelListenerInjectableInRenderer from "../renderer/channel/channel-listeners/enlist-channel-listener.injectable";
import enlistChannelListenerInjectableInMain from "../main/channel/channel-listeners/enlist-channel-listener.injectable";
import sendToMainInjectable from "../renderer/channel/send-to-main.injectable";
export const overrideIpcBridge = ({
rendererDi,
@ -101,4 +103,49 @@ export const overrideIpcBridge = ({
handles.forEach((handle) => handle(...data));
},
);
const mainIpcFakeHandles = new Map<
string,
((...args: any[]) => void)[]
>();
rendererDi.override(
enlistChannelListenerInjectableInRenderer,
() => (channel, handler) => {
const existingHandles = rendererIpcFakeHandles.get(channel.id) || [];
rendererIpcFakeHandles.set(channel.id, [...existingHandles, handler]);
return () => {
};
},
);
rendererDi.override(sendToMainInjectable, () => (channelId, message) => {
const handles = mainIpcFakeHandles.get(channelId);
if (isEmpty(handles)) {
throw new Error(
`Tried to send message to channel "${channelId}" but there where no listeners. Current channels with listeners: "${[
...mainIpcFakeHandles.keys(),
].join('", "')}"`,
);
}
handles.forEach((handle) => handle(message));
});
mainDi.override(
enlistChannelListenerInjectableInMain,
() => (channel, handler) => {
const existingHandles = mainIpcFakeHandles.get(channel.id) || [];
mainIpcFakeHandles.set(channel.id, [...existingHandles, handler]);
return () => {
};
},
);
};