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

Move ExtensionsStore to new format

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-01-24 09:57:07 -05:00
parent 42fd7839fa
commit bd594e12d5
7 changed files with 110 additions and 97 deletions

View File

@ -4,9 +4,9 @@
*/ */
import type Config from "conf"; 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 type { IEqualsComparer } from "mobx";
import { makeObservable, reaction } from "mobx"; import { reaction } from "mobx";
import { disposer, isPromiseLike } from "@k8slens/utilities"; import { disposer, isPromiseLike } from "@k8slens/utilities";
import { broadcastMessage } from "../ipc"; import { broadcastMessage } from "../ipc";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
@ -18,12 +18,31 @@ import type { GetBasenameOfPath } from "../path/get-basename.injectable";
import type { EnlistMessageChannelListener } from "@k8slens/messaging"; import type { EnlistMessageChannelListener } from "@k8slens/messaging";
import { toJS } from "../utils"; import { toJS } from "../utils";
export interface BaseStoreParams<T> extends Omit<ConfOptions<T>, "migrations"> { export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: { syncOptions?: {
fireImmediately?: boolean; fireImmediately?: boolean;
equals?: IEqualsComparer<T>; equals?: IEqualsComparer<T>;
}; };
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 { export interface IpcChannelPrefixes {
@ -33,9 +52,7 @@ export interface IpcChannelPrefixes {
export interface BaseStoreDependencies { export interface BaseStoreDependencies {
readonly logger: Logger; readonly logger: Logger;
readonly storeMigrationVersion: string;
readonly directoryForUserData: string; readonly directoryForUserData: string;
readonly migrations: Migrations<Record<string, unknown>>;
readonly ipcChannelPrefixes: IpcChannelPrefixes; readonly ipcChannelPrefixes: IpcChannelPrefixes;
readonly shouldDisableSyncInListener: boolean; readonly shouldDisableSyncInListener: boolean;
getConfigurationFileModel: GetConfigurationFileModel; getConfigurationFileModel: GetConfigurationFileModel;
@ -47,22 +64,16 @@ export interface BaseStoreDependencies {
/** /**
* Note: T should only contain base JSON serializable types. * Note: T should only contain base JSON serializable types.
*/ */
export abstract class BaseStore<T extends object> { export class BaseStore<T extends object> {
private readonly syncDisposers = disposer(); private readonly syncDisposers = disposer();
readonly displayName = kebabCase(this.params.configName).toUpperCase(); readonly displayName = kebabCase(this.params.configName).toUpperCase();
/** constructor(
* @ignore protected readonly dependencies: BaseStoreDependencies,
*/
protected readonly dependencies: BaseStoreDependencies;
protected constructor(
dependencies: BaseStoreDependencies,
protected readonly params: BaseStoreParams<T>, protected readonly params: BaseStoreParams<T>,
) { ) {
this.dependencies = dependencies; this.dependencies = dependencies;
makeObservable(this);
} }
/** /**
@ -73,13 +84,11 @@ export abstract class BaseStore<T extends object> {
const config = this.dependencies.getConfigurationFileModel({ const config = this.dependencies.getConfigurationFileModel({
projectName: "lens", projectName: "lens",
projectVersion: this.dependencies.storeMigrationVersion,
cwd: this.cwd(), cwd: this.cwd(),
...this.params, ...this.params,
migrations: this.dependencies.migrations as Migrations<T>,
}); });
const res = this.fromStore(config.store); const res = this.params.fromStore(config.store);
if (isPromiseLike(res)) { if (isPromiseLike(res)) {
this.dependencies.logger.error(`${this.displayName} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`); this.dependencies.logger.error(`${this.displayName} extends BaseStore<T>'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<T extends object> {
const enableSync = () => { const enableSync = () => {
this.syncDisposers.push( this.syncDisposers.push(
reaction( reaction(
() => toJS(this.toJSON()), // unwrap possible observables and react to everything () => toJS(this.params.toJSON()), // unwrap possible observables and react to everything
model => { model => {
this.dependencies.persistStateToConfig(config, model); this.dependencies.persistStateToConfig(config, model);
broadcastMessage(`${this.dependencies.ipcChannelPrefixes.remote}:${config.path}`, model); broadcastMessage(`${this.dependencies.ipcChannelPrefixes.remote}:${config.path}`, model);
}, },
this.params.syncOptions, this.params.syncOptions,
), ),
this.dependencies.enlistMessageChannelListener({ this.dependencies.enlistMessageChannelListener<T>({
id: this.displayName, id: this.displayName,
channel: { channel: {
id: `${this.dependencies.ipcChannelPrefixes.local}:${config.path}`, id: `${this.dependencies.ipcChannelPrefixes.local}:${config.path}`,
@ -120,8 +129,8 @@ export abstract class BaseStore<T extends object> {
} }
// todo: use "resourceVersion" if merge required (to avoid equality checks => better performance) // todo: use "resourceVersion" if merge required (to avoid equality checks => better performance)
if (!isEqual(this.toJSON(), model)) { if (!isEqual(this.params.toJSON(), model)) {
this.fromStore(model as T); this.params.fromStore(model);
} }
if (this.dependencies.shouldDisableSyncInListener) { if (this.dependencies.shouldDisableSyncInListener) {
@ -134,23 +143,4 @@ export abstract class BaseStore<T extends object> {
enableSync(); 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;
} }

View File

@ -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 = <T extends object>(params: BaseStoreParams<T>) => BaseStore<T>;
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;

View File

@ -8,21 +8,22 @@ import isLinuxInjectable from "../../common/vars/is-linux.injectable";
import isMacInjectable from "../../common/vars/is-mac.injectable"; import isMacInjectable from "../../common/vars/is-mac.injectable";
import isSnapPackageInjectable from "../../common/vars/is-snap-package.injectable"; import isSnapPackageInjectable from "../../common/vars/is-snap-package.injectable";
import isWindowsInjectable from "../../common/vars/is-windows.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 { 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 { issuesTrackerUrl } from "../../common/vars";
import { buildVersionInjectionToken } from "../../common/vars/build-semantic-version.injectable"; 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 { 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 userStoreInjectable from "../../common/user-store/user-store.injectable";
import enabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable";
const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable); const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable);
const enabledExtensions = asLegacyGlobalForExtensionApi(enabledExtensionsInjectable);
export const App = { export const App = {
Preferences: { Preferences: {
getKubectlPath: () => userStore.kubectlBinariesPath, getKubectlPath: () => userStore.kubectlBinariesPath,
}, },
getEnabledExtensions: asLegacyGlobalFunctionForExtensionApi(getEnabledExtensionsInjectable), getEnabledExtensions: () => enabledExtensions.get(),
get version() { get version() {
const di = getLegacyGlobalDiForExtensionApi(); const di = getLegacyGlobalDiForExtensionApi();
@ -54,7 +55,7 @@ export const App = {
return di.inject(isLinuxInjectable); 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: "", slackUrl: "",
issuesTrackerUrl, issuesTrackerUrl,

View File

@ -5,11 +5,9 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable"; import extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable";
const getEnabledExtensionsInjectable = getInjectable({ const enabledExtensionsInjectable = getInjectable({
id: "get-enabled-extensions", id: "enabled-extensions",
instantiate: (di) => di.inject(extensionsStoreInjectable).enabledExtensions,
instantiate: (di) => () =>
di.inject(extensionsStoreInjectable).enabledExtensions,
}); });
export default getEnabledExtensionsInjectable; export default enabledExtensionsInjectable;

View File

@ -3,30 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import createBaseStoreInjectable from "../../common/base-store/create-base-store.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 storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable"; import storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable";
import { ExtensionsStore } from "./extensions-store"; import { ExtensionsStore } from "./extensions-store";
const extensionsStoreInjectable = getInjectable({ const extensionsStoreInjectable = getInjectable({
id: "extensions-store", id: "extensions-store",
instantiate: (di) => new ExtensionsStore({ instantiate: (di) => new ExtensionsStore({
directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable), storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: {}, createBaseStore: di.inject(createBaseStoreInjectable),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}), }),
}); });

View File

@ -4,12 +4,12 @@
*/ */
import type { LensExtensionId } from "@k8slens/legacy-extensions"; import type { LensExtensionId } from "@k8slens/legacy-extensions";
import { action, computed, makeObservable, observable } from "mobx"; import { action, computed, observable } from "mobx";
import type { BaseStoreDependencies } from "../../common/base-store/base-store"; import type { BaseStore } from "../../common/base-store/base-store";
import { BaseStore } from "../../common/base-store/base-store"; import type { CreateBaseStore } from "../../common/base-store/create-base-store.injectable";
export interface LensExtensionsStoreModel { export interface LensExtensionsStoreModel {
extensions: Record<LensExtensionId, LensExtensionState>; extensions?: Record<LensExtensionId, LensExtensionState>;
} }
export interface LensExtensionState { export interface LensExtensionState {
@ -22,23 +22,35 @@ export interface IsEnabledExtensionDescriptor {
isBundled: boolean; isBundled: boolean;
} }
export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> { export interface ExtensionsStoreDependencies {
constructor(deps: BaseStoreDependencies) { createBaseStore: CreateBaseStore;
super(deps, { readonly storeMigrationVersion: string;
}
export class ExtensionsStore {
private readonly store: BaseStore<LensExtensionsStoreModel>;
constructor(private readonly dependencies: ExtensionsStoreDependencies) {
this.store = this.dependencies.createBaseStore({
configName: "lens-extensions", configName: "lens-extensions",
fromStore: action(({ extensions = {}}) => {
this.state.merge(extensions);
}),
toJSON: () => ({
extensions: Object.fromEntries(this.state),
}),
projectVersion: this.dependencies.storeMigrationVersion,
}); });
makeObservable(this); this.store.load();
this.load();
} }
@computed readonly enabledExtensions = computed(() => (
get enabledExtensions() { Array.from(this.state.values())
return Array.from(this.state.values())
.filter(({ enabled }) => enabled) .filter(({ enabled }) => enabled)
.map(({ name }) => name); .map(({ name }) => name)
} ));
protected state = observable.map<LensExtensionId, LensExtensionState>(); protected readonly state = observable.map<LensExtensionId, LensExtensionState>();
isEnabled({ id, isBundled }: IsEnabledExtensionDescriptor): boolean { isEnabled({ id, isBundled }: IsEnabledExtensionDescriptor): boolean {
// By default false, so that copied extensions are disabled by default. // By default false, so that copied extensions are disabled by default.
@ -49,15 +61,4 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
mergeState = action((extensionsState: Record<LensExtensionId, LensExtensionState> | [LensExtensionId, LensExtensionState][]) => { mergeState = action((extensionsState: Record<LensExtensionId, LensExtensionState> | [LensExtensionId, LensExtensionState][]) => {
this.state.merge(extensionsState); this.state.merge(extensionsState);
}); });
@action
protected fromStore({ extensions }: LensExtensionsStoreModel) {
this.state.merge(extensions);
}
toJSON(): LensExtensionsStoreModel {
return {
extensions: Object.fromEntries(this.state.toJSON()),
};
}
} }

View File

@ -1,3 +1,4 @@
import type { Disposer } from "@k8slens/utilities";
import { getInjectionToken } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable";
import type { import type {
@ -5,9 +6,9 @@ import type {
MessageChannelListener, MessageChannelListener,
} from "./message-channel-listener-injection-token"; } from "./message-channel-listener-injection-token";
export type EnlistMessageChannelListener = ( export type EnlistMessageChannelListener = <T>(
listener: MessageChannelListener<MessageChannel<unknown>>, listener: MessageChannelListener<MessageChannel<T>>,
) => () => void; ) => Disposer;
export const enlistMessageChannelListenerInjectionToken = export const enlistMessageChannelListenerInjectionToken =
getInjectionToken<EnlistMessageChannelListener>({ getInjectionToken<EnlistMessageChannelListener>({