diff --git a/packages/core/src/common/base-store/base-store.ts b/packages/core/src/common/base-store/base-store.ts index a1dd26f0f7..a6f48c1f57 100644 --- a/packages/core/src/common/base-store/base-store.ts +++ b/packages/core/src/common/base-store/base-store.ts @@ -4,9 +4,9 @@ */ import type Config from "conf"; -import type { Migrations, Options as ConfOptions } from "conf/dist/source/types"; +import type { Options as ConfOptions } from "conf/dist/source/types"; import type { IEqualsComparer } from "mobx"; -import { makeObservable, reaction } from "mobx"; +import { reaction } from "mobx"; import { disposer, isPromiseLike } from "@k8slens/utilities"; import { broadcastMessage } from "../ipc"; import isEqual from "lodash/isEqual"; @@ -18,12 +18,31 @@ import type { GetBasenameOfPath } from "../path/get-basename.injectable"; import type { EnlistMessageChannelListener } from "@k8slens/messaging"; import { toJS } from "../utils"; -export interface BaseStoreParams extends Omit, "migrations"> { +export interface BaseStoreParams extends ConfOptions { syncOptions?: { fireImmediately?: boolean; equals?: IEqualsComparer; }; - configName: string; + readonly configName: string; + + /** + * fromStore is called internally when a child class syncs with the file + * system. + * + * Note: This function **must** be synchronous. + * + * @param data the parsed information read from the stored JSON file + */ + fromStore(data: T): void; + + /** + * toJSON is called when syncing the store to the filesystem. It should + * produce a JSON serializable object representation of the current state. + * + * It is recommended that a round trip is valid. Namely, calling + * `this.fromStore(this.toJSON())` shouldn't change the state. + */ + toJSON(): T; } export interface IpcChannelPrefixes { @@ -33,9 +52,7 @@ export interface IpcChannelPrefixes { export interface BaseStoreDependencies { readonly logger: Logger; - readonly storeMigrationVersion: string; readonly directoryForUserData: string; - readonly migrations: Migrations>; readonly ipcChannelPrefixes: IpcChannelPrefixes; readonly shouldDisableSyncInListener: boolean; getConfigurationFileModel: GetConfigurationFileModel; @@ -47,22 +64,16 @@ export interface BaseStoreDependencies { /** * Note: T should only contain base JSON serializable types. */ -export abstract class BaseStore { +export class BaseStore { private readonly syncDisposers = disposer(); readonly displayName = kebabCase(this.params.configName).toUpperCase(); - /** - * @ignore - */ - protected readonly dependencies: BaseStoreDependencies; - - protected constructor( - dependencies: BaseStoreDependencies, + constructor( + protected readonly dependencies: BaseStoreDependencies, protected readonly params: BaseStoreParams, ) { this.dependencies = dependencies; - makeObservable(this); } /** @@ -73,13 +84,11 @@ export abstract class BaseStore { const config = this.dependencies.getConfigurationFileModel({ projectName: "lens", - projectVersion: this.dependencies.storeMigrationVersion, cwd: this.cwd(), ...this.params, - migrations: this.dependencies.migrations as Migrations, }); - const res = this.fromStore(config.store); + const res = this.params.fromStore(config.store); if (isPromiseLike(res)) { this.dependencies.logger.error(`${this.displayName} extends BaseStore's fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`); @@ -100,14 +109,14 @@ export abstract class BaseStore { const enableSync = () => { this.syncDisposers.push( reaction( - () => toJS(this.toJSON()), // unwrap possible observables and react to everything + () => toJS(this.params.toJSON()), // unwrap possible observables and react to everything model => { this.dependencies.persistStateToConfig(config, model); broadcastMessage(`${this.dependencies.ipcChannelPrefixes.remote}:${config.path}`, model); }, this.params.syncOptions, ), - this.dependencies.enlistMessageChannelListener({ + this.dependencies.enlistMessageChannelListener({ id: this.displayName, channel: { id: `${this.dependencies.ipcChannelPrefixes.local}:${config.path}`, @@ -120,8 +129,8 @@ export abstract class BaseStore { } // todo: use "resourceVersion" if merge required (to avoid equality checks => better performance) - if (!isEqual(this.toJSON(), model)) { - this.fromStore(model as T); + if (!isEqual(this.params.toJSON(), model)) { + this.params.fromStore(model); } if (this.dependencies.shouldDisableSyncInListener) { @@ -134,23 +143,4 @@ export abstract class BaseStore { enableSync(); } - - /** - * fromStore is called internally when a child class syncs with the file - * system. - * - * Note: This function **must** be synchronous. - * - * @param data the parsed information read from the stored JSON file - */ - protected abstract fromStore(data: T): void; - - /** - * toJSON is called when syncing the store to the filesystem. It should - * produce a JSON serializable object representation of the current state. - * - * It is recommended that a round trip is valid. Namely, calling - * `this.fromStore(this.toJSON())` shouldn't change the state. - */ - abstract toJSON(): T; } diff --git a/packages/core/src/common/base-store/create-base-store.injectable.ts b/packages/core/src/common/base-store/create-base-store.injectable.ts new file mode 100644 index 0000000000..07972575ca --- /dev/null +++ b/packages/core/src/common/base-store/create-base-store.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; +import loggerInjectable from "../logger.injectable"; +import getBasenameOfPathInjectable from "../path/get-basename.injectable"; +import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; +import type { BaseStoreDependencies, BaseStoreParams } from "./base-store"; +import { BaseStore } from "./base-store"; +import { baseStoreIpcChannelPrefixesInjectionToken } from "./channel-prefix"; +import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "./disable-sync"; +import { persistStateToConfigInjectionToken } from "./save-to-file"; + +export type CreateBaseStore = (params: BaseStoreParams) => BaseStore; + +const createBaseStoreInjectable = getInjectable({ + id: "create-base-store", + instantiate: (di): CreateBaseStore => { + const deps: BaseStoreDependencies = { + directoryForUserData: di.inject(directoryForUserDataInjectable), + getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), + logger: di.inject(loggerInjectable), + getBasenameOfPath: di.inject(getBasenameOfPathInjectable), + ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken), + persistStateToConfig: di.inject(persistStateToConfigInjectionToken), + enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), + shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), + }; + + return (params) => new BaseStore(deps, params); + }, +}); + +export default createBaseStoreInjectable; diff --git a/packages/core/src/extensions/common-api/app.ts b/packages/core/src/extensions/common-api/app.ts index 8c190b984b..c25848539a 100644 --- a/packages/core/src/extensions/common-api/app.ts +++ b/packages/core/src/extensions/common-api/app.ts @@ -8,21 +8,22 @@ import isLinuxInjectable from "../../common/vars/is-linux.injectable"; import isMacInjectable from "../../common/vars/is-mac.injectable"; import isSnapPackageInjectable from "../../common/vars/is-snap-package.injectable"; import isWindowsInjectable from "../../common/vars/is-windows.injectable"; -import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; -import getEnabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable"; import { issuesTrackerUrl } from "../../common/vars"; import { buildVersionInjectionToken } from "../../common/vars/build-semantic-version.injectable"; import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; import userStoreInjectable from "../../common/user-store/user-store.injectable"; +import enabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable"; const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable); +const enabledExtensions = asLegacyGlobalForExtensionApi(enabledExtensionsInjectable); + export const App = { Preferences: { getKubectlPath: () => userStore.kubectlBinariesPath, }, - getEnabledExtensions: asLegacyGlobalFunctionForExtensionApi(getEnabledExtensionsInjectable), + getEnabledExtensions: () => enabledExtensions.get(), get version() { const di = getLegacyGlobalDiForExtensionApi(); @@ -54,7 +55,7 @@ export const App = { return di.inject(isLinuxInjectable); }, /** - * @deprecated This value is now `""` and is left here for backwards compatability. + * @deprecated This value is now `""` and is left here for backwards compatibility. */ slackUrl: "", issuesTrackerUrl, diff --git a/packages/core/src/extensions/common-api/get-enabled-extensions/get-enabled-extensions.injectable.ts b/packages/core/src/extensions/common-api/get-enabled-extensions/get-enabled-extensions.injectable.ts index 747dfa4f42..72a866ff49 100644 --- a/packages/core/src/extensions/common-api/get-enabled-extensions/get-enabled-extensions.injectable.ts +++ b/packages/core/src/extensions/common-api/get-enabled-extensions/get-enabled-extensions.injectable.ts @@ -5,11 +5,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable"; -const getEnabledExtensionsInjectable = getInjectable({ - id: "get-enabled-extensions", - - instantiate: (di) => () => - di.inject(extensionsStoreInjectable).enabledExtensions, +const enabledExtensionsInjectable = getInjectable({ + id: "enabled-extensions", + instantiate: (di) => di.inject(extensionsStoreInjectable).enabledExtensions, }); -export default getEnabledExtensionsInjectable; +export default enabledExtensionsInjectable; diff --git a/packages/core/src/extensions/extensions-store/extensions-store.injectable.ts b/packages/core/src/extensions/extensions-store/extensions-store.injectable.ts index 9e08a96aac..54d0e08013 100644 --- a/packages/core/src/extensions/extensions-store/extensions-store.injectable.ts +++ b/packages/core/src/extensions/extensions-store/extensions-store.injectable.ts @@ -3,30 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import { baseStoreIpcChannelPrefixesInjectionToken } from "../../common/base-store/channel-prefix"; -import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../common/base-store/disable-sync"; -import { persistStateToConfigInjectionToken } from "../../common/base-store/save-to-file"; -import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable"; -import loggerInjectable from "../../common/logger.injectable"; -import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; -import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; +import createBaseStoreInjectable from "../../common/base-store/create-base-store.injectable"; import storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable"; import { ExtensionsStore } from "./extensions-store"; const extensionsStoreInjectable = getInjectable({ id: "extensions-store", instantiate: (di) => new ExtensionsStore({ - directoryForUserData: di.inject(directoryForUserDataInjectable), - getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), - logger: di.inject(loggerInjectable), storeMigrationVersion: di.inject(storeMigrationVersionInjectable), - migrations: {}, - getBasenameOfPath: di.inject(getBasenameOfPathInjectable), - ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken), - persistStateToConfig: di.inject(persistStateToConfigInjectionToken), - enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), - shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), + createBaseStore: di.inject(createBaseStoreInjectable), }), }); diff --git a/packages/core/src/extensions/extensions-store/extensions-store.ts b/packages/core/src/extensions/extensions-store/extensions-store.ts index 17766d26d7..877dd88bd5 100644 --- a/packages/core/src/extensions/extensions-store/extensions-store.ts +++ b/packages/core/src/extensions/extensions-store/extensions-store.ts @@ -4,12 +4,12 @@ */ import type { LensExtensionId } from "@k8slens/legacy-extensions"; -import { action, computed, makeObservable, observable } from "mobx"; -import type { BaseStoreDependencies } from "../../common/base-store/base-store"; -import { BaseStore } from "../../common/base-store/base-store"; +import { action, computed, observable } from "mobx"; +import type { BaseStore } from "../../common/base-store/base-store"; +import type { CreateBaseStore } from "../../common/base-store/create-base-store.injectable"; export interface LensExtensionsStoreModel { - extensions: Record; + extensions?: Record; } export interface LensExtensionState { @@ -22,23 +22,35 @@ export interface IsEnabledExtensionDescriptor { isBundled: boolean; } -export class ExtensionsStore extends BaseStore { - constructor(deps: BaseStoreDependencies) { - super(deps, { +export interface ExtensionsStoreDependencies { + createBaseStore: CreateBaseStore; + readonly storeMigrationVersion: string; +} + +export class ExtensionsStore { + private readonly store: BaseStore; + + constructor(private readonly dependencies: ExtensionsStoreDependencies) { + this.store = this.dependencies.createBaseStore({ configName: "lens-extensions", + fromStore: action(({ extensions = {}}) => { + this.state.merge(extensions); + }), + toJSON: () => ({ + extensions: Object.fromEntries(this.state), + }), + projectVersion: this.dependencies.storeMigrationVersion, }); - makeObservable(this); - this.load(); + this.store.load(); } - @computed - get enabledExtensions() { - return Array.from(this.state.values()) + readonly enabledExtensions = computed(() => ( + Array.from(this.state.values()) .filter(({ enabled }) => enabled) - .map(({ name }) => name); - } + .map(({ name }) => name) + )); - protected state = observable.map(); + protected readonly state = observable.map(); isEnabled({ id, isBundled }: IsEnabledExtensionDescriptor): boolean { // By default false, so that copied extensions are disabled by default. @@ -49,15 +61,4 @@ export class ExtensionsStore extends BaseStore { mergeState = action((extensionsState: Record | [LensExtensionId, LensExtensionState][]) => { this.state.merge(extensionsState); }); - - @action - protected fromStore({ extensions }: LensExtensionsStoreModel) { - this.state.merge(extensions); - } - - toJSON(): LensExtensionsStoreModel { - return { - extensions: Object.fromEntries(this.state.toJSON()), - }; - } } diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/message/enlist-message-channel-listener-injection-token.ts b/packages/technical-features/messaging/agnostic/src/features/actual/message/enlist-message-channel-listener-injection-token.ts index 9ec6f8b93a..1bb114ca09 100644 --- a/packages/technical-features/messaging/agnostic/src/features/actual/message/enlist-message-channel-listener-injection-token.ts +++ b/packages/technical-features/messaging/agnostic/src/features/actual/message/enlist-message-channel-listener-injection-token.ts @@ -1,3 +1,4 @@ +import type { Disposer } from "@k8slens/utilities"; import { getInjectionToken } from "@ogre-tools/injectable"; import type { @@ -5,9 +6,9 @@ import type { MessageChannelListener, } from "./message-channel-listener-injection-token"; -export type EnlistMessageChannelListener = ( - listener: MessageChannelListener>, -) => () => void; +export type EnlistMessageChannelListener = ( + listener: MessageChannelListener>, +) => Disposer; export const enlistMessageChannelListenerInjectionToken = getInjectionToken({