From e04a980e5b72f6eb29e600783e3df4a136f237b3 Mon Sep 17 00:00:00 2001 From: MatthewPattell Date: Wed, 29 May 2024 14:51:44 +0200 Subject: [PATCH] feat: exclude props from export - add storage serialize behaviour --- package.json | 2 +- rollup.config.js | 3 +- src/deep-compare.ts | 29 ++++++++++++++++++ src/make-exported.ts | 10 +++++-- src/manager.ts | 21 ++++++++------ src/storages/combined-storage.ts | 50 +++++++++++++++++++------------- src/types.ts | 2 ++ src/wakeup.ts | 4 +-- 8 files changed, 85 insertions(+), 36 deletions(-) create mode 100644 src/deep-compare.ts diff --git a/package.json b/package.json index b1a619c..02db957 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "homepage": "https://github.com/Lomray-Software/react-mobx-manager", "scripts": { "build": "rollup -c", - "build:watch": "rollup -c -w --environment BUILD:development", + "build:watch": "rollup -c -w", "lint:check": "eslint \"src/**/*.{ts,tsx,*.ts,*tsx}\"", "lint:format": "eslint --fix \"src/**/*.{ts,tsx,*.ts,*tsx}\"", "ts:check": "tsc --project ./tsconfig.checks.json --skipLibCheck --noemit", diff --git a/rollup.config.js b/rollup.config.js index 47b1314..19d818f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,8 +3,7 @@ import { folderInput } from 'rollup-plugin-folder-input'; import copy from 'rollup-plugin-copy'; import terser from '@rollup/plugin-terser'; -const IS_DEVELOP_BUILD = process.env.BUILD === 'development' -const dest = IS_DEVELOP_BUILD ? 'example/node_modules/@lomray/react-mobx-manager' : 'lib'; +const dest = 'lib'; export default { input: [ diff --git a/src/deep-compare.ts b/src/deep-compare.ts new file mode 100644 index 0000000..54144bc --- /dev/null +++ b/src/deep-compare.ts @@ -0,0 +1,29 @@ +/** + * Deep compare two objects + */ +const deepCompare = (obj1: unknown, obj2: unknown): boolean => { + if (obj1 === obj2) { + return true; + } + + if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) { + return false; + } + + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + if (keys1.length !== keys2.length) { + return false; + } + + for (const key of keys1) { + if (!keys2.includes(key) || !deepCompare(obj1[key], obj2[key])) { + return false; + } + } + + return true; +}; + +export default deepCompare; diff --git a/src/make-exported.ts b/src/make-exported.ts index 11a548b..cae2753 100644 --- a/src/make-exported.ts +++ b/src/make-exported.ts @@ -9,7 +9,7 @@ const exportedPropName = 'libExported'; const makeExported = ( store: T, props: { - [P in Exclude]?: 'observable' | 'simple'; + [P in Exclude]?: 'observable' | 'simple' | 'excluded'; }, ): void => { store[exportedPropName] = props; @@ -27,4 +27,10 @@ const isPropObservableExported = (store: TAnyStore, prop: string): boolean => const isPropSimpleExported = (store: TAnyStore, prop: string): boolean => store?.[exportedPropName]?.[prop] === 'simple'; -export { makeExported, isPropObservableExported, isPropSimpleExported }; +/** + * Check if store prop is excluded from export + */ +const isPropExcludedFromExport = (store: TAnyStore, prop: string): boolean => + store?.[exportedPropName]?.[prop] === 'excluded'; + +export { makeExported, isPropObservableExported, isPropSimpleExported, isPropExcludedFromExport }; diff --git a/src/manager.ts b/src/manager.ts index ecd3cee..fd83ff3 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -3,7 +3,11 @@ import { isObservableProp, toJS } from 'mobx'; import { ROOT_CONTEXT_ID } from './constants'; import deepMerge from './deep-merge'; import Events from './events'; -import { isPropObservableExported, isPropSimpleExported } from './make-exported'; +import { + isPropExcludedFromExport, + isPropObservableExported, + isPropSimpleExported, +} from './make-exported'; import onChangeListener from './on-change-listener'; import CombinedStorage from './storages/combined-storage'; import StoreStatus from './store-status'; @@ -621,7 +625,8 @@ class Manager { return Object.entries(props).reduce( (res, [prop, value]) => ({ ...res, - ...(isObservableProp(store, prop) || isPropSimpleExported(store, prop) + ...((isObservableProp(store, prop) && !isPropExcludedFromExport(store, prop)) || + isPropSimpleExported(store, prop) ? { [prop]: value } : {}), ...(isPropObservableExported(store, prop) @@ -640,16 +645,14 @@ class Manager { id: string, options: IPersistOptions = {}, ): IConstructableStore { - if (Manager.persistedStores.has(id)) { - console.warn(`Duplicate serializable store key: ${id}`); - - return store; - } - Manager.persistedStores.add(id); store.libStoreId = id; - store.libStorageOptions = options; + + // add storage options + if (!('libStorageOptions' in store.prototype)) { + store.prototype.libStorageOptions = options; + } // add default wakeup handler if (!('wakeup' in store.prototype)) { diff --git a/src/storages/combined-storage.ts b/src/storages/combined-storage.ts index d373930..b552c2e 100644 --- a/src/storages/combined-storage.ts +++ b/src/storages/combined-storage.ts @@ -1,3 +1,4 @@ +import deepCompare from '../deep-compare'; import type { IPersistOptions, IStorage, IStorePersisted } from '../types'; interface ICombinedStorage { @@ -89,6 +90,7 @@ class CombinedStorage implements IStorage { attributes: { [this.defaultId]: ['*'], }, + behaviour: 'exclude', ...(store.libStorageOptions ?? {}), }; } @@ -130,28 +132,36 @@ class CombinedStorage implements IStorage { data: Record | undefined, ): Promise { const storeId = store.libStoreId!; - const { attributes } = this.getStoreOptions(store); - const dataKeys = Object.keys(data ?? {}); + const { attributes, behaviour } = this.getStoreOptions(store); + const dataKeys = new Set(Object.keys(data ?? {})); const dataByStorages = Object.entries(attributes!).map(([storageId, attr]) => { - const storeData = - attr[0] === '*' - ? data - : attr.reduce( - (r, attrName) => ({ - ...r, - ...(dataKeys.includes(attrName) ? { [attrName]: data?.[attrName] } : {}), - }), - {}, - ); - - return this.set( - { - ...(this.persistData?.[storageId] ?? {}), - [storeId]: storeData, - } as Record, - storageId, - ); + const storeData = (attr[0] === '*' ? [...dataKeys] : attr).reduce((r, attrName) => { + if (!dataKeys.has(attrName)) { + return r; + } + + if (behaviour === 'exclude') { + dataKeys.delete(attrName); + } + + return { + ...r, + [attrName]: data?.[attrName], + }; + }, {}); + + const newData = { + ...(this.persistData?.[storageId] ?? {}), + [storeId]: storeData, + } as Record; + + // skip updating if nothing changed + if (deepCompare(this.persistData?.[storageId]?.[storeId] ?? {}, storeData)) { + return null; + } + + return this.set(newData, storageId); }); await Promise.all(dataByStorages); diff --git a/src/types.ts b/src/types.ts index 1c5bebd..542ee26 100644 --- a/src/types.ts +++ b/src/types.ts @@ -149,6 +149,8 @@ export interface IGroupedStores { } export interface IPersistOptions { + // default: exclude. Exclude - except attributes from other storages + behaviour?: 'exclude' | 'include'; attributes?: { // storageId => attributes, * - all attributes // first storage => *, by default diff --git a/src/wakeup.ts b/src/wakeup.ts index 6a0dd72..ced31c1 100644 --- a/src/wakeup.ts +++ b/src/wakeup.ts @@ -1,11 +1,11 @@ import deepMerge from './deep-merge'; -import type { TStores, TWakeup } from './types'; +import type { IStorePersisted, TWakeup } from './types'; /** * Restore persisted store state */ function wakeup( - this: TStores[string], + this: IStorePersisted, { initState, persistedState, manager }: Parameters[0], ) { const resState = {};