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

use common/base-store for config-file based stores (e.g. user-store)

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-06 10:21:51 +03:00
parent 5c8dd89a88
commit a27ce9cf8b
6 changed files with 151 additions and 84 deletions

124
src/common/base-store.ts Normal file
View File

@ -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<T = any> {
configName: string;
autoLoad?: boolean;
syncEnabled?: boolean;
confOptions?: ConfOptions<T>;
}
export class BaseStore<T = any> extends Singleton {
protected storeConfig: Config<T>;
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<T>) {
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<T> = {}) {
this.data = data as T;
}
toJSON(): T {
return toJS(this.data, {
recurseEverything: true,
})
}
* [Symbol.iterator]() {
yield* Object.entries(this.toJSON());
}
}

View File

@ -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<UserStoreModel>;
@observable isReady = false;
export class UserStore extends BaseStore<UserStoreModel> {
@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<UserStoreModel>({
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<UserStoreModel> = {}) {
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 }

View File

@ -6,7 +6,6 @@
* const usersStore: UsersStore = UsersStore.getInstance();
*/
// todo: maybe convert to @decorator
class Singleton {
private static instances = new WeakMap<object, Singleton>();

View File

@ -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",

View File

@ -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: '<App/>'

View File

@ -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,
}