diff --git a/src/common/sync-box/channel/channel-injection-token.ts b/src/common/sync-box/channel/channel-injection-token.ts new file mode 100644 index 0000000000..1cc73f0841 --- /dev/null +++ b/src/common/sync-box/channel/channel-injection-token.ts @@ -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({ + id: "channel", +}); diff --git a/src/common/sync-box/channel/channel-listener-injection-token.ts b/src/common/sync-box/channel/channel-listener-injection-token.ts new file mode 100644 index 0000000000..6ce3268663 --- /dev/null +++ b/src/common/sync-box/channel/channel-listener-injection-token.ts @@ -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( + { + id: "channel-listener", + }, +); diff --git a/src/common/sync-box/channel/channel.test.ts b/src/common/sync-box/channel/channel.test.ts new file mode 100644 index 0000000000..2e14f3ac58 --- /dev/null +++ b/src/common/sync-box/channel/channel.test.ts @@ -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); +}; diff --git a/src/common/sync-box/channel/enlist-channel-listener-injection-token.ts b/src/common/sync-box/channel/enlist-channel-listener-injection-token.ts new file mode 100644 index 0000000000..7f02254826 --- /dev/null +++ b/src/common/sync-box/channel/enlist-channel-listener-injection-token.ts @@ -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", + }); diff --git a/src/common/sync-box/channel/listening-of-channels.injectable.ts b/src/common/sync-box/channel/listening-of-channels.injectable.ts new file mode 100644 index 0000000000..0e82600642 --- /dev/null +++ b/src/common/sync-box/channel/listening-of-channels.injectable.ts @@ -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; diff --git a/src/common/sync-box/channel/send-to-agnostic-channel-injection-token.ts b/src/common/sync-box/channel/send-to-agnostic-channel-injection-token.ts new file mode 100644 index 0000000000..8322d2b39e --- /dev/null +++ b/src/common/sync-box/channel/send-to-agnostic-channel-injection-token.ts @@ -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", +}); diff --git a/src/main/channel/channel-listeners/enlist-channel-listener.injectable.ts b/src/main/channel/channel-listeners/enlist-channel-listener.injectable.ts new file mode 100644 index 0000000000..55241da3f2 --- /dev/null +++ b/src/main/channel/channel-listeners/enlist-channel-listener.injectable.ts @@ -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; diff --git a/src/main/channel/channel-listeners/start-listening-of-channels.injectable.ts b/src/main/channel/channel-listeners/start-listening-of-channels.injectable.ts new file mode 100644 index 0000000000..200dcdea31 --- /dev/null +++ b/src/main/channel/channel-listeners/start-listening-of-channels.injectable.ts @@ -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; diff --git a/src/main/channel/send-to-agnostic-channel.injectable.ts b/src/main/channel/send-to-agnostic-channel.injectable.ts new file mode 100644 index 0000000000..6de4323244 --- /dev/null +++ b/src/main/channel/send-to-agnostic-channel.injectable.ts @@ -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; diff --git a/src/renderer/channel/channel-listeners/enlist-channel-listener.injectable.ts b/src/renderer/channel/channel-listeners/enlist-channel-listener.injectable.ts new file mode 100644 index 0000000000..b596c0aa7a --- /dev/null +++ b/src/renderer/channel/channel-listeners/enlist-channel-listener.injectable.ts @@ -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; diff --git a/src/renderer/channel/channel-listeners/start-listening-of-channels.injectable.ts b/src/renderer/channel/channel-listeners/start-listening-of-channels.injectable.ts new file mode 100644 index 0000000000..2baf1bd2b2 --- /dev/null +++ b/src/renderer/channel/channel-listeners/start-listening-of-channels.injectable.ts @@ -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; diff --git a/src/renderer/channel/send-to-agnostic-channel.injectable.ts b/src/renderer/channel/send-to-agnostic-channel.injectable.ts new file mode 100644 index 0000000000..8cdb1110e1 --- /dev/null +++ b/src/renderer/channel/send-to-agnostic-channel.injectable.ts @@ -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; diff --git a/src/renderer/channel/send-to-main.injectable.ts b/src/renderer/channel/send-to-main.injectable.ts new file mode 100644 index 0000000000..80223ef56d --- /dev/null +++ b/src/renderer/channel/send-to-main.injectable.ts @@ -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; diff --git a/src/test-utils/override-ipc-bridge.ts b/src/test-utils/override-ipc-bridge.ts index a914cc66b6..61858b3b0a 100644 --- a/src/test-utils/override-ipc-bridge.ts +++ b/src/test-utils/override-ipc-bridge.ts @@ -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 () => { + + }; + }, + ); };