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.
*/
type StaticThis<T, R extends any[]> = { new(...args: R): T };
export class Singleton {
@ -88,3 +89,124 @@ export class 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.
*/
async init(): Promise<void> {
init(): void {
if (ipcRenderer) {
await this.initRenderer();
this.initRenderer();
} else {
await this.initMain();
this.initMain();
}
}
async initRenderer(): Promise<void> {
initRenderer(): void {
const onMessage = ({ isLoaded }: ExtensionDiscoveryChannelMessage) => {
this.isLoaded = isLoaded;
};
@ -147,7 +147,7 @@ export class ExtensionDiscovery extends Singleton {
});
}
async initMain(): Promise<void> {
initMain(): void {
ipcMainHandle(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON());
reaction(() => this.toJSON(), () => {

View File

@ -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);

View File

@ -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<string, [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 { 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

View File

@ -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)

View File

@ -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())

View File

@ -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<AppComponent>, 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();

View File

@ -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],
];