Fixing Singleton typing to correctly return child class (#1914)

- Add distinction between `getInstance` and `getInstanceOrCreate` since
  it is not always possible to create an instance (since you might not
  know the correct arguments)

- Remove all the `export const *Store = *Store.getInstance<*Store>();`
  calls as it defeats the purpose of `Singleton`. Plus with the typing
  changes the appropriate `*Store.getInstance()` is "short enough".

- Special case the two extension export facades to not need to use
  `getInstanceOrCreate`. Plus since they are just facades it is always
  possible to create them.

- Move some other types to be also `Singleton`'s: ExtensionLoader,
  ExtensionDiscovery, ThemeStore, LocalizationStore, ...

- Fixed dev-run always using the same port with electron inspect

- Update Store documentation with new recommendations about creating
  instances of singletons

- Fix all unit tests to create their dependent singletons

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-04-21 09:59:59 -04:00
1 parent b2a570ce28
commit 9563ead2e6
114 files changed
+924 -736

No files matched your search

+71 -42
View File
@@ -4,8 +4,9 @@ import yaml from "js-yaml";
import { Cluster } from "../../main/cluster";
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(process.stdout, process.stderr); // fix mockFS
console = new Console(stdout, stderr);
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
const kubeconfig = `
@@ -47,10 +48,8 @@ jest.mock("electron", () => {
};
});
let clusterStore: ClusterStore;
describe("empty config", () => {
beforeEach(() => {
beforeEach(async () => {
ClusterStore.resetInstance();
const mockOpts = {
"tmp": {
@@ -59,9 +58,8 @@ describe("empty config", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
await ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -70,7 +68,7 @@ describe("empty config", () => {
describe("with foo cluster added", () => {
beforeEach(() => {
clusterStore.addCluster(
ClusterStore.getInstance().addCluster(
new Cluster({
id: "foo",
contextName: "foo",
@@ -85,7 +83,7 @@ describe("empty config", () => {
});
it("adds new cluster to store", async () => {
const storedCluster = clusterStore.getById("foo");
const storedCluster = ClusterStore.getInstance().getById("foo");
expect(storedCluster.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
@@ -94,19 +92,19 @@ describe("empty config", () => {
});
it("removes cluster from store", async () => {
await clusterStore.removeById("foo");
expect(clusterStore.getById("foo")).toBeNull();
await ClusterStore.getInstance().removeById("foo");
expect(ClusterStore.getInstance().getById("foo")).toBeNull();
});
it("sets active cluster", () => {
clusterStore.setActive("foo");
expect(clusterStore.active.id).toBe("foo");
ClusterStore.getInstance().setActive("foo");
expect(ClusterStore.getInstance().active.id).toBe("foo");
});
});
describe("with prod and dev clusters added", () => {
beforeEach(() => {
clusterStore.addClusters(
ClusterStore.getInstance().addClusters(
new Cluster({
id: "prod",
contextName: "foo",
@@ -127,8 +125,8 @@ describe("empty config", () => {
});
it("check if store can contain multiple clusters", () => {
expect(clusterStore.hasClusters()).toBeTruthy();
expect(clusterStore.clusters.size).toBe(2);
expect(ClusterStore.getInstance().hasClusters()).toBeTruthy();
expect(ClusterStore.getInstance().clusters.size).toBe(2);
});
it("check if cluster's kubeconfig file saved", () => {
@@ -178,9 +176,8 @@ describe("config with existing clusters", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -188,24 +185,24 @@ describe("config with existing clusters", () => {
});
it("allows to retrieve a cluster", () => {
const storedCluster = clusterStore.getById("cluster1");
const storedCluster = ClusterStore.getInstance().getById("cluster1");
expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
});
it("allows to delete a cluster", () => {
clusterStore.removeById("cluster2");
const storedCluster = clusterStore.getById("cluster1");
ClusterStore.getInstance().removeById("cluster2");
const storedCluster = ClusterStore.getInstance().getById("cluster1");
expect(storedCluster).toBeTruthy();
const storedCluster2 = clusterStore.getById("cluster2");
const storedCluster2 = ClusterStore.getInstance().getById("cluster2");
expect(storedCluster2).toBeNull();
});
it("allows getting all of the clusters", async () => {
const storedClusters = clusterStore.clustersList;
const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1");
@@ -216,7 +213,7 @@ describe("config with existing clusters", () => {
});
it("marks owned cluster disabled by default", () => {
const storedClusters = clusterStore.clustersList;
const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters[0].enabled).toBe(true);
expect(storedClusters[2].enabled).toBe(false);
@@ -276,9 +273,8 @@ users:
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -286,7 +282,7 @@ users:
});
it("does not enable clusters with invalid kubeconfig", () => {
const storedClusters = clusterStore.clustersList;
const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters.length).toBe(2);
expect(storedClusters[0].enabled).toBeFalsy;
@@ -319,9 +315,8 @@ describe("pre 2.0 config with an existing cluster", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -329,7 +324,7 @@ describe("pre 2.0 config with an existing cluster", () => {
});
it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath;
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[]`);
});
@@ -347,16 +342,51 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
}
},
cluster1: {
kubeConfig: "apiVersion: v1\nclusters:\n- cluster:\n server: https://10.211.55.6:8443\n name: minikube\ncontexts:\n- context:\n cluster: minikube\n user: minikube\n name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n user:\n client-certificate: /Users/kimmo/.minikube/client.crt\n client-key: /Users/kimmo/.minikube/client.key\n auth-provider:\n config:\n access-token:\n - should be string\n expiry:\n - should be string\n"
kubeConfig: JSON.stringify({
apiVersion: "v1",
clusters: [{
cluster: {
server: "https://10.211.55.6:8443",
},
name: "minikube",
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
name: "minikube",
},
name: "minikube",
}],
"current-context": "minikube",
kind: "Config",
preferences: {},
users: [{
name: "minikube",
user: {
"client-certificate": "/Users/foo/.minikube/client.crt",
"client-key": "/Users/foo/.minikube/client.key",
"auth-provider": {
config: {
"access-token": [
"should be string"
],
expiry: [
"should be string"
],
}
}
},
}]
}),
},
})
}
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -364,10 +394,12 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
});
it("replaces array format access token and expiry into string", async () => {
const file = clusterStore.clustersList[0].kubeConfigPath;
const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
const config = fs.readFileSync(file, "utf8");
const kc = yaml.safeLoad(config);
console.log(kc);
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");
});
@@ -397,9 +429,8 @@ describe("pre 2.6.0 config with a cluster icon", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -407,7 +438,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
});
it("moves the icon into preferences", async () => {
const storedClusterData = clusterStore.clustersList[0];
const storedClusterData = ClusterStore.getInstance().clustersList[0];
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
@@ -437,9 +468,8 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -474,9 +504,8 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -484,13 +513,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
});
it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath;
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig);
});
it("migrates to modern format with icon not in file", async () => {
const { icon } = clusterStore.clustersList[0].preferences;
const { icon } = ClusterStore.getInstance().clustersList[0].preferences;
expect(icon.startsWith("data:;base64,")).toBe(true);
});
+4
View File
@@ -1,4 +1,8 @@
import { appEventBus, AppEvent } from "../event-bus";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
describe("event bus tests", () => {
describe("emit", () => {
+7 -3
View File
@@ -1,8 +1,12 @@
import mockFs from "mock-fs";
import { HotbarStore, hotbarStore } from "../hotbar-store";
import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store";
describe("HotbarStore", () => {
beforeEach(() => {
ClusterStore.resetInstance();
ClusterStore.getInstanceOrCreate();
HotbarStore.resetInstance();
mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });
});
@@ -13,8 +17,8 @@ describe("HotbarStore", () => {
describe("load", () => {
it("loads one hotbar by default", () => {
hotbarStore.load();
expect(hotbarStore.hotbars.length).toEqual(1);
HotbarStore.getInstanceOrCreate().load();
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
});
});
});
@@ -1,4 +1,8 @@
import { SearchStore } from "../search-store";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
let searchStore: SearchStore = null;
const logs = [
+17 -8
View File
@@ -10,7 +10,7 @@ jest.mock("electron", () => {
getVersion: () => "99.99.99",
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
setLoginItemSettings: (): void => void 0,
}
};
});
@@ -18,12 +18,19 @@ jest.mock("electron", () => {
import { UserStore } from "../user-store";
import { SemVer } from "semver";
import electron from "electron";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
describe("user store tests", () => {
describe("for an empty config", () => {
beforeEach(() => {
UserStore.resetInstance();
mockFs({ tmp: { "config.json": "{}" } });
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
(UserStore.getInstanceOrCreate() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
return UserStore.getInstance().load();
});
afterEach(() => {
@@ -31,14 +38,14 @@ describe("user store tests", () => {
});
it("allows setting and retrieving lastSeenAppVersion", () => {
const us = UserStore.getInstance<UserStore>();
const us = UserStore.getInstance();
us.lastSeenAppVersion = "1.2.3";
expect(us.lastSeenAppVersion).toBe("1.2.3");
});
it("allows adding and listing seen contexts", () => {
const us = UserStore.getInstance<UserStore>();
const us = UserStore.getInstance();
us.seenContexts.add("foo");
expect(us.seenContexts.size).toBe(1);
@@ -51,7 +58,7 @@ describe("user store tests", () => {
});
it("allows setting and getting preferences", () => {
const us = UserStore.getInstance<UserStore>();
const us = UserStore.getInstance();
us.preferences.httpsProxy = "abcd://defg";
@@ -63,7 +70,7 @@ describe("user store tests", () => {
});
it("correctly resets theme to default value", async () => {
const us = UserStore.getInstance<UserStore>();
const us = UserStore.getInstance();
us.isLoaded = true;
@@ -73,7 +80,7 @@ describe("user store tests", () => {
});
it("correctly calculates if the last seen version is an old release", () => {
const us = UserStore.getInstance<UserStore>();
const us = UserStore.getInstance();
expect(us.isNewVersion).toBe(true);
@@ -94,6 +101,8 @@ describe("user store tests", () => {
})
}
});
return UserStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -101,7 +110,7 @@ describe("user store tests", () => {
});
it("sets last seen app version to 0.0.0", () => {
const us = UserStore.getInstance<UserStore>();
const us = UserStore.getInstance();
expect(us.lastSeenAppVersion).toBe("0.0.0");
});
@@ -3,7 +3,7 @@ import { observable } from "mobx";
import { catalogCategoryRegistry } from "../catalog-category-registry";
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
import { clusterDisconnectHandler } from "../cluster-ipc";
import { clusterStore } from "../cluster-store";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
export type KubernetesClusterSpec = {
@@ -56,7 +56,7 @@ export class KubernetesCluster implements CatalogEntity {
icon: "delete",
title: "Delete",
onlyVisibleForSource: "local",
onClick: async () => clusterStore.removeById(this.metadata.uid),
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
confirm: {
message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?`
}
@@ -68,7 +68,7 @@ export class KubernetesCluster implements CatalogEntity {
icon: "link_off",
title: "Disconnect",
onClick: async () => {
clusterStore.deactivate(this.metadata.uid);
ClusterStore.getInstance().deactivate(this.metadata.uid);
requestMain(clusterDisconnectHandler, this.metadata.uid);
}
});
+6 -7
View File
@@ -1,5 +1,5 @@
import { handleRequest } from "./ipc";
import { ClusterId, clusterStore } from "./cluster-store";
import { ClusterId, ClusterStore } from "./cluster-store";
import { appEventBus } from "./event-bus";
import { ResourceApplier } from "../main/resource-applier";
import { ipcMain, IpcMainInvokeEvent } from "electron";
@@ -11,10 +11,9 @@ export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
if (ipcMain) {
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
const cluster = clusterStore.getById(clusterId);
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
return cluster.activate(force);
@@ -22,7 +21,7 @@ if (ipcMain) {
});
handleRequest(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = clusterStore.getById(clusterId);
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
@@ -32,14 +31,14 @@ if (ipcMain) {
});
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
const cluster = clusterStore.getById(clusterId);
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) return cluster.refresh({ refreshMetadata: true });
});
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({name: "cluster", action: "stop"});
const cluster = clusterStore.getById(clusterId);
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
cluster.disconnect();
@@ -49,7 +48,7 @@ if (ipcMain) {
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
const cluster = clusterStore.getById(clusterId);
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
+2 -4
View File
@@ -112,7 +112,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static stateRequestChannel = "cluster:states";
private constructor() {
constructor() {
super({
configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@@ -337,8 +337,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
}
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
@@ -355,5 +353,5 @@ export function getHostedClusterId() {
}
export function getHostedCluster(): Cluster {
return clusterStore.getById(getHostedClusterId());
return ClusterStore.getInstance().getById(getHostedClusterId());
}
+1 -3
View File
@@ -23,7 +23,7 @@ export interface HotbarStoreModel {
export class HotbarStore extends BaseStore<HotbarStoreModel> {
@observable hotbars: Hotbar[] = [];
private constructor() {
constructor() {
super({
configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@@ -67,5 +67,3 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
});
}
}
export const hotbarStore = HotbarStore.getInstance<HotbarStore>();
+5 -5
View File
@@ -5,8 +5,8 @@ import { pathToRegexp } from "path-to-regexp";
import logger from "../../main/logger";
import Url from "url-parse";
import { RoutingError, RoutingErrorType } from "./error";
import { extensionsStore } from "../../extensions/extensions-store";
import { extensionLoader } from "../../extensions/extension-loader";
import { ExtensionsStore } from "../../extensions/extensions-store";
import { ExtensionLoader } from "../../extensions/extension-loader";
import { LensExtension } from "../../extensions/lens-extension";
import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry";
@@ -72,7 +72,7 @@ export abstract class LensProtocolRouter extends Singleton {
/**
* find the most specific matching handler and call it
* @param routes the array of (path schemas, handler) paris to match against
* @param routes the array of (path schemas, handler) pairs to match against
* @param url the url (in its current state)
*/
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {
@@ -124,7 +124,7 @@ export abstract class LensProtocolRouter extends Singleton {
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
const name = [publisher, partialName].filter(Boolean).join("/");
const extension = extensionLoader.userExtensionsByName.get(name);
const extension = ExtensionLoader.getInstance().userExtensionsByName.get(name);
if (!extension) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
@@ -132,7 +132,7 @@ export abstract class LensProtocolRouter extends Singleton {
return name;
}
if (!extensionsStore.isEnabled(extension.id)) {
if (!ExtensionsStore.getInstance().isEnabled(extension.id)) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
return name;
+2 -2
View File
@@ -1,12 +1,12 @@
import request from "request";
import requestPromise from "request-promise-native";
import { userStore } from "./user-store";
import { UserStore } from "./user-store";
// todo: get rid of "request" (deprecated)
// https://github.com/lensapp/lens/issues/459
function getDefaultRequestOpts(): Partial<request.Options> {
const { httpsProxy, allowUntrustedCAs } = userStore.preferences;
const { httpsProxy, allowUntrustedCAs } = UserStore.getInstance().preferences;
return {
proxy: httpsProxy || undefined,
+8 -10
View File
@@ -38,7 +38,7 @@ export interface UserPreferences {
export class UserStore extends BaseStore<UserStoreModel> {
static readonly defaultTheme: ThemeId = "lens-dark";
private constructor() {
constructor() {
super({
configName: "lens-user-store",
migrations,
@@ -163,14 +163,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.newContexts.clear();
}
/**
* Getting default directory to download kubectl binaries
* @returns string
*/
getDefaultKubectlPath(): string {
return path.join((app || remote.app).getPath("userData"), "binaries");
}
@action
protected async fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data;
@@ -200,4 +192,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
}
}
export const userStore = UserStore.getInstance<UserStore>();
/**
* Getting default directory to download kubectl binaries
* @returns string
*/
export function getDefaultKubectlPath(): string {
return path.join((app || remote.app).getPath("userData"), "binaries");
}
+2 -2
View File
@@ -3,8 +3,8 @@
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
let timer: NodeJS.Timeout;
return (...params: any[]) => new Promise(resolve => {
return (...params: F) => new Promise(resolve => {
clearTimeout(timer);
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
timer = global.setTimeout(() => resolve(func(...params)), timeout);
});
}
+21 -7
View File
@@ -5,25 +5,39 @@
* @example
* const usersStore: UsersStore = UsersStore.getInstance();
*/
type StaticThis<T, R extends any[]> = { new(...args: R): T };
type Constructor<T = {}> = new (...args: any[]) => T;
class Singleton {
export class Singleton {
private static instances = new WeakMap<object, Singleton>();
private static creating = "";
// todo: improve types inferring
static getInstance<T>(...args: ConstructorParameters<Constructor<T>>): T {
constructor() {
if (Singleton.creating.length === 0) {
throw new TypeError("A singleton class must be created by getInstanceOrCreate()");
}
}
static getInstanceOrCreate<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
if (!Singleton.instances.has(this)) {
Singleton.instances.set(this, Reflect.construct(this, args));
Singleton.creating = this.name;
Singleton.instances.set(this, new this(...args));
Singleton.creating = "";
}
return Singleton.instances.get(this) as T;
}
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined {
if (!Singleton.instances.has(this) && strict) {
throw new TypeError(`instance of ${this.name} is not created`);
}
return Singleton.instances.get(this) as (T | undefined);
}
static resetInstance() {
Singleton.instances.delete(this);
}
}
export { Singleton };
export default Singleton;