From b9de596274074764b4b4f10cf6cf114a5ef22860 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 15 Jul 2021 11:28:17 -0400 Subject: [PATCH] Use declaritive system for creating singletons - Let the code say the requirements of each singleton is instead of comments Signed-off-by: Sebastian Malton --- src/common/utils/singleton.ts | 122 ++++++++++++++++++ src/extensions/extension-discovery.ts | 10 +- src/extensions/extension-loader.ts | 32 ++--- src/main/catalog-sources/weblinks.ts | 5 +- src/main/index.ts | 68 +++++----- .../cluster-metadata-detectors.ts | 6 +- src/main/initializers/metrics-providers.ts | 7 +- src/renderer/bootstrap.tsx | 97 +++++--------- src/renderer/initializers/index.ts | 39 ++++-- 9 files changed, 245 insertions(+), 141 deletions(-) diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts index ca8eb3eb61..e98104457d 100644 --- a/src/common/utils/singleton.ts +++ b/src/common/utils/singleton.ts @@ -19,6 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + type StaticThis = { new(...args: R): T }; export class Singleton { @@ -88,3 +89,124 @@ export class Singleton { } export default Singleton; + +export interface DeclareOpts { + requires?: Iterable, +} + +type Initializer = (instance: T) => void; + +interface CreateRequirements { + builds: typeof Singleton, + args?: ConstructorParameters | (() => ConstructorParameters), + requires?: Set, + init?: (single: Singleton) => Promise; +} + +/** + * A builder class for declaring the dependency graph of a set of singletons + * + * This is useful because it allows the code to declare the requirements + */ +export class CreateSingletons { + #requirements: CreateRequirements[] = []; + + private constructor() { + // + } + + static begin(): CreateSingletons { + return new CreateSingletons(); + } + + /** + * Add a Singleton that requires no arguments to build + * @param builds The constructor which must be a type that extends Singleton + * @param opts The optional singletons that must be required before this can be created + * @returns the builder + */ + declare(builds: StaticThis, opts?: DeclareOpts): this; + declare(builds: StaticThis, init?: Initializer, opts?: DeclareOpts): this; + declare(builds: StaticThis, initOrOpts?: Initializer | DeclareOpts, opts?: DeclareOpts): this { + if (typeof initOrOpts === "function") { + const { requires } = opts ?? {}; + + this.#requirements.push({ + builds: builds as any, + requires: new Set(requires), + init: initOrOpts as any, + }); + } else { + const { requires } = initOrOpts ?? {}; + + this.#requirements.push({ + builds: builds as any, + requires: new Set(requires), + }); + } + + return this; + } + + /** + * Like `declare` but with the ability to pass in arguments for the constructor + */ + declareWithArgs(builds: StaticThis, args: R | (() => R), opts?: DeclareOpts): this; + declareWithArgs(builds: StaticThis, args: R | (() => R), init?: Initializer, opts?: DeclareOpts): this; + declareWithArgs(builds: StaticThis, args: R | (() => R), initOrOpts?: Initializer | DeclareOpts, opts?: DeclareOpts): this { + if (typeof initOrOpts === "function") { + const { requires } = opts ?? {}; + + this.#requirements.push({ + builds: builds as any, + args: args as any, + requires: new Set(requires), + init: initOrOpts as any, + }); + } else { + const { requires } = initOrOpts ?? {}; + + this.#requirements.push({ + builds: builds as any, + args: args as any, + requires: new Set(requires), + }); + } + + return this; + } + + buildAll(): void { + while (this.#requirements.length > 0) { + const nextSatisfiedIndex = this.#requirements.findIndex(creation => creation.requires.size === 0); + + if (nextSatisfiedIndex < 0) { + throw new Error("Circular dependency for Singleton creation"); + } + + const [{ builds, args, init }] = this.#requirements.splice(nextSatisfiedIndex, 1); + + if (builds.createInstance !== Singleton.createInstance) { + throw new TypeError("Builds is not T extends Singleton"); + } + + if (args) { + const resolvedArgs = typeof args === "function" + ? args() + : args; + + builds.createInstance(...resolvedArgs); + } else { + builds.createInstance(); + } + + init?.(builds.getInstance()); + + for (const requirement of this.#requirements) { + requirement.requires.delete(builds); + } + } + + this.#requirements.length = 0; + } +} diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index dbaeb0142d..cbe4ad7c39 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -128,15 +128,15 @@ export class ExtensionDiscovery extends Singleton { /** * Initializes the class and setups the file watcher for added/removed local extensions. */ - async init(): Promise { + init(): void { if (ipcRenderer) { - await this.initRenderer(); + this.initRenderer(); } else { - await this.initMain(); + this.initMain(); } } - async initRenderer(): Promise { + initRenderer(): void { const onMessage = ({ isLoaded }: ExtensionDiscoveryChannelMessage) => { this.isLoaded = isLoaded; }; @@ -147,7 +147,7 @@ export class ExtensionDiscovery extends Singleton { }); } - async initMain(): Promise { + initMain(): void { ipcMainHandle(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON()); reaction(() => this.toJSON(), () => { diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 8d249355c3..3ce52acfb4 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -72,10 +72,6 @@ export class ExtensionLoader extends Singleton { @observable isLoaded = false; - get whenLoaded() { - return when(() => this.isLoaded); - } - constructor() { super(); makeObservable(this); @@ -138,23 +134,23 @@ export class ExtensionLoader extends Singleton { } @action - async init() { + init() { if (ipcRenderer) { - await this.initRenderer(); + this.initRenderer(); } else { - await this.initMain(); + this.initMain(); } - await Promise.all([this.whenLoaded]); + when(() => this.isLoaded, () => { + // broadcasting extensions between main/renderer processes + reaction(() => this.toJSON(), () => this.broadcastExtensions(), { + fireImmediately: true, + }); - // broadcasting extensions between main/renderer processes - reaction(() => this.toJSON(), () => this.broadcastExtensions(), { - fireImmediately: true, - }); - - // save state on change `extension.isEnabled` - reaction(() => this.storeState, extensionsState => { - ExtensionsStore.getInstance().mergeState(extensionsState); + // save state on change `extension.isEnabled` + reaction(() => this.storeState, extensionsState => { + ExtensionsStore.getInstance().mergeState(extensionsState); + }); }); } @@ -197,7 +193,7 @@ export class ExtensionLoader extends Singleton { this.extensions.get(lensExtensionId).isEnabled = isEnabled; } - protected async initMain() { + protected initMain() { this.isLoaded = true; this.loadOnMain(); @@ -210,7 +206,7 @@ export class ExtensionLoader extends Singleton { }); } - protected async initRenderer() { + protected initRenderer() { const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => { this.isLoaded = true; this.syncExtensions(extensions); diff --git a/src/main/catalog-sources/weblinks.ts b/src/main/catalog-sources/weblinks.ts index 7c7917aa1d..6a49f59bb5 100644 --- a/src/main/catalog-sources/weblinks.ts +++ b/src/main/catalog-sources/weblinks.ts @@ -20,7 +20,7 @@ */ import { computed, observable, reaction } from "mobx"; -import { WeblinkStore } from "../../common/weblink-store"; +import type { WeblinkStore } from "../../common/weblink-store"; import { WebLink } from "../../common/catalog-entities"; import { catalogEntityRegistry } from "../catalog"; import got from "got"; @@ -45,8 +45,7 @@ async function validateLink(link: WebLink) { } -export function syncWeblinks() { - const weblinkStore = WeblinkStore.getInstance(); +export function syncWeblinks(weblinkStore: WeblinkStore) { const webLinkEntities = observable.map(); function periodicallyCheckLink(link: WebLink): Disposer { diff --git a/src/main/index.ts b/src/main/index.ts index 1c5b80042c..3bd2cc8122 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -41,7 +41,7 @@ import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension- import type { LensExtensionId } from "../extensions/lens-extension"; import { installDeveloperTools } from "./developer-tools"; import { LensProtocolRouterMain } from "./protocol-handler"; -import { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils"; +import { CreateSingletons, disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils"; import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc"; import { startUpdateChecking } from "./app-updater"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; @@ -66,6 +66,7 @@ import { initTray } from "./tray"; import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions"; import { AppPaths } from "../common/app-paths"; import { ShellSession } from "./shell-session/shell-session"; +import { DetectorRegistry } from "./cluster-detectors/detector-registry"; injectSystemCAs(); @@ -144,10 +145,6 @@ app.on("ready", async () => { registerFileProtocol("static", __static); - PrometheusProviderRegistry.createInstance(); - ShellRequestAuthenticator.createInstance().init(); - initializers.initPrometheusProviderRegistry(); - /** * The following sync MUST be done before HotbarStore creation, because that * store has migrations that will remove items that previous migrations add @@ -157,32 +154,38 @@ app.on("ready", async () => { logger.info("💾 Loading stores"); - UserStore.createInstance().startMainReactions(); + CreateSingletons.begin() + .declare(ClusterManager, manager => manager.init(), { + requires: [ClusterStore], + }) + .declare(KubeconfigSyncManager) + .declare(ShellRequestAuthenticator, auth => auth.init()) + .declareWithArgs(LensProxy, [ + new Router(), + { + getClusterForRequest: req => ClusterManager.getInstance().getClusterForRequest(req), + kubeApiRequest, + shellApiRequest, + }, + ]) + .declare(ClusterStore, store => store.provideInitialFromMain(), { + requires: [UserStore], + }) + .declare(HotbarStore, { + requires: [ClusterStore], + }) + .declare(PrometheusProviderRegistry, initializers.initPrometheusProviderRegistry) + .declare(UserStore, store => store.startMainReactions()) + .declare(ExtensionsStore) + .declare(DetectorRegistry, initializers.initClusterMetadataDetectors) + .declare(FilesystemProvisionerStore) + .declare(WeblinkStore, syncWeblinks) + .declare(HelmRepoManager) + .declare(ExtensionDiscovery) + .declare(ExtensionLoader) + .buildAll(); - // ClusterStore depends on: UserStore - ClusterStore.createInstance().provideInitialFromMain(); - - // HotbarStore depends on: ClusterStore - HotbarStore.createInstance(); - - ExtensionsStore.createInstance(); - FilesystemProvisionerStore.createInstance(); - WeblinkStore.createInstance(); - - syncWeblinks(); - - HelmRepoManager.createInstance(); // create the instance - - const lensProxy = LensProxy.createInstance(new Router(), { - getClusterForRequest: req => ClusterManager.getInstance().getClusterForRequest(req), - kubeApiRequest, - shellApiRequest, - }); - - ClusterManager.createInstance().init(); - KubeconfigSyncManager.createInstance(); - - initializers.initClusterMetadataDetectors(); + const lensProxy = LensProxy.getInstance(); try { logger.info("🔌 Starting LensProxy"); @@ -224,9 +227,10 @@ app.on("ready", async () => { } initializers.initRegistries(); - const extensionDiscovery = ExtensionDiscovery.createInstance(); + ExtensionLoader.getInstance().init(); + + const extensionDiscovery = ExtensionDiscovery.getInstance(); - ExtensionLoader.createInstance().init(); extensionDiscovery.init(); // Start the app without showing the main window when auto starting on login diff --git a/src/main/initializers/cluster-metadata-detectors.ts b/src/main/initializers/cluster-metadata-detectors.ts index 47942cd28e..b15bd10c2d 100644 --- a/src/main/initializers/cluster-metadata-detectors.ts +++ b/src/main/initializers/cluster-metadata-detectors.ts @@ -20,14 +20,14 @@ */ import { ClusterIdDetector } from "../cluster-detectors/cluster-id-detector"; -import { DetectorRegistry } from "../cluster-detectors/detector-registry"; +import type { DetectorRegistry } from "../cluster-detectors/detector-registry"; import { DistributionDetector } from "../cluster-detectors/distribution-detector"; import { LastSeenDetector } from "../cluster-detectors/last-seen-detector"; import { NodesCountDetector } from "../cluster-detectors/nodes-count-detector"; import { VersionDetector } from "../cluster-detectors/version-detector"; -export function initClusterMetadataDetectors() { - DetectorRegistry.createInstance() +export function initClusterMetadataDetectors(registry: DetectorRegistry) { + registry .add(ClusterIdDetector) .add(LastSeenDetector) .add(VersionDetector) diff --git a/src/main/initializers/metrics-providers.ts b/src/main/initializers/metrics-providers.ts index 3d952f1859..ad74d1c36a 100644 --- a/src/main/initializers/metrics-providers.ts +++ b/src/main/initializers/metrics-providers.ts @@ -19,15 +19,14 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { PrometheusProviderRegistry } from "../prometheus"; +import type { PrometheusProviderRegistry } from "../prometheus"; import { PrometheusHelm } from "../prometheus/helm"; import { PrometheusLens } from "../prometheus/lens"; import { PrometheusOperator } from "../prometheus/operator"; import { PrometheusStacklight } from "../prometheus/stacklight"; -export function initPrometheusProviderRegistry() { - PrometheusProviderRegistry - .getInstance() +export function initPrometheusProviderRegistry(registry: PrometheusProviderRegistry) { + registry .registerProvider(new PrometheusLens()) .registerProvider(new PrometheusHelm()) .registerProvider(new PrometheusOperator()) diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index f42a671de6..40c304a3ef 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -29,17 +29,16 @@ import * as ReactRouterDom from "react-router-dom"; import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsRendererApi from "../extensions/renderer-api"; import { render } from "react-dom"; -import { delay } from "../common/utils"; +import { CreateSingletons, delay } from "../common/utils"; import { isMac, isDevelopment } from "../common/vars"; import { ClusterStore } from "../common/cluster-store"; import { UserStore } from "../common/user-store"; import { ExtensionDiscovery } from "../extensions/extension-discovery"; import { ExtensionLoader } from "../extensions/extension-loader"; import { HelmRepoManager } from "../main/helm/helm-repo-manager"; -import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store"; import { DefaultProps } from "./mui-base-theme"; import configurePackages from "../common/configure-packages"; -import * as initializers from "./initializers"; +import { initializers } from "./initializers"; import logger from "../common/logger"; import { HotbarStore } from "../common/hotbar-store"; import { WeblinkStore } from "../common/weblink-store"; @@ -53,6 +52,7 @@ import { registerCustomThemes } from "./components/monaco-editor"; import { getDi } from "./components/getDi"; import { DiContextProvider } from "@ogre-tools/injectable-react"; import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; +import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store"; if (process.isMainFrame) { SentryInit(); @@ -80,73 +80,42 @@ export async function bootstrap(comp: () => Promise, di: Dependenc const rootElem = document.getElementById("app"); const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`; - await AppPaths.init(); - UserStore.createInstance(); - await attachChromeDebugger(); rootElem.classList.toggle("is-mac", isMac); - logger.info(`${logPrefix} initializing Registries`); - initializers.initRegistries(); + for (const [name, init] of initializers) { + logger.info(`${logPrefix} initializing ${name}`); + init(); + } - logger.info(`${logPrefix} initializing CommandRegistry`); - initializers.initCommandRegistry(); - - logger.info(`${logPrefix} initializing EntitySettingsRegistry`); - initializers.initEntitySettingsRegistry(); - - logger.info(`${logPrefix} initializing KubeObjectMenuRegistry`); - initializers.initKubeObjectMenuRegistry(); - - logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`); - initializers.initKubeObjectDetailRegistry(); - - logger.info(`${logPrefix} initializing WelcomeMenuRegistry`); - initializers.initWelcomeMenuRegistry(); - - logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`); - initializers.initWorkloadsOverviewDetailRegistry(); - - logger.info(`${logPrefix} initializing CatalogEntityDetailRegistry`); - initializers.initCatalogEntityDetailRegistry(); - - logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntries`); - initializers.initCatalogCategoryRegistryEntries(); - - logger.info(`${logPrefix} initializing Catalog`); - initializers.initCatalog(); - - logger.info(`${logPrefix} initializing IpcRendererListeners`); - initializers.initIpcRendererListeners(); - - logger.info(`${logPrefix} initializing StatusBarRegistry`); - initializers.initStatusBarRegistry(); - - ExtensionLoader.createInstance().init(); - ExtensionDiscovery.createInstance().init(); - - // ClusterStore depends on: UserStore - const clusterStore = ClusterStore.createInstance(); - - await clusterStore.loadInitialOnRenderer(); - - // HotbarStore depends on: ClusterStore - HotbarStore.createInstance(); - ExtensionsStore.createInstance(); - FilesystemProvisionerStore.createInstance(); - - // ThemeStore depends on: UserStore - ThemeStore.createInstance(); - - // TerminalStore depends on: ThemeStore - TerminalStore.createInstance(); - WeblinkStore.createInstance(); + await AppPaths.init(); + CreateSingletons.begin() + .declare(ExtensionDiscovery, discovery => discovery.init()) + .declare(ExtensionLoader, loader => loader.init()) + .declare(UserStore) + .declare(ExtensionsStore) + .declare(FilesystemProvisionerStore) + .declare(WeblinkStore) + .declare(HelmRepoManager) + .declare(ClusterStore, async store => { + await store.loadInitialOnRenderer(); + store.registerIpcListener(); + }, + { + requires: [UserStore], + }) + .declare(HotbarStore, { + requires: [ClusterStore], + }) + .declare(ThemeStore, { + requires: [UserStore], + }) + .declare(TerminalStore, { + requires: [ThemeStore], + }) + .buildAll(); ExtensionInstallationStateStore.bindIpcListeners(); - HelmRepoManager.createInstance(); // initialize the manager - - // Register additional store listeners - clusterStore.registerIpcListener(); // init app's dependencies if any const App = await comp(); diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index 55d1bbbb1b..c1c7ea902e 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -19,15 +19,30 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export * from "./catalog-entity-detail-registry"; -export * from "./catalog"; -export * from "./command-registry"; -export * from "./entity-settings-registry"; -export * from "./ipc"; -export * from "./kube-object-detail-registry"; -export * from "./kube-object-menu-registry"; -export * from "./registries"; -export * from "./welcome-menu-registry"; -export * from "./workloads-overview-detail-registry"; -export * from "./catalog-category-registry"; -export * from "./status-bar-registry"; +import { initCatalogEntityDetailRegistry } from "./catalog-entity-detail-registry"; +import { initCatalog } from "./catalog"; +import { initCommandRegistry } from "./command-registry"; +import { initEntitySettingsRegistry } from "./entity-settings-registry"; +import { initIpcRendererListeners } from "./ipc"; +import { initKubeObjectDetailRegistry } from "./kube-object-detail-registry"; +import { initKubeObjectMenuRegistry } from "./kube-object-menu-registry"; +import { initRegistries } from "./registries"; +import { initWelcomeMenuRegistry } from "./welcome-menu-registry"; +import { initWorkloadsOverviewDetailRegistry } from "./workloads-overview-detail-registry"; +import { initCatalogCategoryRegistryEntries } from "./catalog-category-registry"; +import { initStatusBarRegistry } from "./status-bar-registry"; + +export const initializers: [string, () => void][] = [ + ["Registries", initRegistries], // This line must be must be first + ["CommandRegistry", initCommandRegistry], + ["EntitySettingsRegistry", initEntitySettingsRegistry], + ["KubeObjectMenuRegistry", initKubeObjectMenuRegistry], + ["KubeObjectDetailRegistry", initKubeObjectDetailRegistry], + ["WelcomeMenuRegistry", initWelcomeMenuRegistry], + ["WorkloadsOverviewDetailRegistry", initWorkloadsOverviewDetailRegistry], + ["CatalogEntityDetailRegistry", initCatalogEntityDetailRegistry], + ["CatalogCategoryRegistryEntries", initCatalogCategoryRegistryEntries], + ["Catalog", initCatalog], + ["IpcRendererListeners", initIpcRendererListeners], + ["StatusBarRegistry", initStatusBarRegistry], +];