diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 4000684d16..241247f60c 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -11,7 +11,7 @@ import { appEventBus } from "./event-bus"; import { dumpConfigYaml } from "./kube-helpers"; import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; -import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; +import { createTypedInvoker, isEmptyArgs, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; import _ from "lodash"; import move from "array-move"; import type { WorkspaceId } from "./workspace-store"; @@ -90,6 +90,24 @@ export interface ClusterPrometheusPreferences { }; } +interface ClusterStateSync { + id: string; + state: ClusterState; +} + +function ClusterStoreStateHandler(): ClusterStateSync[] { + return clusterStore.clustersList.map(cluster => ({ + state: cluster.getState(), + id: cluster.id, + })); +} + +const clusterStoreStateRequest = createTypedInvoker({ + channel: "cluster:states", + handler: ClusterStoreStateHandler, + verifier: isEmptyArgs, +}); + export class ClusterStore extends BaseStore { static getCustomKubeConfigPath(clusterId: ClusterId): string { return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId); @@ -108,8 +126,6 @@ export class ClusterStore extends BaseStore { @observable removedClusters = observable.map(); @observable clusters = observable.map(); - private static stateRequestChannel = "cluster:states"; - private constructor() { super({ configName: "lens-cluster-store", @@ -125,35 +141,13 @@ export class ClusterStore extends BaseStore { async load() { await super.load(); - type clusterStateSync = { - id: string; - state: ClusterState; - }; if (ipcRenderer) { logger.info("[CLUSTER-STORE] requesting initial state sync"); - const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel); - clusterStates.forEach((clusterState) => { - const cluster = this.getById(clusterState.id); - - if (cluster) { - cluster.setState(clusterState.state); - } - }); - } else { - handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => { - const states: clusterStateSync[] = []; - - this.clustersList.forEach((cluster) => { - states.push({ - state: cluster.getState(), - id: cluster.id - }); - }); - - return states; - }); + for (const { id, state } of await clusterStoreStateRequest.invoke()) { + this.getById(id)?.setState(state); + } } } diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index 48b0b89153..1d13b0aea4 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -6,8 +6,13 @@ import { ipcMain, ipcRenderer, webContents, remote } from "electron"; import { toJS } from "mobx"; import logger from "../../main/logger"; import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; +import { createTypedInvoker, isEmptyArgs } from "./type-enforced-ipc"; -const subFramesChannel = "ipc:get-sub-frames"; +const subFrames = createTypedInvoker({ + channel: "ipc:get-sub-frames", + handler: getSubFrames, + verifier: isEmptyArgs, +}); export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { ipcMain.handle(channel, listener); @@ -39,11 +44,11 @@ export async function broadcastMessage(channel: string, ...args: any[]) { view.send(channel, ...args); try { - const subFrames: ClusterFrameInfo[] = ipcRenderer - ? await requestMain(subFramesChannel) + const childFrames: ClusterFrameInfo[] = ipcRenderer + ? await subFrames.invoke() : getSubFrames(); - for (const frameInfo of subFrames) { + for (const frameInfo of childFrames) { view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); } } catch (error) { @@ -77,9 +82,3 @@ export function unsubscribeAllFromBroadcast(channel: string) { ipcMain.removeAllListeners(channel); } } - -export function bindBroadcastHandlers() { - handleRequest(subFramesChannel, () => { - return getSubFrames(); - }); -} diff --git a/src/common/ipc/type-enforced-ipc.ts b/src/common/ipc/type-enforced-ipc.ts index c0d4ee0d65..7f4458204d 100644 --- a/src/common/ipc/type-enforced-ipc.ts +++ b/src/common/ipc/type-enforced-ipc.ts @@ -8,6 +8,10 @@ export type ListVerifier = (args: unknown[]) => args is T; export type Rest = T extends [any, ...infer R] ? R : []; export type IpcListener = (e: E, ...args: Args) => void; +export function isEmptyArgs(args: unknown[]): args is [] { + return args.length === 0; +} + /** * Adds a listener to `source` that waits for the first IPC message with the correct * argument data is sent. @@ -210,7 +214,7 @@ export function createTypedSender< source: ipcMain ?? ipcRenderer, channel, listener, - verifier: verifier as any, // safety: this verifier is correct, TS just doesn't equate Rest<..> correctly + verifier: verifier as ListVerifier>, }); }, once(listener) { @@ -218,7 +222,7 @@ export function createTypedSender< source: ipcMain ?? ipcRenderer, channel, listener, - verifier: verifier as any, // safety: this verifier is correct, TS just doesn't equate Rest<..> correctly + verifier: verifier as ListVerifier>, }); } }; diff --git a/src/common/utils/type-narrowing.ts b/src/common/utils/type-narrowing.ts index cdfa98666f..57c8b79184 100644 --- a/src/common/utils/type-narrowing.ts +++ b/src/common/utils/type-narrowing.ts @@ -107,8 +107,14 @@ export function isNull(val: unknown): val is null { * This is useful for when using `hasOptionalProperty` and `hasTypedProperty` * @param fn A typescript user predicate function to be bound * @param boundArgs the set of arguments to be passed to `fn` in the new function + * + * Example: + * ``` + * bindTypeGuard(isTypedArray, isString); // Predicate + * bindTypeGuard(isRecord, isString, isBoolean); // Predicate> + * ``` */ -export function bindTypeGuard(fn: (arg1: unknown, ...args: FnArgs) => arg1 is T, ...boundArgs: FnArgs): (arg1: unknown) => arg1 is T { +export function bindTypeGuard(fn: (arg1: unknown, ...args: FnArgs) => arg1 is T, ...boundArgs: FnArgs): Predicate { return (arg1: unknown): arg1 is T => fn(arg1, ...boundArgs); } @@ -122,6 +128,11 @@ type OrReturnPredicateType[]> = ReturnPredicateType + * ``` */ export function createUnionGuard[]>(...predicates: Predicates): Predicate> { return (arg: unknown): arg is OrReturnPredicateType => { diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 921a9126a9..a863aa319f 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -3,7 +3,7 @@ import { action, computed, observable, toJS, reaction } from "mobx"; import { BaseStore } from "./base-store"; import { clusterStore } from "./cluster-store"; import { appEventBus } from "./event-bus"; -import { broadcastMessage, handleRequest, requestMain } from "../common/ipc"; +import { broadcastMessage, createTypedInvoker, isEmptyArgs } from "../common/ipc"; import logger from "../main/logger"; import type { ClusterId } from "./cluster-store"; @@ -141,9 +141,26 @@ export class Workspace implements WorkspaceModel, WorkspaceState { } } +interface WorkspaceStateSync { + id: string; + state: WorkspaceState; +} + +function WorkspaceStoreStateHandler(): WorkspaceStateSync[] { + return clusterStore.clustersList.map(cluster => ({ + state: cluster.getState(), + id: cluster.id, + })); +} + +const workspaceStoreStateRequest = createTypedInvoker({ + channel: "workspace:states", + handler: WorkspaceStoreStateHandler, + verifier: isEmptyArgs, +}); + export class WorkspaceStore extends BaseStore { static readonly defaultId: WorkspaceId = "default"; - private static stateRequestChannel = "workspace:states"; @observable currentWorkspaceId = WorkspaceStore.defaultId; @observable workspaces = observable.map(); @@ -161,35 +178,13 @@ export class WorkspaceStore extends BaseStore { async load() { await super.load(); - type workspaceStateSync = { - id: string; - state: WorkspaceState; - }; if (ipcRenderer) { logger.info("[WORKSPACE-STORE] requesting initial state sync"); - const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel); - workspaceStates.forEach((workspaceState) => { - const workspace = this.getById(workspaceState.id); - - if (workspace) { - workspace.setState(workspaceState.state); - } - }); - } else { - handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => { - const states: workspaceStateSync[] = []; - - this.workspacesList.forEach((workspace) => { - states.push({ - state: workspace.getState(), - id: workspace.id - }); - }); - - return states; - }); + for (const { id, state } of await workspaceStoreStateRequest.invoke()) { + this.getById(id)?.setState(state); + } } } diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 0994f89995..49bafabe3f 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -5,8 +5,9 @@ import fs from "fs-extra"; import { observable, reaction, toJS, when } from "mobx"; import os from "os"; import path from "path"; -import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; +import { createTypedInvoker, createTypedSender, isEmptyArgs } from "../common/ipc"; import { getBundledExtensions } from "../common/utils/app-version"; +import { hasTypedProperty, isBoolean } from "../common/utils/type-narrowing"; import logger from "../main/logger"; import { extensionInstaller, PackageJson } from "./extension-installer"; import { extensionsStore } from "./extensions-store"; @@ -31,15 +32,35 @@ const logModule = "[EXTENSION-DISCOVERY]"; export const manifestFilename = "package.json"; -interface ExtensionDiscoveryChannelMessage { - isLoaded: boolean; +type DiscoveryLoadingState = [isLoaded: boolean]; + +function isExtensionDiscoveryChannelMessage(args: unknown[]): args is DiscoveryLoadingState { + return hasTypedProperty(args, 0, isBoolean) + && args.length === 0; } /** * Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link) * @param lstat the stats to compare */ -const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymbolicLink(); +function isDirectoryLike(lstat: fs.Stats): boolean { + return lstat.isDirectory() || lstat.isSymbolicLink(); +} + +const extensionDiscoveryState = createTypedSender({ + channel: "extension-discovery:state", + verifier: isExtensionDiscoveryChannelMessage, +}); + +function ExtensionDiscoveryInitState(): boolean { + return extensionDiscovery.isLoaded; +} + +const extensionDiscoveryInitState = createTypedInvoker({ + channel: "extension-discovery:init-state", + handler: ExtensionDiscoveryInitState, + verifier: isEmptyArgs, +}); /** * Discovers installed bundled and local extensions from the filesystem. @@ -61,9 +82,6 @@ export class ExtensionDiscovery { @observable isLoaded = false; whenLoaded = when(() => this.isLoaded); - // IPC channel to broadcast changes to extension-discovery from main - protected static readonly extensionDiscoveryChannel = "extension-discovery:main"; - public events: EventEmitter; constructor() { @@ -95,31 +113,18 @@ export class ExtensionDiscovery { */ async init() { if (ipcRenderer) { - await this.initRenderer(); + extensionDiscoveryState.on((event, isLoaded) => { + this.isLoaded = isLoaded; + }); + + this.isLoaded = await extensionDiscoveryInitState.invoke(); } else { - await this.initMain(); + reaction(() => this.toJSON(), loadingState => { + extensionDiscoveryState.broadcast(...loadingState); + }); } } - async initRenderer() { - const onMessage = ({ isLoaded }: ExtensionDiscoveryChannelMessage) => { - this.isLoaded = isLoaded; - }; - - requestMain(ExtensionDiscovery.extensionDiscoveryChannel).then(onMessage); - subscribeToBroadcast(ExtensionDiscovery.extensionDiscoveryChannel, (_event, message: ExtensionDiscoveryChannelMessage) => { - onMessage(message); - }); - } - - async initMain() { - handleRequest(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON()); - - reaction(() => this.toJSON(), () => { - this.broadcast(); - }); - } - /** * Watches for added/removed local extensions. * Dependencies are installed automatically after an extension folder is copied. @@ -450,17 +455,13 @@ export class ExtensionDiscovery { return this.getByManifest(manifestPath, { isBundled }); } - toJSON(): ExtensionDiscoveryChannelMessage { - return toJS({ - isLoaded: this.isLoaded - }, { + toJSON(): DiscoveryLoadingState { + return toJS([ + this.isLoaded + ], { recurseEverything: true }); } - - broadcast() { - broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON()); - } } export const extensionDiscovery = new ExtensionDiscovery(); diff --git a/src/main/index.ts b/src/main/index.ts index c98595fa35..1e7ee38573 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -26,8 +26,8 @@ import type { LensExtensionId } from "../extensions/lens-extension"; import { installDeveloperTools } from "./developer-tools"; import { filesystemProvisionerStore } from "./extension-filesystem"; import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; -import { bindBroadcastHandlers } from "../common/ipc"; import { startUpdateChecking } from "./app-updater"; +import "../common/ipc"; // make sure that the handlers are registered const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -66,8 +66,6 @@ app.on("ready", async () => { logger.info("🐚 Syncing shell environment"); await shellSync(); - bindBroadcastHandlers(); - powerMonitor.on("shutdown", () => { app.exit(); });