diff --git a/src/features/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx b/src/features/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx index 4cf63ee353..0d49469951 100644 --- a/src/features/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx +++ b/src/features/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx @@ -11,8 +11,6 @@ import asyncFn from "@async-fn/jest"; import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable"; import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable"; import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; -import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable"; -import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready"; import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; import { TabKind } from "../../../renderer/components/dock/dock/store"; import { Namespace } from "../../../common/k8s-api/endpoints"; @@ -20,7 +18,6 @@ import { Namespace } from "../../../common/k8s-api/endpoints"; describe("cluster/namespaces - edit namespaces from previously opened tab", () => { let builder: ApplicationBuilder; let callForNamespaceMock: AsyncFnMock; - let storagesAreReady: () => Promise; beforeEach(() => { builder = getApplicationBuilder(); @@ -35,10 +32,6 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () = () => "/some-directory-for-lens-local-storage", ); - windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id"); - - storagesAreReady = controlWhenStoragesAreReady(windowDi); - windowDi.override(callForResourceInjectable, () => callForNamespaceMock); }); @@ -83,8 +76,6 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () = }); rendered = await builder.render(); - - await storagesAreReady(); }); it("renders", () => { diff --git a/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap b/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap index add277f6ff..7636ade12e 100644 --- a/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap +++ b/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap @@ -4,7 +4,7 @@ exports[`installing helm chart from previously opened tab given tab for installi
+ class="InstallChart flex column" + > +
+
+
+ + Chart + +
+ some-repository/some-name +
+ + Version + +
+ + +
+
+
+ some-other-version +
+
+ +
+
+
+ + +
+
+
+ + Namespace + +
+ + +
+
+
+ some-other-namespace +
+
+ +
+
+
+ + +
+
+
+
+ +
+
+
+
+
+ + +
+ +
diff --git a/src/features/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts b/src/features/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts index 0b4f749acc..db11c245aa 100644 --- a/src/features/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts +++ b/src/features/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts @@ -9,13 +9,9 @@ import type { ApplicationBuilder } from "../../../renderer/components/test-utils import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable"; -import namespaceStoreInjectable from "../../../renderer/components/+namespaces/store.injectable"; -import type { NamespaceStore } from "../../../renderer/components/+namespaces/store"; import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; -import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable"; import { TabKind } from "../../../renderer/components/dock/dock/store"; -import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready"; import requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable"; import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable"; import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable"; @@ -26,7 +22,6 @@ describe("installing helm chart from previously opened tab", () => { let builder: ApplicationBuilder; let requestHelmChartVersionsMock: AsyncFnMock; let requestHelmChartValuesMock: AsyncFnMock; - let storagesAreReady: () => Promise; beforeEach(() => { builder = getApplicationBuilder(); @@ -36,29 +31,15 @@ describe("installing helm chart from previously opened tab", () => { requestHelmChartVersionsMock = asyncFn(); requestHelmChartValuesMock = asyncFn(); - builder.beforeWindowStart((windowDi) => { - storagesAreReady = controlWhenStoragesAreReady(windowDi); + builder.namespaces.add("default"); + builder.namespaces.add("some-other-namespace"); + builder.beforeWindowStart((windowDi) => { windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage"); - windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id"); windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock); windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock); windowDi.override(requestCreateHelmReleaseInjectable, () => jest.fn()); - // TODO: Replace store mocking with mock for the actual side-effect (where the namespaces are coming from) - windowDi.override( - namespaceStoreInjectable, - () => - ({ - contextNamespaces: [], - items: [ - { getName: () => "default" }, - { getName: () => "some-other-namespace" }, - ], - selectNamespaces: () => {}, - } as unknown as NamespaceStore), - ); - windowDi.override(getRandomInstallChartTabIdInjectable, () => jest .fn(() => "some-irrelevant-tab-id") @@ -106,8 +87,6 @@ describe("installing helm chart from previously opened tab", () => { }); rendered = await builder.render(); - - await storagesAreReady(); }); it("renders", () => { diff --git a/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts b/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts index eec8c5a27f..d6ec705d2a 100644 --- a/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts +++ b/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts @@ -6,7 +6,7 @@ import { action, observable, reaction } from "mobx"; import type { StorageLayer } from "../../../utils"; import { autoBind, toJS } from "../../../utils"; -import type { CreateStorage } from "../../../utils/create-storage/create-storage"; +import type { CreateStorage } from "../../../utils/create-storage/create-storage.injectable"; import type { TabId } from "../dock/store"; export interface DockTabStoreOptions { @@ -33,10 +33,8 @@ export class DockTabStore { if (autoInit && storageKey) { const storage = this.storage = this.dependencies.createStorage(storageKey, {}); - storage.whenReady.then(() => { - this.data.replace(storage.value); - reaction(() => this.toJSON(), data => storage.set(data)); - }); + this.data.replace(storage.get()); + reaction(() => this.toJSON(), data => storage.set(data)); } } diff --git a/src/renderer/components/dock/dock/store.ts b/src/renderer/components/dock/dock/store.ts index 88560632cc..48e04c3a76 100644 --- a/src/renderer/components/dock/dock/store.ts +++ b/src/renderer/components/dock/dock/store.ts @@ -108,19 +108,25 @@ export class DockStore implements DockStorageState { constructor(private readonly dependencies: Dependencies) { makeObservable(this); autoBind(this); - this.init(); + + // adjust terminal height if window size changes + window.addEventListener("resize", throttle(this.adjustHeight, 250)); + + for (const tab of this.tabs) { + const tabDataIsValid = this.dependencies.tabDataValidator[tab.kind] ?? (() => true); + + if (!tabDataIsValid(tab.id)) { + this.closeTab(tab.id); + } + } } readonly minHeight = 100; @observable fullSize = false; - get whenReady() { - return this.dependencies.storage.whenReady; - } - @computed get isOpen(): boolean { - return this.dependencies.storage.value.isOpen; + return this.dependencies.storage.get().isOpen; } set isOpen(isOpen: boolean) { @@ -129,7 +135,7 @@ export class DockStore implements DockStorageState { @computed get height(): number { - return this.dependencies.storage.value.height; + return this.dependencies.storage.get().height; } set height(height: number) { @@ -140,7 +146,7 @@ export class DockStore implements DockStorageState { @computed get tabs(): DockTab[] { - return this.dependencies.storage.value.tabs; + return this.dependencies.storage.get().tabs; } set tabs(tabs: DockTab[]) { @@ -149,7 +155,7 @@ export class DockStore implements DockStorageState { @computed get selectedTabId(): TabId | undefined { - const storageData = this.dependencies.storage.value; + const storageData = this.dependencies.storage.get(); return ( storageData.selectedTabId || @@ -171,21 +177,6 @@ export class DockStore implements DockStorageState { return this.tabs.find(tab => tab.id === this.selectedTabId); } - private init() { - // adjust terminal height if window size changes - window.addEventListener("resize", throttle(this.adjustHeight, 250)); - - this.whenReady.then(action(() => { - for (const tab of this.tabs) { - const validator = this.dependencies.tabDataValidator[tab.kind]; - - if (validator && !validator(tab.id)) { - this.closeTab(tab.id); - } - } - })); - } - get maxHeight() { const mainLayoutHeader = 40; const mainLayoutTabs = 33; diff --git a/src/renderer/utils/__tests__/storageHelper.test.ts b/src/renderer/utils/__tests__/storage-helper.test.ts similarity index 67% rename from src/renderer/utils/__tests__/storageHelper.test.ts rename to src/renderer/utils/__tests__/storage-helper.test.ts index af34ab4322..71e9635d61 100644 --- a/src/renderer/utils/__tests__/storageHelper.test.ts +++ b/src/renderer/utils/__tests__/storage-helper.test.ts @@ -4,9 +4,11 @@ */ import { observable, reaction } from "mobx"; -import { StorageHelper } from "../storageHelper"; -import { delay } from "../../../common/utils/delay"; -import { noop, toJS } from "../../../common/utils"; +import type { StorageHelper } from "../storage-helper"; +import { toJS } from "../../../common/utils"; +import type { CreateStorageHelper } from "../create-storage-helper.injectable"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import createStorageHelperInjectable from "../create-storage-helper.injectable"; interface StorageModel { [prop: string]: any /*json-serializable*/; @@ -15,27 +17,25 @@ interface StorageModel { } describe("renderer/utils/StorageHelper", () => { + let createStorageHelper: CreateStorageHelper; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + createStorageHelper = di.inject(createStorageHelperInjectable); + }); + describe("Using custom StorageAdapter", () => { const storageKey = "ui-settings"; const remoteStorageMock = observable.map(); let storageHelper: StorageHelper; - let storageHelperAsync: StorageHelper; beforeEach(() => { remoteStorageMock.set(storageKey, { message: "saved-before", // pretending as previously saved data }); - storageHelper = new StorageHelper({ - logger: { - debug: noop, - error: noop, - info: noop, - silly: noop, - warn: noop, - }, - }, storageKey, { - autoInit: false, + storageHelper = createStorageHelper(storageKey, { defaultValue: { message: "blabla", description: "default", @@ -55,46 +55,14 @@ describe("renderer/utils/StorageHelper", () => { }, }, }); - - storageHelperAsync = new StorageHelper({ - logger: { - debug: noop, - error: noop, - info: noop, - silly: noop, - warn: noop, - }, - }, storageKey, { - autoInit: false, - defaultValue: storageHelper.defaultValue, - storage: { - ...storageHelper.storage, - async getItem(key: string): Promise { - await delay(500); // fake loading timeout - - return storageHelper.storage.getItem(key); - }, - }, - }); }); it("initialized with default value", async () => { - storageHelper.init(); expect(storageHelper.key).toBe(storageKey); expect(storageHelper.get()).toEqual(storageHelper.defaultValue); }); - it("async loading from storage supported too", async () => { - expect(storageHelperAsync.initialized).toBeFalsy(); - storageHelperAsync.init(); - await delay(300); - expect(storageHelperAsync.get()).toEqual(storageHelper.defaultValue); - await delay(200); - expect(storageHelperAsync.get().message).toBe("saved-before"); - }); - it("set() fully replaces data in storage", () => { - storageHelper.init(); storageHelper.set({ message: "msg" }); storageHelper.get().description = "desc"; expect(storageHelper.get().message).toBe("msg"); @@ -106,7 +74,6 @@ describe("renderer/utils/StorageHelper", () => { }); it("merge() does partial data tree updates", () => { - storageHelper.init(); storageHelper.merge({ message: "updated" }); expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated" }); @@ -134,16 +101,7 @@ describe("renderer/utils/StorageHelper", () => { beforeEach(() => { observedChanges.length = 0; - storageHelper = new StorageHelper({ - logger: { - debug: noop, - error: noop, - info: noop, - silly: noop, - warn: noop, - }, - }, "some-key", { - autoInit: true, + storageHelper = createStorageHelper("some-key", { defaultValue, storage: { getItem: jest.fn(), diff --git a/src/renderer/utils/create-storage-helper.injectable.ts b/src/renderer/utils/create-storage-helper.injectable.ts new file mode 100644 index 0000000000..36eebb246d --- /dev/null +++ b/src/renderer/utils/create-storage-helper.injectable.ts @@ -0,0 +1,23 @@ +/** + * 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 loggerInjectable from "../../common/logger.injectable"; +import type { StorageHelperDependencies, StorageHelperOptions } from "./storage-helper"; +import { StorageHelper } from "./storage-helper"; + +export type CreateStorageHelper = (key: string, options: StorageHelperOptions) => StorageHelper; + +const createStorageHelperInjectable = getInjectable({ + id: "create-storage-helper", + instantiate: (di): CreateStorageHelper => { + const deps: StorageHelperDependencies = { + logger: di.inject(loggerInjectable), + }; + + return (key, options) => new StorageHelper(deps, key, options); + }, +}); + +export default createStorageHelperInjectable; diff --git a/src/renderer/utils/create-storage/create-storage.injectable.ts b/src/renderer/utils/create-storage/create-storage.injectable.ts index 2d295b802b..712f5b3b2f 100644 --- a/src/renderer/utils/create-storage/create-storage.injectable.ts +++ b/src/renderer/utils/create-storage/create-storage.injectable.ts @@ -3,33 +3,29 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; -import { createStorage } from "./create-storage"; -import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable"; -import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; -import { observable } from "mobx"; -import loggerInjectable from "../../../common/logger.injectable"; -import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable"; -import storageSaveDelayInjectable from "./storage-save-delay.injectable"; -import joinPathsInjectable from "../../../common/path/join-paths.injectable"; +import { action } from "mobx"; +import lensLocalStorageStateInjectable from "./state.injectable"; +import createStorageHelperInjectable from "../create-storage-helper.injectable"; +import type { StorageLayer } from "../storage-helper"; + +export type CreateStorage = (key: string, defaultValue: T) => StorageLayer; const createStorageInjectable = getInjectable({ id: "create-storage", - instantiate: (di) => createStorage({ - storage: observable({ - initialized: false, - loaded: false, - data: {}, - }), - readJsonFile: di.inject(readJsonFileInjectable), - writeJsonFile: di.inject(writeJsonFileInjectable), - logger: di.inject(loggerInjectable), - directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable), - joinPaths: di.inject(joinPathsInjectable), - hostedClusterId: di.inject(hostedClusterIdInjectable), - saveDelay: di.inject(storageSaveDelayInjectable), - }), + instantiate: (di): CreateStorage => { + const lensLocalStorageState = di.inject(lensLocalStorageStateInjectable); + const createStorageHelper = di.inject(createStorageHelperInjectable); + + return (key: string, defaultValue: T) => createStorageHelper(key, { + defaultValue, + storage: { + getItem: (key) => lensLocalStorageState[key] as T, + setItem: action((key, value) => lensLocalStorageState[key] = value), + removeItem: action((key) => delete lensLocalStorageState[key]), + }, + }); + }, }); export default createStorageInjectable; diff --git a/src/renderer/utils/create-storage/create-storage.ts b/src/renderer/utils/create-storage/create-storage.ts deleted file mode 100755 index 04a0339ef1..0000000000 --- a/src/renderer/utils/create-storage/create-storage.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -// Keeps window.localStorage state in external JSON-files. -// Because app creates random port between restarts => storage session wiped out each time. -import { comparer, reaction, toJS, when } from "mobx"; -import type { StorageLayer } from "../storageHelper"; -import { storageHelperLogPrefix, StorageHelper } from "../storageHelper"; -import type { JsonObject } from "type-fest"; -import type { Logger } from "../../../common/logger"; -import type { JoinPaths } from "../../../common/path/join-paths.injectable"; -import type { WriteJson } from "../../../common/fs/write-json-file.injectable"; -import type { ReadJson } from "../../../common/fs/read-json-file.injectable"; - -interface Dependencies { - storage: { initialized: boolean; loaded: boolean; data: Partial> }; - logger: Logger; - directoryForLensLocalStorage: string; - readJsonFile: ReadJson; - writeJsonFile: WriteJson; - joinPaths: JoinPaths; - hostedClusterId: string | undefined; - saveDelay: number; -} - -export type CreateStorage = (key: string, defaultValue: T) => StorageLayer; - -/** - * Creates a helper for saving data under the "key" intended for window.localStorage - */ -export const createStorage = ({ - storage, - joinPaths, - logger, - directoryForLensLocalStorage, - readJsonFile, - writeJsonFile, - hostedClusterId, - saveDelay, -}: Dependencies): CreateStorage => (key: string, defaultValue: T) => { - if (!storage.initialized) { - storage.initialized = true; - - (async () => { - const filePath = joinPaths(directoryForLensLocalStorage, `${hostedClusterId || "app"}.json`); - - try { - storage.data = (await readJsonFile(filePath)) as JsonObject; - } catch { - // do nothing - } finally { - logger.info(`${storageHelperLogPrefix} loading finished for ${filePath}`); - storage.loaded = true; - } - - // bind auto-saving data changes to %storage-file.json - reaction(() => toJS(storage.data), saveFile, { - delay: saveDelay, // lazy, avoid excessive writes to fs - equals: comparer.structural, // save only when something really changed - }); - - async function saveFile(state: Record = {}) { - logger.info(`${storageHelperLogPrefix} saving ${filePath}`); - - try { - await writeJsonFile(filePath, state); - } catch (error) { - logger.error(`${storageHelperLogPrefix} saving failed: ${error}`, { - json: state, jsonFilePath: filePath, - }); - } - } - })() - .catch(error => logger.error(`${storageHelperLogPrefix} Failed to initialize storage: ${error}`)); - } - - return new StorageHelper({ - logger, - }, key, { - autoInit: true, - defaultValue, - storage: { - async getItem(key: string) { - await when(() => storage.loaded); - - return storage.data[key] as T; - }, - setItem(key: string, value: T) { - storage.data[key] = value; - }, - removeItem(key: string) { - delete storage.data[key]; - }, - }, - }); -}; diff --git a/src/renderer/utils/create-storage/initialize-state.injectable.ts b/src/renderer/utils/create-storage/initialize-state.injectable.ts new file mode 100644 index 0000000000..2c0d832c9e --- /dev/null +++ b/src/renderer/utils/create-storage/initialize-state.injectable.ts @@ -0,0 +1,75 @@ +/** + * 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 AwaitLock from "await-lock"; +import { comparer, reaction, runInAction, toJS } from "mobx"; +import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; +import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable"; +import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; +import setupAppPathsInjectable from "../../app-paths/setup-app-paths.injectable"; +import { beforeFrameStartsFirstInjectionToken } from "../../before-frame-starts/tokens"; +import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable"; +import { storageHelperLogPrefix } from "../storage-helper"; +import lensLocalStorageStateInjectable from "./state.injectable"; +import storageSaveDelayInjectable from "./storage-save-delay.injectable"; + +const initializeStateInjectable = getInjectable({ + id: "initialize-lens-local-storage-state", + instantiate: (di) => ({ + id: "initialize-lens-local-storage-state", + run: async () => { + const joinPaths = di.inject(joinPathsInjectable); + const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable); + const hostedClusterId = di.inject(hostedClusterIdInjectable); + const lensLocalStorageState = di.inject(lensLocalStorageStateInjectable); + const readJsonFile = di.inject(readJsonFileInjectable); + const writeJsonFile = di.inject(writeJsonFileInjectable); + const logger = di.inject(loggerInjectable); + const storageSaveDelay = di.inject(storageSaveDelayInjectable); + const lock = new AwaitLock(); + + const filePath = joinPaths(directoryForLensLocalStorage, `${hostedClusterId || "app"}.json`); + + try { + const localFile = await readJsonFile(filePath); + + if (typeof localFile === "object") { + runInAction(() => { + Object.assign(lensLocalStorageState, localFile); + }); + } + } catch { + // do nothing + } finally { + logger.info(`${storageHelperLogPrefix} loading finished for ${filePath}`); + } + + reaction(() => toJS(lensLocalStorageState), saveFile, { + delay: storageSaveDelay, // lazy, avoid excessive writes to fs + equals: comparer.structural, // save only when something really changed + }); + + async function saveFile(state: Record) { + try { + await lock.acquireAsync(); + logger.info(`${storageHelperLogPrefix} saving ${filePath}`); + await writeJsonFile(filePath, state); + } catch (error) { + logger.error(`${storageHelperLogPrefix} saving failed: ${error}`, { + json: state, jsonFilePath: filePath, + }); + } finally { + lock.release(); + } + } + }, + runAfter: di.inject(setupAppPathsInjectable), + }), + injectionToken: beforeFrameStartsFirstInjectionToken, +}); + +export default initializeStateInjectable; diff --git a/src/renderer/utils/create-storage/state.injectable.ts b/src/renderer/utils/create-storage/state.injectable.ts new file mode 100644 index 0000000000..dc5f632afa --- /dev/null +++ b/src/renderer/utils/create-storage/state.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 { observable } from "mobx"; + +const lensLocalStorageStateInjectable = getInjectable({ + id: "lens-local-storage-state", + instantiate: () => observable.object({} as Record), +}); + +export default lensLocalStorageStateInjectable; diff --git a/src/renderer/utils/create-storage/storages-are-ready.ts b/src/renderer/utils/create-storage/storages-are-ready.ts deleted file mode 100644 index 8489138d1d..0000000000 --- a/src/renderer/utils/create-storage/storages-are-ready.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { DiContainer } from "@ogre-tools/injectable"; -import { runInAction } from "mobx"; -import type { CreateStorage } from "./create-storage"; -import createStorageInjectable from "./create-storage.injectable"; - -export const controlWhenStoragesAreReady = (di: DiContainer) => { - const storagesAreReady: Promise[] = []; - - const decorated = - (toBeDecorated: CreateStorage) => - (key: string, defaultValue: any) => { - const storage = toBeDecorated(key, defaultValue); - - storagesAreReady.push(storage.whenReady); - - return storage; - }; - - runInAction(() => { - // TODO: Remove when typing is added to the library - (di as any).decorateFunction(createStorageInjectable, decorated); - }); - - return async () => { - await Promise.all(storagesAreReady); - }; -}; diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index b4e315be17..cde1a70077 100755 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -17,4 +17,4 @@ export * from "./metricUnitsToNumber"; export * from "./name-parts"; export * from "./prevDefault"; export * from "./saveFile"; -export * from "./storageHelper"; +export * from "./storage-helper"; diff --git a/src/renderer/utils/storageHelper.ts b/src/renderer/utils/storage-helper.ts similarity index 67% rename from src/renderer/utils/storageHelper.ts rename to src/renderer/utils/storage-helper.ts index 4f6dcec88a..4ca4957bd8 100755 --- a/src/renderer/utils/storageHelper.ts +++ b/src/renderer/utils/storage-helper.ts @@ -4,7 +4,7 @@ */ // Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.) -import { action, comparer, computed, makeObservable, observable, observe, toJS, when } from "mobx"; +import { action, comparer, computed, makeObservable, observable, observe, toJS } from "mobx"; import type { Draft } from "immer"; import { produce, isDraft } from "immer"; import { isEqual, isPlainObject } from "lodash"; @@ -19,14 +19,13 @@ export interface StorageChange { export interface StorageAdapter { [metadata: string]: unknown; - getItem(key: string): T | Promise; + getItem(key: string): T; setItem(key: string, value: T): void; removeItem(key: string): void; onChange?(change: StorageChange): void; } export interface StorageHelperOptions { - readonly autoInit?: boolean; // start preloading data immediately, default: true readonly storage: StorageAdapter; readonly defaultValue: T; } @@ -34,8 +33,6 @@ export interface StorageHelperOptions { export interface StorageLayer { isDefaultValue(val: T): boolean; get(): T; - readonly value: T; - readonly whenReady: Promise; set(value: T): void; reset(): void; merge(value: Partial | ((draft: Draft) => Partial | void)): void; @@ -43,76 +40,44 @@ export interface StorageLayer { export const storageHelperLogPrefix = "[STORAGE-HELPER]:"; -interface Dependencies { +export interface StorageHelperDependencies { readonly logger: Logger; } export class StorageHelper implements StorageLayer { readonly storage: StorageAdapter; - private readonly data = observable.box(undefined, { + private readonly data = observable.box(undefined, { deep: true, equals: comparer.structural, }); - @observable initialized = false; - - get whenReady() { - return when(() => this.initialized); - } + private readonly value = computed(() => this.data.get() ?? this.defaultValue); get defaultValue(): T { // return as-is since options.defaultValue might be a getter too return this.options.defaultValue; } - constructor(private readonly dependencies: Dependencies, readonly key: string, private readonly options: StorageHelperOptions) { + constructor(private readonly dependencies: StorageHelperDependencies, readonly key: string, private readonly options: StorageHelperOptions) { makeObservable(this); - const { storage, autoInit = true } = options; - - this.storage = storage; + this.storage = this.options.storage; observe(this.data, (change) => { this.onChange(change.newValue as T | undefined, change.oldValue as T | undefined); }); - if (autoInit) { - this.init(); - } - } - - private onData = (data: T): void => { - const notEmpty = data != null; - const notDefault = !this.isDefaultValue(data); - - if (notEmpty && notDefault) { - this.set(data); - } - - this.initialized = true; - }; - - private onError = (error: any): void => { - this.dependencies.logger.error(`${storageHelperLogPrefix} loading error: ${error}`, this); - }; - - @action - init({ force = false } = {}) { - if (this.initialized && !force) { - return; - } - try { const data = this.storage.getItem(this.key); + const notEmpty = data != null; + const notDefault = !this.isDefaultValue(data); - if (data instanceof Promise) { - data.then(this.onData, this.onError); - } else { - this.onData(data); + if (notEmpty && notDefault) { + this.set(data); } } catch (error) { - this.onError(error); + this.dependencies.logger.error(`${storageHelperLogPrefix} loading error: ${error}`, this); } } @@ -120,9 +85,7 @@ export class StorageHelper implements StorageLayer { return isEqual(value, this.defaultValue); } - protected onChange(value: T | undefined, oldValue: T | undefined) { - if (!this.initialized) return; - + private onChange(value: T | undefined, oldValue: T | undefined) { try { if (value == null) { this.storage.removeItem(this.key); @@ -137,18 +100,13 @@ export class StorageHelper implements StorageLayer { } get(): T { - return this.value; - } - - @computed - get value(): T { - return this.data.get() ?? this.defaultValue; + return this.value.get(); } @action set(value: T) { if (this.isDefaultValue(value)) { - this.reset(); + this.data.set(undefined); } else { this.data.set(value); }