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

Use declaritive system for creating singletons

- Let the code say the requirements of each singleton is instead of
  comments

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-07-15 11:28:17 -04:00
parent d73df7fe0d
commit b9de596274
9 changed files with 245 additions and 141 deletions

View File

@ -19,6 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
type StaticThis<T, R extends any[]> = { new(...args: R): T }; type StaticThis<T, R extends any[]> = { new(...args: R): T };
export class Singleton { export class Singleton {
@ -88,3 +89,124 @@ export class Singleton {
} }
export default Singleton; export default Singleton;
export interface DeclareOpts {
requires?: Iterable<typeof Singleton>,
}
type Initializer<T> = (instance: T) => void;
interface CreateRequirements {
builds: typeof Singleton,
args?: ConstructorParameters<typeof Singleton> | (() => ConstructorParameters<typeof Singleton>),
requires?: Set<typeof Singleton>,
init?: (single: Singleton) => Promise<void>;
}
/**
* 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<T>(builds: StaticThis<T, []>, opts?: DeclareOpts): this;
declare<T>(builds: StaticThis<T, []>, init?: Initializer<T>, opts?: DeclareOpts): this;
declare<T>(builds: StaticThis<T, []>, initOrOpts?: Initializer<T> | 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<T, R extends any[]>(builds: StaticThis<T, R>, args: R | (() => R), opts?: DeclareOpts): this;
declareWithArgs<T, R extends any[]>(builds: StaticThis<T, R>, args: R | (() => R), init?: Initializer<T>, opts?: DeclareOpts): this;
declareWithArgs<T, R extends any[]>(builds: StaticThis<T, R>, args: R | (() => R), initOrOpts?: Initializer<T> | 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;
}
}

View File

@ -128,15 +128,15 @@ export class ExtensionDiscovery extends Singleton {
/** /**
* Initializes the class and setups the file watcher for added/removed local extensions. * Initializes the class and setups the file watcher for added/removed local extensions.
*/ */
async init(): Promise<void> { init(): void {
if (ipcRenderer) { if (ipcRenderer) {
await this.initRenderer(); this.initRenderer();
} else { } else {
await this.initMain(); this.initMain();
} }
} }
async initRenderer(): Promise<void> { initRenderer(): void {
const onMessage = ({ isLoaded }: ExtensionDiscoveryChannelMessage) => { const onMessage = ({ isLoaded }: ExtensionDiscoveryChannelMessage) => {
this.isLoaded = isLoaded; this.isLoaded = isLoaded;
}; };
@ -147,7 +147,7 @@ export class ExtensionDiscovery extends Singleton {
}); });
} }
async initMain(): Promise<void> { initMain(): void {
ipcMainHandle(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON()); ipcMainHandle(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON());
reaction(() => this.toJSON(), () => { reaction(() => this.toJSON(), () => {

View File

@ -72,10 +72,6 @@ export class ExtensionLoader extends Singleton {
@observable isLoaded = false; @observable isLoaded = false;
get whenLoaded() {
return when(() => this.isLoaded);
}
constructor() { constructor() {
super(); super();
makeObservable(this); makeObservable(this);
@ -138,23 +134,23 @@ export class ExtensionLoader extends Singleton {
} }
@action @action
async init() { init() {
if (ipcRenderer) { if (ipcRenderer) {
await this.initRenderer(); this.initRenderer();
} else { } 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 // save state on change `extension.isEnabled`
reaction(() => this.toJSON(), () => this.broadcastExtensions(), { reaction(() => this.storeState, extensionsState => {
fireImmediately: true, 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; this.extensions.get(lensExtensionId).isEnabled = isEnabled;
} }
protected async initMain() { protected initMain() {
this.isLoaded = true; this.isLoaded = true;
this.loadOnMain(); this.loadOnMain();
@ -210,7 +206,7 @@ export class ExtensionLoader extends Singleton {
}); });
} }
protected async initRenderer() { protected initRenderer() {
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => { const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
this.isLoaded = true; this.isLoaded = true;
this.syncExtensions(extensions); this.syncExtensions(extensions);

View File

@ -20,7 +20,7 @@
*/ */
import { computed, observable, reaction } from "mobx"; 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 { WebLink } from "../../common/catalog-entities";
import { catalogEntityRegistry } from "../catalog"; import { catalogEntityRegistry } from "../catalog";
import got from "got"; import got from "got";
@ -45,8 +45,7 @@ async function validateLink(link: WebLink) {
} }
export function syncWeblinks() { export function syncWeblinks(weblinkStore: WeblinkStore) {
const weblinkStore = WeblinkStore.getInstance();
const webLinkEntities = observable.map<string, [WebLink, Disposer]>(); const webLinkEntities = observable.map<string, [WebLink, Disposer]>();
function periodicallyCheckLink(link: WebLink): Disposer { function periodicallyCheckLink(link: WebLink): Disposer {

View File

@ -41,7 +41,7 @@ import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-
import type { LensExtensionId } from "../extensions/lens-extension"; import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools"; import { installDeveloperTools } from "./developer-tools";
import { LensProtocolRouterMain } from "./protocol-handler"; 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 { bindBroadcastHandlers, ipcMainOn } from "../common/ipc";
import { startUpdateChecking } from "./app-updater"; import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
@ -66,6 +66,7 @@ import { initTray } from "./tray";
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions"; import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
import { AppPaths } from "../common/app-paths"; import { AppPaths } from "../common/app-paths";
import { ShellSession } from "./shell-session/shell-session"; import { ShellSession } from "./shell-session/shell-session";
import { DetectorRegistry } from "./cluster-detectors/detector-registry";
injectSystemCAs(); injectSystemCAs();
@ -144,10 +145,6 @@ app.on("ready", async () => {
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
PrometheusProviderRegistry.createInstance();
ShellRequestAuthenticator.createInstance().init();
initializers.initPrometheusProviderRegistry();
/** /**
* The following sync MUST be done before HotbarStore creation, because that * The following sync MUST be done before HotbarStore creation, because that
* store has migrations that will remove items that previous migrations add * store has migrations that will remove items that previous migrations add
@ -157,32 +154,38 @@ app.on("ready", async () => {
logger.info("💾 Loading stores"); 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 const lensProxy = LensProxy.getInstance();
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();
try { try {
logger.info("🔌 Starting LensProxy"); logger.info("🔌 Starting LensProxy");
@ -224,9 +227,10 @@ app.on("ready", async () => {
} }
initializers.initRegistries(); initializers.initRegistries();
const extensionDiscovery = ExtensionDiscovery.createInstance(); ExtensionLoader.getInstance().init();
const extensionDiscovery = ExtensionDiscovery.getInstance();
ExtensionLoader.createInstance().init();
extensionDiscovery.init(); extensionDiscovery.init();
// Start the app without showing the main window when auto starting on login // Start the app without showing the main window when auto starting on login

View File

@ -20,14 +20,14 @@
*/ */
import { ClusterIdDetector } from "../cluster-detectors/cluster-id-detector"; 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 { DistributionDetector } from "../cluster-detectors/distribution-detector";
import { LastSeenDetector } from "../cluster-detectors/last-seen-detector"; import { LastSeenDetector } from "../cluster-detectors/last-seen-detector";
import { NodesCountDetector } from "../cluster-detectors/nodes-count-detector"; import { NodesCountDetector } from "../cluster-detectors/nodes-count-detector";
import { VersionDetector } from "../cluster-detectors/version-detector"; import { VersionDetector } from "../cluster-detectors/version-detector";
export function initClusterMetadataDetectors() { export function initClusterMetadataDetectors(registry: DetectorRegistry) {
DetectorRegistry.createInstance() registry
.add(ClusterIdDetector) .add(ClusterIdDetector)
.add(LastSeenDetector) .add(LastSeenDetector)
.add(VersionDetector) .add(VersionDetector)

View File

@ -19,15 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { PrometheusHelm } from "../prometheus/helm";
import { PrometheusLens } from "../prometheus/lens"; import { PrometheusLens } from "../prometheus/lens";
import { PrometheusOperator } from "../prometheus/operator"; import { PrometheusOperator } from "../prometheus/operator";
import { PrometheusStacklight } from "../prometheus/stacklight"; import { PrometheusStacklight } from "../prometheus/stacklight";
export function initPrometheusProviderRegistry() { export function initPrometheusProviderRegistry(registry: PrometheusProviderRegistry) {
PrometheusProviderRegistry registry
.getInstance()
.registerProvider(new PrometheusLens()) .registerProvider(new PrometheusLens())
.registerProvider(new PrometheusHelm()) .registerProvider(new PrometheusHelm())
.registerProvider(new PrometheusOperator()) .registerProvider(new PrometheusOperator())

View File

@ -29,17 +29,16 @@ import * as ReactRouterDom from "react-router-dom";
import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsRendererApi from "../extensions/renderer-api"; import * as LensExtensionsRendererApi from "../extensions/renderer-api";
import { render } from "react-dom"; import { render } from "react-dom";
import { delay } from "../common/utils"; import { CreateSingletons, delay } from "../common/utils";
import { isMac, isDevelopment } from "../common/vars"; import { isMac, isDevelopment } from "../common/vars";
import { ClusterStore } from "../common/cluster-store"; import { ClusterStore } from "../common/cluster-store";
import { UserStore } from "../common/user-store"; import { UserStore } from "../common/user-store";
import { ExtensionDiscovery } from "../extensions/extension-discovery"; import { ExtensionDiscovery } from "../extensions/extension-discovery";
import { ExtensionLoader } from "../extensions/extension-loader"; import { ExtensionLoader } from "../extensions/extension-loader";
import { HelmRepoManager } from "../main/helm/helm-repo-manager"; import { HelmRepoManager } from "../main/helm/helm-repo-manager";
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
import { DefaultProps } from "./mui-base-theme"; import { DefaultProps } from "./mui-base-theme";
import configurePackages from "../common/configure-packages"; import configurePackages from "../common/configure-packages";
import * as initializers from "./initializers"; import { initializers } from "./initializers";
import logger from "../common/logger"; import logger from "../common/logger";
import { HotbarStore } from "../common/hotbar-store"; import { HotbarStore } from "../common/hotbar-store";
import { WeblinkStore } from "../common/weblink-store"; import { WeblinkStore } from "../common/weblink-store";
@ -53,6 +52,7 @@ import { registerCustomThemes } from "./components/monaco-editor";
import { getDi } from "./components/getDi"; import { getDi } from "./components/getDi";
import { DiContextProvider } from "@ogre-tools/injectable-react"; import { DiContextProvider } from "@ogre-tools/injectable-react";
import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
if (process.isMainFrame) { if (process.isMainFrame) {
SentryInit(); SentryInit();
@ -80,73 +80,42 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
const rootElem = document.getElementById("app"); const rootElem = document.getElementById("app");
const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`; const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`;
await AppPaths.init();
UserStore.createInstance();
await attachChromeDebugger(); await attachChromeDebugger();
rootElem.classList.toggle("is-mac", isMac); rootElem.classList.toggle("is-mac", isMac);
logger.info(`${logPrefix} initializing Registries`); for (const [name, init] of initializers) {
initializers.initRegistries(); logger.info(`${logPrefix} initializing ${name}`);
init();
}
logger.info(`${logPrefix} initializing CommandRegistry`); await AppPaths.init();
initializers.initCommandRegistry(); CreateSingletons.begin()
.declare(ExtensionDiscovery, discovery => discovery.init())
logger.info(`${logPrefix} initializing EntitySettingsRegistry`); .declare(ExtensionLoader, loader => loader.init())
initializers.initEntitySettingsRegistry(); .declare(UserStore)
.declare(ExtensionsStore)
logger.info(`${logPrefix} initializing KubeObjectMenuRegistry`); .declare(FilesystemProvisionerStore)
initializers.initKubeObjectMenuRegistry(); .declare(WeblinkStore)
.declare(HelmRepoManager)
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`); .declare(ClusterStore, async store => {
initializers.initKubeObjectDetailRegistry(); await store.loadInitialOnRenderer();
store.registerIpcListener();
logger.info(`${logPrefix} initializing WelcomeMenuRegistry`); },
initializers.initWelcomeMenuRegistry(); {
requires: [UserStore],
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`); })
initializers.initWorkloadsOverviewDetailRegistry(); .declare(HotbarStore, {
requires: [ClusterStore],
logger.info(`${logPrefix} initializing CatalogEntityDetailRegistry`); })
initializers.initCatalogEntityDetailRegistry(); .declare(ThemeStore, {
requires: [UserStore],
logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntries`); })
initializers.initCatalogCategoryRegistryEntries(); .declare(TerminalStore, {
requires: [ThemeStore],
logger.info(`${logPrefix} initializing Catalog`); })
initializers.initCatalog(); .buildAll();
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();
ExtensionInstallationStateStore.bindIpcListeners(); ExtensionInstallationStateStore.bindIpcListeners();
HelmRepoManager.createInstance(); // initialize the manager
// Register additional store listeners
clusterStore.registerIpcListener();
// init app's dependencies if any // init app's dependencies if any
const App = await comp(); const App = await comp();

View File

@ -19,15 +19,30 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export * from "./catalog-entity-detail-registry"; import { initCatalogEntityDetailRegistry } from "./catalog-entity-detail-registry";
export * from "./catalog"; import { initCatalog } from "./catalog";
export * from "./command-registry"; import { initCommandRegistry } from "./command-registry";
export * from "./entity-settings-registry"; import { initEntitySettingsRegistry } from "./entity-settings-registry";
export * from "./ipc"; import { initIpcRendererListeners } from "./ipc";
export * from "./kube-object-detail-registry"; import { initKubeObjectDetailRegistry } from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry"; import { initKubeObjectMenuRegistry } from "./kube-object-menu-registry";
export * from "./registries"; import { initRegistries } from "./registries";
export * from "./welcome-menu-registry"; import { initWelcomeMenuRegistry } from "./welcome-menu-registry";
export * from "./workloads-overview-detail-registry"; import { initWorkloadsOverviewDetailRegistry } from "./workloads-overview-detail-registry";
export * from "./catalog-category-registry"; import { initCatalogCategoryRegistryEntries } from "./catalog-category-registry";
export * from "./status-bar-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],
];