From a27ce9cf8b2bf8ef0a215d075beaab3625ceb523 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 6 Jul 2020 10:21:51 +0300 Subject: [PATCH] use common/base-store for config-file based stores (e.g. user-store) Signed-off-by: Roman --- src/common/base-store.ts | 124 ++++++++++++++++++ src/common/user-store.ts | 91 +++---------- src/common/utils/singleton.ts | 1 - src/renderer/_vue/components/WhatsNewPage.vue | 7 +- src/renderer/_vue/index.js | 6 +- src/renderer/_vue/store/index.js | 6 +- 6 files changed, 151 insertions(+), 84 deletions(-) create mode 100644 src/common/base-store.ts diff --git a/src/common/base-store.ts b/src/common/base-store.ts new file mode 100644 index 0000000000..eaf1154a5a --- /dev/null +++ b/src/common/base-store.ts @@ -0,0 +1,124 @@ +import path from "path" +import Config from "conf" +import { Options as ConfOptions } from "conf/dist/source/types" +import { app, remote } from "electron" +import { observable, reaction, toJS, when } from "mobx"; +import Singleton from "./utils/singleton"; +import isEqual from "lodash/isEqual" +import { getAppVersion } from "./utils/app-version"; + +export interface BaseStoreParams { + configName: string; + autoLoad?: boolean; + syncEnabled?: boolean; + confOptions?: ConfOptions; +} + +export class BaseStore extends Singleton { + protected storeConfig: Config; + protected syncDisposers: Function[] = []; + public whenLoaded = when(() => this.isLoaded); + + @observable isLoaded = false; + @observable protected data: T; + + protected constructor(protected params: BaseStoreParams) { + super(); + this.params = { + autoLoad: true, + syncEnabled: true, + ...params, + } + this.onConfigChange = this.onConfigChange.bind(this) + this.onModelChange = this.onModelChange.bind(this) + this.init(); + } + + get name() { + return path.basename(this.storeConfig.path); + } + + get storeModel(): T { + const storeModel = { ...(this.storeConfig.store || {}) }; + Reflect.deleteProperty(storeModel, "__internal__"); // fixme: avoid "external-internals" + return storeModel as T; + } + + protected async init() { + if (this.params.autoLoad) { + await this.load(); + } + if (this.params.syncEnabled) { + await this.whenLoaded; + this.enableSync(); + } + } + + protected async load() { + const { configName, syncEnabled, confOptions = {} } = this.params; + + // use "await" to make pseudo-async "load" for more future-proof usages + this.storeConfig = await new Config({ + projectName: "lens", + projectVersion: getAppVersion(), + configName: configName, + watch: syncEnabled, // watch for changes in multi-process app (e.g. main/renderer) + cwd: (app || remote.app).getPath("userData"), // todo: remove remote.app in favor ipc.invoke + ...confOptions, + }); + const data = this.storeConfig.store; + console.info(`[STORE]: [LOADED] ${this.storeConfig.path}`, data); + this.fromStore(data); + this.isLoaded = true; + } + + enableSync() { + const onConfigChangeStop = this.storeConfig.onDidAnyChange(this.onConfigChange); + const onModelChangeStop = reaction(() => this.toJSON(), this.onModelChange); + + this.syncDisposers.push( + onConfigChangeStop, // watch for changes from file-system updates + onModelChangeStop, // refresh config file from runtime + ); + } + + disableSync() { + this.syncDisposers.forEach(dispose => dispose()); + this.syncDisposers.length = 0; + } + + protected onConfigChange(data: T, oldValue: Partial) { + if (!isEqual(this.toJSON(), data)) { + console.info(`[STORE]: [UPDATE] from ${this.name}`, { data, oldValue }); + this.fromStore(data); + } + } + + protected onModelChange(model: T) { + if (!isEqual(this.storeModel, model)) { + console.info(`[STORE]: [SAVE] ${this.name} from runtime update`, { + data: model, + oldValue: this.storeModel + }); + // fixme: https://github.com/sindresorhus/conf/issues/114 + Object.entries(model).forEach(([key, value]) => { + this.storeConfig.set(key, value); + }); + } + } + + // todo: use "serializr" ? + protected fromStore(data: Partial = {}) { + this.data = data as T; + } + + toJSON(): T { + return toJS(this.data, { + recurseEverything: true, + }) + } + + * [Symbol.iterator]() { + yield* Object.entries(this.toJSON()); + } +} diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 09f3b765ea..a3ef9ac944 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -1,13 +1,9 @@ -import path from "path" -import { app, remote } from "electron" -import { observable, reaction, toJS } from "mobx"; -import Config from "conf" import semver from "semver" +import { observable, reaction, toJS } from "mobx"; +import { BaseStore } from "./base-store"; import migrations from "../migrations/user-store" -import Singleton from "./utils/singleton"; import { getAppVersion } from "./utils/app-version"; import { tracker } from "./tracker"; -import isEqual from "lodash/isEqual" export interface UserStoreModel { lastSeenAppVersion: string; @@ -23,10 +19,7 @@ export interface UserPreferences { downloadMirror?: string | "default"; } -export class UserStore extends Singleton { - private storeConfig: Config; - - @observable isReady = false; +export class UserStore extends BaseStore { @observable lastSeenAppVersion = "0.0.0" @observable seenContexts = observable.set(); @@ -36,67 +29,11 @@ export class UserStore extends Singleton { downloadMirror: "default", }; - get name() { - return path.basename(this.storeConfig.path); - } - - get hasNewAppVersion() { - return semver.gt(getAppVersion(), this.lastSeenAppVersion); - } - - get storeModel() { - const storeModel = { ...this.storeConfig.store }; - Reflect.deleteProperty(storeModel, "__internal__"); // fixme: avoid "external-internals" - return storeModel; - } - - saveLastSeenAppVersion() { - this.lastSeenAppVersion = getAppVersion(); - } - - private constructor() { - super(); - this.init(); - } - - protected async init() { - await this.load(); - this.bindEvents(); - this.isReady = true; - } - - protected async load() { - this.storeConfig = new Config({ + protected constructor() { + super({ configName: "lens-user-store", - migrations: migrations, - cwd: (app || remote.app).getPath("userData"), // todo: move to main process + with ipc.invoke - watch: true, // enable onDidChange()-callback - }); - const data = this.storeConfig.store; - console.info(`[STORE]: [LOADED] ${this.storeConfig.path}`, data); - this.fromStore(data); - } - - protected bindEvents() { - // refresh from file-system updates - this.storeConfig.onDidAnyChange((data, oldValue) => { - if (!isEqual(this.toJSON(), data)) { - console.info(`[STORE]: [UPDATE] from ${this.name}`, { data, oldValue }); - this.fromStore(data); - } - }); - - // refresh config file from runtime - reaction(() => this.toJSON(), (model: UserStoreModel) => { - if (!isEqual(this.storeModel, model)) { - console.info(`[STORE]: [SAVE] ${this.name} from runtime update`, { - data: model, - oldValue: this.storeModel - }); - // fixme: https://github.com/sindresorhus/conf/issues/114 - Object.entries(model).forEach(([key, value]) => { - this.storeConfig.set(key, value); - }); + confOptions: { + migrations: migrations } }); @@ -106,7 +43,14 @@ export class UserStore extends Singleton { }); } - // todo: maybe use "serializr" + get hasNewAppVersion() { + return semver.gt(getAppVersion(), this.lastSeenAppVersion); + } + + saveLastSeenAppVersion() { + this.lastSeenAppVersion = getAppVersion(); + } + protected fromStore(data: Partial = {}) { const { lastSeenAppVersion, seenContexts, preferences } = data if (lastSeenAppVersion) { @@ -120,7 +64,7 @@ export class UserStore extends Singleton { } } - protected toJSON(): UserStoreModel { + toJSON() { return toJS({ lastSeenAppVersion: this.lastSeenAppVersion, seenContexts: Array.from(this.seenContexts), @@ -131,4 +75,5 @@ export class UserStore extends Singleton { } } -export const userStore: UserStore = UserStore.getInstance(); +const userStore: UserStore = UserStore.getInstance(); +export { userStore } diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts index 6ecb89c1cf..b940f2bfbb 100644 --- a/src/common/utils/singleton.ts +++ b/src/common/utils/singleton.ts @@ -6,7 +6,6 @@ * const usersStore: UsersStore = UsersStore.getInstance(); */ -// todo: maybe convert to @decorator class Singleton { private static instances = new WeakMap(); diff --git a/src/renderer/_vue/components/WhatsNewPage.vue b/src/renderer/_vue/components/WhatsNewPage.vue index 5ae3df00a0..8766f54868 100644 --- a/src/renderer/_vue/components/WhatsNewPage.vue +++ b/src/renderer/_vue/components/WhatsNewPage.vue @@ -48,10 +48,9 @@ export default { }, methods: { toLanding: async function() { - if(userStore.hasNewAppVersion) { - userStore.saveLastSeenAppVersion(); - tracker.event("app", "whats-new-seen") - } + userStore.saveLastSeenAppVersion(); + tracker.event("app", "whats-new-seen") + // await this.$store.dispatch("updateLastSeenAppVersion") this.$router.push({ name: "landing-page", diff --git a/src/renderer/_vue/index.js b/src/renderer/_vue/index.js index fae0206b81..699647f46a 100644 --- a/src/renderer/_vue/index.js +++ b/src/renderer/_vue/index.js @@ -10,7 +10,6 @@ import App from './App' import router from './router' import store from './store' import { userStore } from "../../common/user-store" -import { when } from "mobx" const promiseIpc = new PromiseIpc({maxTimeoutMs: 6000}); @@ -30,10 +29,11 @@ Vue.mixin({ // any initialization we want to do for app state setTimeout(async () => { - await when(() => userStore.isReady); + await userStore.whenLoaded; await store.dispatch('init') + new Vue({ - components: { App }, + components: {App}, store, router, template: '' diff --git a/src/renderer/_vue/store/index.js b/src/renderer/_vue/store/index.js index 1693a6604e..fb9d30edad 100644 --- a/src/renderer/_vue/store/index.js +++ b/src/renderer/_vue/store/index.js @@ -1,6 +1,5 @@ import Vue from 'vue' import Vuex from 'vuex' -import semver from "semver" import { userStore } from "../../../common/user-store" import { getAppVersion } from "../../../common/utils/app-version" import KubeContexts from './modules/kube-contexts' @@ -72,10 +71,11 @@ export default new Vuex.Store({ seenContexts: state => state.seenContexts, hud: state => state.hud, isMenuVisible: function (state, getters) { - return state.hud.isMenuVisible && !getters.showWhatsNew; + if (userStore.hasNewAppVersion) return false; + return state.hud.isMenuVisible; }, showWhatsNew: function (state) { - return semver.gt(getAppVersion(), state.lastSeenAppVersion); + return userStore.hasNewAppVersion; }, preferences: state => state.preferences, }