From dbfca224d812c2ba80a48852ba047bb65c4c6dd9 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 4 Aug 2022 01:56:52 +0300 Subject: server-side storage for flags --- src/modules/serverSideStorage.js | 158 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/modules/serverSideStorage.js (limited to 'src/modules/serverSideStorage.js') diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js new file mode 100644 index 00000000..5ebe17fd --- /dev/null +++ b/src/modules/serverSideStorage.js @@ -0,0 +1,158 @@ +import { toRaw } from 'vue' + +const VERSION = 1 +const NEW_USER_DATE = new Date('04-08-2022') // date of writing this, basically + +const COMMAND_TRIM_FLAGS = 1000 +const COMMAND_TRIM_FLAGS_AND_RESET = 1001 + +const defaultState = { + // last timestamp + timestamp: 0, + // need to update server + dirty: false, + // storage of flags - stuff that can only be set and incremented + flagStorage: { + updateCounter: 0, // Counter for most recent update notification seen + // TODO move to prefsStorage when that becomes a thing since only way + // this can be reset is by complete reset of all flags + dontShowUpdateNotifs: 0, // if user chose to not show update notifications ever again + reset: 0 // special flag that can be used to force-reset all flags, debug purposes only + // special reset codes: + // 1000: trim keys to those known by currently running FE + // 1001: same as above + reset everything to 0 + }, + // raw data + raw: null, + // local cache + cache: null +} + +const newUserFlags = { + ...defaultState.flagStorage, + updateCounter: 1 // new users don't need to see update notification +} + +const serverSideStorage = { + state: { + ...defaultState + }, + mutations: { + setServerSideStorage (state, userData) { + const live = userData.storage + const userNew = userData.created_at > NEW_USER_DATE + const flagsTemplate = userNew ? newUserFlags : defaultState.defaultState + state.raw = live + console.log(1111, live._timestamp) + let recent = null + const cache = state.cache || {} + const cacheValid = cache._timestamp > 0 && cache._version > 0 + const liveValid = live._timestamp > 0 && live._version > 0 + if (!liveValid) { + state.dirty = true + console.debug('Nothing valid stored on server, assuming cache to be source of truth') + if (cacheValid) { + recent = cache + } else { + console.debug(`Local cache is empty, initializing for ${userNew ? 'new' : 'existing'} user`) + + recent = { + _timestamp: Date.now(), + _version: VERSION, + flagStorage: { ...flagsTemplate } + } + } + } else if (!cacheValid) { + console.debug('Valid storage on server found, no local cache found, using live as source of truth') + recent = live + } else { + console.debug('Both sources have valid data, figuring things out...') + console.log(live._timestamp, cache._timestamp) + if (live._timestamp === cache._timestamp && live._version === cache._version) { + console.debug('Same version/timestamp on both source, source of truth irrelevant') + recent = cache + } else { + state.dirty = true + console.debug('Different timestamp, figuring out which one is more recent') + let stale + if (live._timestamp < cache._timestamp) { + recent = cache + stale = live + } else { + recent = live + stale = cache + } + + // Merge the flags + console.debug('Merging the flags...') + recent.flagStorage = recent.flagStorage || { ...flagsTemplate } + stale.flagStorage = stale.flagStorage || { ...flagsTemplate } + const allFlags = Array.from(new Set([ + ...Object.keys(toRaw(recent.flagStorage)), + ...Object.keys(toRaw(stale.flagStorage)) + ])) + + const totalFlags = Object.fromEntries(allFlags.map(flag => { + const recentFlag = recent.flagStorage[flag] + const staleFlag = stale.flagStorage[flag] + // use flag that is of higher value + return [flag, recentFlag > staleFlag ? recentFlag : staleFlag] + })) + + console.debug('AAA', totalFlags) + // flag reset functionality + if (totalFlags.reset >= COMMAND_TRIM_FLAGS && totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET) { + console.debug('Received command to trim the flags') + const knownKeys = new Set(Object.keys(defaultState.flagStorage)) + allFlags.forEach(flag => { + if (!knownKeys.has(flag)) { + delete totalFlags[flag] + } + }) + if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) { + // 1001 - and reset everything to 0 + console.debug('Received command to reset the flags') + allFlags.forEach(flag => { totalFlags[flag] = 0 }) + } else { + // reset the reset 0 + totalFlags.reset = 0 + } + } else if (totalFlags.reset > 0 && totalFlags.reset < 9000) { + console.debug('Received command to reset the flags') + allFlags.forEach(flag => { totalFlags[flag] = 0 }) + // for good luck + totalFlags.reset = 0 + } + console.log('AAAA', totalFlags) + state.cache.flagStorage = totalFlags + } + } + state.cache = recent + state.flagStorage = state.cache.flagStorage + }, + setFlag (state, { flag, value }) { + state.flagStorage[flag] = value + state.dirty = true + } + }, + actions: { + pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { + console.log('PUSH') + const needPush = state.dirty || force + if (!needPush) return + state.cache = { + _timestamp: Date.now(), + _version: VERSION, + flagStorage: toRaw(state.flagStorage) + } + console.log('YES') + const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } + rootState.api.backendInteractor + .updateProfile({ params }) + .then((user) => commit('setServerSideStorage', user)) + state.dirty = false + } + } +} + +export default serverSideStorage -- cgit v1.2.3-70-g09d2 From 9c00610d0031969cce4d50c0f947098a632ca712 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 4 Aug 2022 17:20:11 +0300 Subject: refactoring --- src/modules/serverSideStorage.js | 251 ++++++++++++++++++++++++--------------- 1 file changed, 155 insertions(+), 96 deletions(-) (limited to 'src/modules/serverSideStorage.js') diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 5ebe17fd..da908728 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -1,15 +1,14 @@ import { toRaw } from 'vue' +import { isEqual } from 'lodash' -const VERSION = 1 -const NEW_USER_DATE = new Date('04-08-2022') // date of writing this, basically +export const VERSION = 1 +export const NEW_USER_DATE = new Date('04-08-2022') // date of writing this, basically -const COMMAND_TRIM_FLAGS = 1000 -const COMMAND_TRIM_FLAGS_AND_RESET = 1001 +export const COMMAND_TRIM_FLAGS = 1000 +export const COMMAND_TRIM_FLAGS_AND_RESET = 1001 const defaultState = { - // last timestamp - timestamp: 0, - // need to update server + // do we need to update data on server? dirty: false, // storage of flags - stuff that can only be set and incremented flagStorage: { @@ -33,6 +32,113 @@ const newUserFlags = { updateCounter: 1 // new users don't need to see update notification } +const _wrapData = (data) => ({ + ...data, + _timestamp: Date.now(), + _version: VERSION +}) + +export const _checkValidity = (data) => data._timestamp > 0 && data._version > 0 + +export const _getRecentData = (cache, live) => { + const result = { recent: null, stale: null, needUpload: false } + const cacheValid = _checkValidity(cache || {}) + const liveValid = _checkValidity(live || {}) + if (!liveValid) { + result.needUpload = true + console.debug('Nothing valid stored on server, assuming cache to be source of truth') + result.recent = cache + result.stale = live + } else if (!cacheValid) { + console.debug('Valid storage on server found, no local cache found, using live as source of truth') + result.recent = live + result.stale = cache + } else { + console.debug('Both sources have valid data, figuring things out...') + console.log(live._timestamp, cache._timestamp) + if (live._timestamp === cache._timestamp && live._version === cache._version) { + console.debug('Same version/timestamp on both source, source of truth irrelevant') + result.recent = cache + result.stale = live + } else { + console.debug('Different timestamp, figuring out which one is more recent') + if (live._timestamp < cache._timestamp) { + result.recent = cache + result.stale = live + } else { + result.recent = live + result.stale = cache + } + } + } + return result +} + +export const _getAllFlags = (recent = {}, stale = {}) => { + return Array.from(new Set([ + ...Object.keys(toRaw(recent.flagStorage || {})), + ...Object.keys(toRaw(stale.flagStorage || {})) + ])) +} + +export const _mergeFlags = (recent, stale, allFlagKeys) => { + return Object.fromEntries(allFlagKeys.map(flag => { + const recentFlag = recent.flagStorage[flag] + const staleFlag = stale.flagStorage[flag] + // use flag that is of higher value + return [flag, recentFlag > staleFlag ? recentFlag : staleFlag] + })) +} + +export const _resetFlags = (totalFlags, allFlagKeys) => { + // flag reset functionality + if (totalFlags.reset >= COMMAND_TRIM_FLAGS && totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET) { + console.debug('Received command to trim the flags') + const knownKeys = new Set(Object.keys(defaultState.flagStorage)) + allFlagKeys.forEach(flag => { + if (!knownKeys.has(flag)) { + delete totalFlags[flag] + } + }) + if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) { + // 1001 - and reset everything to 0 + console.debug('Received command to reset the flags') + allFlagKeys.forEach(flag => { totalFlags[flag] = 0 }) + } else { + // reset the reset 0 + totalFlags.reset = 0 + } + } else if (totalFlags.reset > 0 && totalFlags.reset < 9000) { + console.debug('Received command to reset the flags') + allFlagKeys.forEach(flag => { totalFlags[flag] = 0 }) + // for good luck + totalFlags.reset = 0 + } +} + +export const _doMigrations = (cache) => { + if (cache._version < VERSION) { + console.debug('Local cached data has older version, seeing if there any migrations that can be applied') + + // no migrations right now since we only have one version + console.debug('No migrations found') + } + + if (cache._version > VERSION) { + console.debug('Local cached data has newer version, seeing if there any reverse migrations that can be applied') + + // no reverse migrations right now but we leave a possibility of loading a hotpatch if need be + if (window._PLEROMA_HOTPATCH) { + if (window._PLEROMA_HOTPATCH.reverseMigrations) { + console.debug('Found hotpatch migration, applying') + return window._PLEROMA_HOTPATCH.reverseMigrations('serverSideStorage', { from: cache._version, to: VERSION }, cache) + } + } + } + + return cache +} + const serverSideStorage = { state: { ...defaultState @@ -40,93 +146,50 @@ const serverSideStorage = { mutations: { setServerSideStorage (state, userData) { const live = userData.storage + state.raw = live + let cache = state.cache + + cache = _doMigrations(cache) + + let { recent, stale, needsUpload } = _getRecentData(cache, live) + const userNew = userData.created_at > NEW_USER_DATE const flagsTemplate = userNew ? newUserFlags : defaultState.defaultState - state.raw = live - console.log(1111, live._timestamp) - let recent = null - const cache = state.cache || {} - const cacheValid = cache._timestamp > 0 && cache._version > 0 - const liveValid = live._timestamp > 0 && live._version > 0 - if (!liveValid) { - state.dirty = true - console.debug('Nothing valid stored on server, assuming cache to be source of truth') - if (cacheValid) { - recent = cache - } else { - console.debug(`Local cache is empty, initializing for ${userNew ? 'new' : 'existing'} user`) - - recent = { - _timestamp: Date.now(), - _version: VERSION, - flagStorage: { ...flagsTemplate } - } - } - } else if (!cacheValid) { - console.debug('Valid storage on server found, no local cache found, using live as source of truth') - recent = live + let dirty = false + + if (recent === null) { + console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) + recent = _wrapData({ + flagStorage: { ...flagsTemplate } + }) + } + + if (!needsUpload && recent && stale) { + console.debug('Checking if data needs merging...') + // discarding timestamps and versions + const { _timestamp: _0, _version: _1, ...recentData } = recent + const { _timestamp: _2, _version: _3, ...staleData } = stale + dirty = !isEqual(recentData, staleData) + console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`) + } + + const allFlagKeys = _getAllFlags(recent, stale) + let totalFlags + if (dirty) { + // Merge the flags + console.debug('Merging the flags...') + totalFlags = _mergeFlags(recent, stale, allFlagKeys) } else { - console.debug('Both sources have valid data, figuring things out...') - console.log(live._timestamp, cache._timestamp) - if (live._timestamp === cache._timestamp && live._version === cache._version) { - console.debug('Same version/timestamp on both source, source of truth irrelevant') - recent = cache - } else { - state.dirty = true - console.debug('Different timestamp, figuring out which one is more recent') - let stale - if (live._timestamp < cache._timestamp) { - recent = cache - stale = live - } else { - recent = live - stale = cache - } - - // Merge the flags - console.debug('Merging the flags...') - recent.flagStorage = recent.flagStorage || { ...flagsTemplate } - stale.flagStorage = stale.flagStorage || { ...flagsTemplate } - const allFlags = Array.from(new Set([ - ...Object.keys(toRaw(recent.flagStorage)), - ...Object.keys(toRaw(stale.flagStorage)) - ])) - - const totalFlags = Object.fromEntries(allFlags.map(flag => { - const recentFlag = recent.flagStorage[flag] - const staleFlag = stale.flagStorage[flag] - // use flag that is of higher value - return [flag, recentFlag > staleFlag ? recentFlag : staleFlag] - })) - - console.debug('AAA', totalFlags) - // flag reset functionality - if (totalFlags.reset >= COMMAND_TRIM_FLAGS && totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET) { - console.debug('Received command to trim the flags') - const knownKeys = new Set(Object.keys(defaultState.flagStorage)) - allFlags.forEach(flag => { - if (!knownKeys.has(flag)) { - delete totalFlags[flag] - } - }) - if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) { - // 1001 - and reset everything to 0 - console.debug('Received command to reset the flags') - allFlags.forEach(flag => { totalFlags[flag] = 0 }) - } else { - // reset the reset 0 - totalFlags.reset = 0 - } - } else if (totalFlags.reset > 0 && totalFlags.reset < 9000) { - console.debug('Received command to reset the flags') - allFlags.forEach(flag => { totalFlags[flag] = 0 }) - // for good luck - totalFlags.reset = 0 - } - console.log('AAAA', totalFlags) - state.cache.flagStorage = totalFlags - } + totalFlags = recent.flagStorage } + + // This does side effects on totalFlags !!! + // only resets if needed (checks are inside) + _resetFlags(totalFlags, allFlagKeys) + + recent.flagStorage = totalFlags + + state.dirty = dirty || needsUpload state.cache = recent state.flagStorage = state.cache.flagStorage }, @@ -137,15 +200,11 @@ const serverSideStorage = { }, actions: { pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { - console.log('PUSH') const needPush = state.dirty || force if (!needPush) return - state.cache = { - _timestamp: Date.now(), - _version: VERSION, + state.cache = _wrapData({ flagStorage: toRaw(state.flagStorage) - } - console.log('YES') + }) const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor .updateProfile({ params }) -- cgit v1.2.3-70-g09d2 From 8c59bad3c2444e7deea20f9d301b675d2ef51016 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 4 Aug 2022 22:09:42 +0300 Subject: unit test + some refactoring --- .../update_notification/update_notification.js | 10 +- src/modules/serverSideStorage.js | 159 +++++++++--------- test/unit/specs/modules/serverSideStorage.spec.js | 178 +++++++++++++++++++++ 3 files changed, 269 insertions(+), 78 deletions(-) create mode 100644 test/unit/specs/modules/serverSideStorage.spec.js (limited to 'src/modules/serverSideStorage.js') diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js index da3e1876..172be889 100644 --- a/src/components/update_notification/update_notification.js +++ b/src/components/update_notification/update_notification.js @@ -10,7 +10,7 @@ library.add( faTimes ) -const CURRENT_UPDATE_COUNTER = 1 +export const CURRENT_UPDATE_COUNTER = 1 const UpdateNotification = { data () { @@ -40,13 +40,13 @@ const UpdateNotification = { }, neverShowAgain () { this.toggleShow() - // this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) - // this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 }) - // this.$store.dispatch('pushServerSideStorage') + this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) + this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 }) + this.$store.dispatch('pushServerSideStorage') }, dismiss () { this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) - // this.$store.dispatch('pushServerSideStorage') + this.$store.dispatch('pushServerSideStorage') } }, mounted () { diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index da908728..fd3fe781 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -1,13 +1,14 @@ import { toRaw } from 'vue' -import { isEqual } from 'lodash' +import { isEqual, cloneDeep } from 'lodash' +import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' export const VERSION = 1 -export const NEW_USER_DATE = new Date('04-08-2022') // date of writing this, basically +export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically export const COMMAND_TRIM_FLAGS = 1000 export const COMMAND_TRIM_FLAGS_AND_RESET = 1001 -const defaultState = { +export const defaultState = { // do we need to update data on server? dirty: false, // storage of flags - stuff that can only be set and incremented @@ -27,9 +28,9 @@ const defaultState = { cache: null } -const newUserFlags = { +export const newUserFlags = { ...defaultState.flagStorage, - updateCounter: 1 // new users don't need to see update notification + updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification } const _wrapData = (data) => ({ @@ -38,24 +39,23 @@ const _wrapData = (data) => ({ _version: VERSION }) -export const _checkValidity = (data) => data._timestamp > 0 && data._version > 0 +const _checkValidity = (data) => data._timestamp > 0 && data._version > 0 export const _getRecentData = (cache, live) => { const result = { recent: null, stale: null, needUpload: false } const cacheValid = _checkValidity(cache || {}) const liveValid = _checkValidity(live || {}) - if (!liveValid) { + if (!liveValid && cacheValid) { result.needUpload = true console.debug('Nothing valid stored on server, assuming cache to be source of truth') result.recent = cache result.stale = live - } else if (!cacheValid) { + } else if (!cacheValid && liveValid) { console.debug('Valid storage on server found, no local cache found, using live as source of truth') result.recent = live result.stale = cache - } else { + } else if (cacheValid && liveValid) { console.debug('Both sources have valid data, figuring things out...') - console.log(live._timestamp, cache._timestamp) if (live._timestamp === cache._timestamp && live._version === cache._version) { console.debug('Same version/timestamp on both source, source of truth irrelevant') result.recent = cache @@ -70,14 +70,17 @@ export const _getRecentData = (cache, live) => { result.stale = cache } } + } else { + console.debug('Both sources are invalid, start from scratch') + result.needUpload = true } return result } -export const _getAllFlags = (recent = {}, stale = {}) => { +export const _getAllFlags = (recent, stale) => { return Array.from(new Set([ - ...Object.keys(toRaw(recent.flagStorage || {})), - ...Object.keys(toRaw(stale.flagStorage || {})) + ...Object.keys(toRaw((recent || {}).flagStorage || {})), + ...Object.keys(toRaw((stale || {}).flagStorage || {})) ])) } @@ -86,37 +89,43 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { const recentFlag = recent.flagStorage[flag] const staleFlag = stale.flagStorage[flag] // use flag that is of higher value - return [flag, recentFlag > staleFlag ? recentFlag : staleFlag] + return [flag, Number((recentFlag > staleFlag ? recentFlag : staleFlag) || 0)] })) } -export const _resetFlags = (totalFlags, allFlagKeys) => { +export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => { + let result = { ...totalFlags } + const allFlagKeys = Object.keys(totalFlags) // flag reset functionality if (totalFlags.reset >= COMMAND_TRIM_FLAGS && totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET) { console.debug('Received command to trim the flags') - const knownKeys = new Set(Object.keys(defaultState.flagStorage)) + const knownKeysSet = new Set(Object.keys(knownKeys)) + + // Trim + result = {} allFlagKeys.forEach(flag => { - if (!knownKeys.has(flag)) { - delete totalFlags[flag] + if (knownKeysSet.has(flag)) { + result[flag] = totalFlags[flag] } }) + + // Reset if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) { // 1001 - and reset everything to 0 console.debug('Received command to reset the flags') - allFlagKeys.forEach(flag => { totalFlags[flag] = 0 }) - } else { - // reset the reset 0 - totalFlags.reset = 0 + Object.keys(knownKeys).forEach(flag => { result[flag] = 0 }) } } else if (totalFlags.reset > 0 && totalFlags.reset < 9000) { console.debug('Received command to reset the flags') - allFlagKeys.forEach(flag => { totalFlags[flag] = 0 }) - // for good luck - totalFlags.reset = 0 + allFlagKeys.forEach(flag => { result[flag] = 0 }) } + result.reset = 0 + return result } export const _doMigrations = (cache) => { + if (!cache) return cache + if (cache._version < VERSION) { console.debug('Local cached data has older version, seeing if there any migrations that can be applied') @@ -139,65 +148,69 @@ export const _doMigrations = (cache) => { return cache } -const serverSideStorage = { - state: { - ...defaultState - }, - mutations: { - setServerSideStorage (state, userData) { - const live = userData.storage - state.raw = live - let cache = state.cache +export const mutations = { + setServerSideStorage (state, userData) { + const live = userData.storage + state.raw = live + let cache = state.cache - cache = _doMigrations(cache) + cache = _doMigrations(cache) - let { recent, stale, needsUpload } = _getRecentData(cache, live) + let { recent, stale, needsUpload } = _getRecentData(cache, live) - const userNew = userData.created_at > NEW_USER_DATE - const flagsTemplate = userNew ? newUserFlags : defaultState.defaultState - let dirty = false + const userNew = userData.created_at > NEW_USER_DATE + const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage + let dirty = false - if (recent === null) { - console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) - recent = _wrapData({ - flagStorage: { ...flagsTemplate } - }) - } + if (recent === null) { + console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) + recent = _wrapData({ + flagStorage: { ...flagsTemplate } + }) + } - if (!needsUpload && recent && stale) { - console.debug('Checking if data needs merging...') - // discarding timestamps and versions - const { _timestamp: _0, _version: _1, ...recentData } = recent - const { _timestamp: _2, _version: _3, ...staleData } = stale - dirty = !isEqual(recentData, staleData) - console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`) - } + if (!needsUpload && recent && stale) { + console.debug('Checking if data needs merging...') + // discarding timestamps and versions + const { _timestamp: _0, _version: _1, ...recentData } = recent + const { _timestamp: _2, _version: _3, ...staleData } = stale + dirty = !isEqual(recentData, staleData) + console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`) + } - const allFlagKeys = _getAllFlags(recent, stale) - let totalFlags - if (dirty) { - // Merge the flags - console.debug('Merging the flags...') - totalFlags = _mergeFlags(recent, stale, allFlagKeys) - } else { - totalFlags = recent.flagStorage - } + const allFlagKeys = _getAllFlags(recent, stale) + let totalFlags + if (dirty) { + // Merge the flags + console.debug('Merging the flags...') + totalFlags = _mergeFlags(recent, stale, allFlagKeys) + } else { + totalFlags = recent.flagStorage + } - // This does side effects on totalFlags !!! - // only resets if needed (checks are inside) - _resetFlags(totalFlags, allFlagKeys) + totalFlags = _resetFlags(totalFlags) - recent.flagStorage = totalFlags + recent.flagStorage = totalFlags - state.dirty = dirty || needsUpload - state.cache = recent - state.flagStorage = state.cache.flagStorage - }, - setFlag (state, { flag, value }) { - state.flagStorage[flag] = value - state.dirty = true + state.dirty = dirty || needsUpload + state.cache = recent + // set local timestamp to smaller one if we don't have any changes + if (stale && recent && !state.dirty) { + state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp) } + state.flagStorage = state.cache.flagStorage + }, + setFlag (state, { flag, value }) { + state.flagStorage[flag] = value + state.dirty = true + } +} + +const serverSideStorage = { + state: { + ...cloneDeep(defaultState) }, + mutations, actions: { pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { const needPush = state.dirty || force diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js new file mode 100644 index 00000000..e06c6ada --- /dev/null +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -0,0 +1,178 @@ +import { cloneDeep } from 'lodash' + +import { + VERSION, + COMMAND_TRIM_FLAGS, + COMMAND_TRIM_FLAGS_AND_RESET, + _getRecentData, + _getAllFlags, + _mergeFlags, + _resetFlags, + mutations, + defaultState, + newUserFlags +} from 'src/modules/serverSideStorage.js' + +describe('The serverSideStorage module', () => { + describe('mutations', () => { + describe('setServerSideStorage', () => { + const { setServerSideStorage } = mutations + const user = { + created_at: new Date('1999-02-09'), + storage: {} + } + + it('should initialize storage if none present', () => { + const state = cloneDeep(defaultState) + setServerSideStorage(state, user) + expect(state.cache._version).to.eql(VERSION) + expect(state.cache._timestamp).to.be.a('number') + expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) + }) + + it('should initialize storage with proper flags for new users if none present', () => { + const state = cloneDeep(defaultState) + setServerSideStorage(state, { ...user, created_at: new Date() }) + expect(state.cache._version).to.eql(VERSION) + expect(state.cache._timestamp).to.be.a('number') + expect(state.cache.flagStorage).to.eql(newUserFlags) + }) + + it('should merge flags even if remote timestamp is older', () => { + const state = { + ...cloneDeep(defaultState), + cache: { + _timestamp: Date.now(), + _version: VERSION, + ...cloneDeep(defaultState) + } + } + setServerSideStorage( + state, + { + ...user, + storage: { + _timestamp: 123, + _version: VERSION, + flagStorage: { + ...defaultState.flagStorage, + updateCounter: 1 + } + } + } + ) + expect(state.cache.flagStorage).to.eql({ + ...defaultState.flagStorage, + updateCounter: 1 + }) + }) + + it('should reset local timestamp to remote if contents are the same', () => { + const state = { + ...cloneDeep(defaultState), + cache: null + } + setServerSideStorage( + state, + { + ...user, + storage: { + _timestamp: 123, + _version: VERSION, + flagStorage: { + ...defaultState.flagStorage, + updateCounter: 999 + } + } + } + ) + expect(state.cache._timestamp).to.eql(123) + expect(state.flagStorage.updateCounter).to.eql(999) + expect(state.cache.flagStorage.updateCounter).to.eql(999) + }) + + it('should remote version if local missing', () => { + const state = cloneDeep(defaultState) + setServerSideStorage(state, user) + expect(state.cache._version).to.eql(VERSION) + expect(state.cache._timestamp).to.be.a('number') + expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) + }) + }) + }) + + describe('helper functions', () => { + describe('_getRecentData', () => { + it('should handle nulls correctly', () => { + expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true }) + }) + + it('doesn\'t choke on invalid data', () => { + expect(_getRecentData({ a: 1 }, { b: 2 })).to.eql({ recent: null, stale: null, needUpload: true }) + }) + + it('should prefer the valid non-null correctly, needUpload works properly', () => { + const nonNull = { _version: VERSION, _timestamp: 1 } + expect(_getRecentData(nonNull, null)).to.eql({ recent: nonNull, stale: null, needUpload: true }) + expect(_getRecentData(null, nonNull)).to.eql({ recent: nonNull, stale: null, needUpload: false }) + }) + + it('should prefer the one with higher timestamp', () => { + const a = { _version: VERSION, _timestamp: 1 } + const b = { _version: VERSION, _timestamp: 2 } + + expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false }) + expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false }) + }) + + it('case where both are same', () => { + const a = { _version: VERSION, _timestamp: 3 } + const b = { _version: VERSION, _timestamp: 3 } + + expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false }) + expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false }) + }) + }) + + describe('_getAllFlags', () => { + it('should handle nulls properly', () => { + expect(_getAllFlags(null, null)).to.eql([]) + }) + it('should output list of keys if passed single object', () => { + expect(_getAllFlags({ flagStorage: { a: 1, b: 1, c: 1 } }, null)).to.eql(['a', 'b', 'c']) + }) + it('should union keys of both objects', () => { + expect(_getAllFlags({ flagStorage: { a: 1, b: 1, c: 1 } }, { flagStorage: { c: 1, d: 1 } })).to.eql(['a', 'b', 'c', 'd']) + }) + }) + + describe('_mergeFlags', () => { + it('should handle merge two flag sets correctly picking higher numbers', () => { + expect( + _mergeFlags( + { flagStorage: { a: 0, b: 3 } }, + { flagStorage: { b: 1, c: 4, d: 9 } }, + ['a', 'b', 'c', 'd']) + ).to.eql({ a: 0, b: 3, c: 4, d: 9 }) + }) + }) + + describe('_resetFlags', () => { + it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => { + const totalFlags = { a: 0, b: 3, reset: 1 } + + expect(_resetFlags(totalFlags)).to.eql({ a: 0, b: 0, reset: 0 }) + }) + it('should trim all flags to known when reset is set to 1000', () => { + const totalFlags = { a: 0, b: 3, c: 33, reset: COMMAND_TRIM_FLAGS } + + expect(_resetFlags(totalFlags, { a: 0, b: 0, reset: 0 })).to.eql({ a: 0, b: 3, reset: 0 }) + }) + it('should trim all flags to known and reset when reset is set to 1001', () => { + const totalFlags = { a: 0, b: 3, c: 33, reset: COMMAND_TRIM_FLAGS_AND_RESET } + + expect(_resetFlags(totalFlags, { a: 0, b: 0, reset: 0 })).to.eql({ a: 0, b: 0, reset: 0 }) + }) + }) + }) +}) -- cgit v1.2.3-70-g09d2 From 95f03a56abe9211e5e9550f226ec0b4fbb18758d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 8 Aug 2022 02:18:29 +0300 Subject: don't let function access `this` --- src/modules/serverSideStorage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/modules/serverSideStorage.js') diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index fd3fe781..e516a6e6 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -140,7 +140,7 @@ export const _doMigrations = (cache) => { if (window._PLEROMA_HOTPATCH) { if (window._PLEROMA_HOTPATCH.reverseMigrations) { console.debug('Found hotpatch migration, applying') - return window._PLEROMA_HOTPATCH.reverseMigrations('serverSideStorage', { from: cache._version, to: VERSION }, cache) + return window._PLEROMA_HOTPATCH.reverseMigrations.call({}, 'serverSideStorage', { from: cache._version, to: VERSION }, cache) } } } -- cgit v1.2.3-70-g09d2 From aa41cedd932e88b030ecc3cc54848b5aa300eec3 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 10 Aug 2022 02:19:07 +0300 Subject: initial prefs storage work --- src/modules/serverSideStorage.js | 54 ++++++++++++- test/unit/specs/modules/serverSideStorage.spec.js | 94 +++++++++++++++++++++++ 2 files changed, 144 insertions(+), 4 deletions(-) (limited to 'src/modules/serverSideStorage.js') diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index e516a6e6..08ba48bb 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -1,5 +1,5 @@ import { toRaw } from 'vue' -import { isEqual, cloneDeep } from 'lodash' +import { isEqual, uniqBy, cloneDeep, set } from 'lodash' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' export const VERSION = 1 @@ -22,6 +22,10 @@ export const defaultState = { // 1000: trim keys to those known by currently running FE // 1001: same as above + reset everything to 0 }, + prefsStorage: { + _journal: [], + simple: {} + }, // raw data raw: null, // local cache @@ -93,6 +97,42 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { })) } +export const _mergePrefs = (recent, stale, allFlagKeys) => { + if (!stale) return recent + if (!recent) return stale + const { _journal: recentJournal, ...recentData } = recent + const { _journal: staleJournal } = stale + /** Journal entry format: + * path: path to entry in prefsStorage + * timestamp: timestamp of the change + * operation: operation type + * arguments: array of arguments, depends on operation type + * + * currently only supported operation type is "set" which just sets the value + * to requested one. Intended only to be used with simple preferences (boolean, number) + * shouldn't be used with collections! + */ + const resultOutput = { ...recentData } + const totalJournal = uniqBy( + [...recentJournal, ...staleJournal].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), + 'path' + ).reverse() + totalJournal.forEach(({ path, timestamp, operation, args }) => { + if (path.startsWith('_')) { + console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`) + return + } + switch (operation) { + case 'set': + set(resultOutput, path, args[0]) + break + default: + console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`) + } + }) + return { ...resultOutput, _journal: totalJournal } +} + export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => { let result = { ...totalFlags } const allFlagKeys = Object.keys(totalFlags) @@ -165,7 +205,8 @@ export const mutations = { if (recent === null) { console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) recent = _wrapData({ - flagStorage: { ...flagsTemplate } + flagStorage: { ...flagsTemplate }, + prefsStorage: { ...defaultState.prefsStorage } }) } @@ -180,17 +221,21 @@ export const mutations = { const allFlagKeys = _getAllFlags(recent, stale) let totalFlags + let totalPrefs if (dirty) { // Merge the flags - console.debug('Merging the flags...') + console.debug('Merging the data...') totalFlags = _mergeFlags(recent, stale, allFlagKeys) + totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage) } else { totalFlags = recent.flagStorage + totalPrefs = recent.prefsStorage } totalFlags = _resetFlags(totalFlags) recent.flagStorage = totalFlags + recent.prefsStorage = totalPrefs state.dirty = dirty || needsUpload state.cache = recent @@ -216,7 +261,8 @@ const serverSideStorage = { const needPush = state.dirty || force if (!needPush) return state.cache = _wrapData({ - flagStorage: toRaw(state.flagStorage) + flagStorage: toRaw(state.flagStorage), + prefsStorage: toRaw(state.flagStorage) }) const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index e06c6ada..6059dc84 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -7,6 +7,7 @@ import { _getRecentData, _getAllFlags, _mergeFlags, + _mergePrefs, _resetFlags, mutations, defaultState, @@ -28,6 +29,7 @@ describe('The serverSideStorage module', () => { expect(state.cache._version).to.eql(VERSION) expect(state.cache._timestamp).to.be.a('number') expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) + expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) it('should initialize storage with proper flags for new users if none present', () => { @@ -36,6 +38,7 @@ describe('The serverSideStorage module', () => { expect(state.cache._version).to.eql(VERSION) expect(state.cache._timestamp).to.be.a('number') expect(state.cache.flagStorage).to.eql(newUserFlags) + expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) it('should merge flags even if remote timestamp is older', () => { @@ -57,6 +60,9 @@ describe('The serverSideStorage module', () => { flagStorage: { ...defaultState.flagStorage, updateCounter: 1 + }, + prefsStorage: { + ...defaultState.flagStorage, } } } @@ -157,6 +163,94 @@ describe('The serverSideStorage module', () => { }) }) + describe('_mergePrefs', () => { + it('should prefer recent and apply journal to it', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 1, b: 0, c: true }, + _journal: [ + { path: 'simple.b', operation: 'set', args: [0], timestamp: 2 }, + { path: 'simple.c', operation: 'set', args: [true], timestamp: 4 } + ] + }, + // STALE + { + simple: { a: 1, b: 1, c: false }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [1], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [1], timestamp: 3 } + ] + } + ) + ).to.eql({ + simple: { a: 1, b: 1, c: true }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [1], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [1], timestamp: 3 }, + { path: 'simple.c', operation: 'set', args: [true], timestamp: 4 } + ] + }) + }) + + it('should allow setting falsy values', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 1, b: 0, c: false }, + _journal: [ + { path: 'simple.b', operation: 'set', args: [0], timestamp: 2 }, + { path: 'simple.c', operation: 'set', args: [false], timestamp: 4 } + ] + }, + // STALE + { + simple: { a: 0, b: 0, c: true }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [0], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [0], timestamp: 3 } + ] + } + ) + ).to.eql({ + simple: { a: 0, b: 0, c: false }, + _journal: [ + { path: 'simple.a', operation: 'set', args: [0], timestamp: 1 }, + { path: 'simple.b', operation: 'set', args: [0], timestamp: 3 }, + { path: 'simple.c', operation: 'set', args: [false], timestamp: 4 } + ] + }) + }) + + it('should work with strings', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { a: 'foo' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['foo'], timestamp: 2 } + ] + }, + // STALE + { + simple: { a: 'bar' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 } + ] + } + ) + ).to.eql({ + simple: { a: 'bar' }, + _journal: [ + { path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 } + ] + }) + }) + }) + describe('_resetFlags', () => { it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => { const totalFlags = { a: 0, b: 3, reset: 1 } -- cgit v1.2.3-70-g09d2 From 2c0eb29b286edf57d75c6044855ea5be9187493b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 10 Aug 2022 02:31:41 +0300 Subject: more prefs storage work + move dontShowUpdateNotifs to prefs --- .../update_notification/update_notification.js | 4 +-- src/modules/serverSideStorage.js | 23 ++++++++++++---- test/unit/specs/modules/serverSideStorage.spec.js | 32 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) (limited to 'src/modules/serverSideStorage.js') diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js index ba008d81..609842c4 100644 --- a/src/components/update_notification/update_notification.js +++ b/src/components/update_notification/update_notification.js @@ -38,7 +38,7 @@ const UpdateNotification = { return !this.$store.state.instance.disableUpdateNotification && this.$store.state.users.currentUser && this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER && - !this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs + !this.$store.state.serverSideStorage.prefsStorage.simple.dontShowUpdateNotifs } }, methods: { @@ -48,7 +48,7 @@ const UpdateNotification = { neverShowAgain () { this.toggleShow() this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) - this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 }) + this.$store.commit('setPreference', { path: 'simple.dontShowUpdateNotifs', value: true }) this.$store.dispatch('pushServerSideStorage') }, dismiss () { diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 08ba48bb..bb647b97 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -14,9 +14,6 @@ export const defaultState = { // storage of flags - stuff that can only be set and incremented flagStorage: { updateCounter: 0, // Counter for most recent update notification seen - // TODO move to prefsStorage when that becomes a thing since only way - // this can be reset is by complete reset of all flags - dontShowUpdateNotifs: 0, // if user chose to not show update notifications ever again reset: 0 // special flag that can be used to force-reset all flags, debug purposes only // special reset codes: // 1000: trim keys to those known by currently running FE @@ -24,7 +21,9 @@ export const defaultState = { }, prefsStorage: { _journal: [], - simple: {} + simple: { + dontShowUpdateNotifs: false + } }, // raw data raw: null, @@ -248,6 +247,20 @@ export const mutations = { setFlag (state, { flag, value }) { state.flagStorage[flag] = value state.dirty = true + }, + setPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + set(state.prefsStorage, path, value) + state.prefsStorage._journal = uniqBy( + [ + ...state.prefsStorage._journal, + { command: 'set', path, args: [value], timestamp: Date.now() } + ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), + 'path' + ).reverse() } } @@ -262,7 +275,7 @@ const serverSideStorage = { if (!needPush) return state.cache = _wrapData({ flagStorage: toRaw(state.flagStorage), - prefsStorage: toRaw(state.flagStorage) + prefsStorage: toRaw(state.prefsStorage) }) const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index 6059dc84..da790793 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -105,6 +105,38 @@ describe('The serverSideStorage module', () => { expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) }) }) + describe('setPreference', () => { + const { setPreference } = mutations + + it('should set preference and update journal log accordingly', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.testing', value: 1 }) + expect(state.prefsStorage.simple.testing).to.eql(1) + expect(state.prefsStorage._journal.length).to.eql(1) + expect(state.prefsStorage._journal[0]).to.eql({ + path: 'simple.testing', + command: 'set', + args: [1], + // should have A timestamp, we don't really care what it is + timestamp: state.prefsStorage._journal[0].timestamp + }) + }) + + it('should keep journal to a minimum (one entry per path)', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.testing', value: 1 }) + setPreference(state, { path: 'simple.testing', value: 2 }) + expect(state.prefsStorage.simple.testing).to.eql(1) + expect(state.prefsStorage._journal.length).to.eql(1) + expect(state.prefsStorage._journal[0]).to.eql({ + path: 'simple.testing', + command: 'set', + args: [2], + // should have A timestamp, we don't really care what it is + timestamp: state.prefsStorage._journal[0].timestamp + }) + }) + }) }) describe('helper functions', () => { -- cgit v1.2.3-70-g09d2 From 894a506382b18748e889fec69028da9897555446 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 10 Aug 2022 02:59:08 +0300 Subject: fixes --- src/modules/serverSideStorage.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/modules/serverSideStorage.js') diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index bb647b97..1a7e02b3 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -88,6 +88,8 @@ export const _getAllFlags = (recent, stale) => { } export const _mergeFlags = (recent, stale, allFlagKeys) => { + if (!stale.flagStorage) return recent.flagStorage + if (!recent.flagStorage) return stale.flagStorage return Object.fromEntries(allFlagKeys.map(flag => { const recentFlag = recent.flagStorage[flag] const staleFlag = stale.flagStorage[flag] @@ -113,7 +115,10 @@ export const _mergePrefs = (recent, stale, allFlagKeys) => { */ const resultOutput = { ...recentData } const totalJournal = uniqBy( - [...recentJournal, ...staleJournal].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), + [ + ...(Array.isArray(recentJournal) ? recentJournal : []), + ...(Array.isArray(staleJournal) ? staleJournal : []) + ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), 'path' ).reverse() totalJournal.forEach(({ path, timestamp, operation, args }) => { @@ -243,6 +248,7 @@ export const mutations = { state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp) } state.flagStorage = state.cache.flagStorage + state.prefsStorage = state.cache.prefsStorage }, setFlag (state, { flag, value }) { state.flagStorage[flag] = value -- cgit v1.2.3-70-g09d2 From 72e238ceb34304cb023a01a84c3f453aadaa775c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 11 Aug 2022 01:07:51 +0300 Subject: server side storage support for collections + fixes --- src/modules/serverSideStorage.js | 111 ++++++++++++++++++---- test/unit/specs/modules/serverSideStorage.spec.js | 20 +++- 2 files changed, 108 insertions(+), 23 deletions(-) (limited to 'src/modules/serverSideStorage.js') diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 1a7e02b3..11e66220 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -1,5 +1,5 @@ import { toRaw } from 'vue' -import { isEqual, uniqBy, cloneDeep, set } from 'lodash' +import { isEqual, uniqWith, cloneDeep, set, get, clamp } from 'lodash' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' export const VERSION = 1 @@ -36,6 +36,17 @@ export const newUserFlags = { updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification } +export const _moveItemInArray = (array, value, movement) => { + const oldIndex = array.indexOf(value) + const newIndex = oldIndex + movement + const newArray = [...array] + // remove old + newArray.splice(oldIndex, 1) + // add new + newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value) + return newArray +} + const _wrapData = (data) => ({ ...data, _timestamp: Date.now(), @@ -98,6 +109,23 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { })) } +const _mergeJournal = (a, b) => uniqWith( + [ + ...(Array.isArray(a) ? a : []), + ...(Array.isArray(b) ? b : []) + ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), + (a, b) => { + if (a.operation !== b.operation) return false + switch (a.operation) { + case 'set': + case 'arrangeSet': + return a.path === b.path + default: + return a.path === b.path && a.timestamp === b.timestamp + } + } +).reverse() + export const _mergePrefs = (recent, stale, allFlagKeys) => { if (!stale) return recent if (!recent) return stale @@ -114,13 +142,7 @@ export const _mergePrefs = (recent, stale, allFlagKeys) => { * shouldn't be used with collections! */ const resultOutput = { ...recentData } - const totalJournal = uniqBy( - [ - ...(Array.isArray(recentJournal) ? recentJournal : []), - ...(Array.isArray(staleJournal) ? staleJournal : []) - ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), - 'path' - ).reverse() + const totalJournal = _mergeJournal(staleJournal, recentJournal) totalJournal.forEach(({ path, timestamp, operation, args }) => { if (path.startsWith('_')) { console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`) @@ -130,6 +152,17 @@ export const _mergePrefs = (recent, stale, allFlagKeys) => { case 'set': set(resultOutput, path, args[0]) break + case 'addToCollection': + set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0]))) + break + case 'removeFromCollection': + set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).remove(args[0]))) + break + case 'reorderCollection': { + const [value, movement] = args + set(resultOutput, path, _moveItemInArray(get(resultOutput, path), value, movement)) + break + } default: console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`) } @@ -260,13 +293,56 @@ export const mutations = { return } set(state.prefsStorage, path, value) - state.prefsStorage._journal = uniqBy( - [ - ...state.prefsStorage._journal, - { command: 'set', path, args: [value], timestamp: Date.now() } - ].sort((a, b) => a.timestamp > b.timestamp ? -1 : 1), - 'path' - ).reverse() + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { command: 'set', path, args: [value], timestamp: Date.now() } + ] + }, + addCollectionPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = new Set(get(state.prefsStorage, path)) + collection.add(value) + set(state.prefsStorage, path, collection) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { command: 'addToCollection', path, args: [value], timestamp: Date.now() } + ] + }, + removeCollectionPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = new Set(get(state.prefsStorage, path)) + collection.remove(value) + set(state.prefsStorage, path, collection) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { command: 'removeFromCollection', path, args: [value], timestamp: Date.now() } + ] + }, + reorderCollectionPreference (state, { path, value, movement }) { + if (path.startsWith('_')) { + console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + const collection = get(state.prefsStorage, path) + const newCollection = _moveItemInArray(collection, value, movement) + set(state.prefsStorage, path, newCollection) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { command: 'arrangeCollection', path, args: [value], timestamp: Date.now() } + ] + }, + updateCache (state) { + state.prefsStorage._journal = _mergeJournal(state.prefsStorage._journal) + state.cache = _wrapData({ + flagStorage: toRaw(state.flagStorage), + prefsStorage: toRaw(state.prefsStorage) + }) } } @@ -279,10 +355,7 @@ const serverSideStorage = { pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { const needPush = state.dirty || force if (!needPush) return - state.cache = _wrapData({ - flagStorage: toRaw(state.flagStorage), - prefsStorage: toRaw(state.prefsStorage) - }) + commit('updateCache') const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } rootState.api.backendInteractor .updateProfile({ params }) diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index da790793..ada3b7ca 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -4,6 +4,7 @@ import { VERSION, COMMAND_TRIM_FLAGS, COMMAND_TRIM_FLAGS_AND_RESET, + _moveItemInArray, _getRecentData, _getAllFlags, _mergeFlags, @@ -62,7 +63,7 @@ describe('The serverSideStorage module', () => { updateCounter: 1 }, prefsStorage: { - ...defaultState.flagStorage, + ...defaultState.prefsStorage } } } @@ -106,7 +107,7 @@ describe('The serverSideStorage module', () => { }) }) describe('setPreference', () => { - const { setPreference } = mutations + const { setPreference, updateCache } = mutations it('should set preference and update journal log accordingly', () => { const state = cloneDeep(defaultState) @@ -122,11 +123,12 @@ describe('The serverSideStorage module', () => { }) }) - it('should keep journal to a minimum (one entry per path)', () => { + it('should keep journal to a minimum (one entry per path for sets)', () => { const state = cloneDeep(defaultState) setPreference(state, { path: 'simple.testing', value: 1 }) setPreference(state, { path: 'simple.testing', value: 2 }) - expect(state.prefsStorage.simple.testing).to.eql(1) + updateCache(state) + expect(state.prefsStorage.simple.testing).to.eql(2) expect(state.prefsStorage._journal.length).to.eql(1) expect(state.prefsStorage._journal[0]).to.eql({ path: 'simple.testing', @@ -140,6 +142,16 @@ describe('The serverSideStorage module', () => { }) describe('helper functions', () => { + describe('_moveItemInArray', () => { + it('should move item according to movement value', () => { + expect(_moveItemInArray([1, 2, 3, 4], 4, -1)).to.eql([1, 2, 4, 3]) + expect(_moveItemInArray([1, 2, 3, 4], 1, 2)).to.eql([2, 3, 1, 4]) + }) + it('should clamp movement to within array', () => { + expect(_moveItemInArray([1, 2, 3, 4], 4, -10)).to.eql([4, 1, 2, 3]) + expect(_moveItemInArray([1, 2, 3, 4], 3, 99)).to.eql([1, 2, 4, 3]) + }) + }) describe('_getRecentData', () => { it('should handle nulls correctly', () => { expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true }) -- cgit v1.2.3-70-g09d2 From 732733f115a863408a339e164ff88f1022c46101 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 11 Aug 2022 02:23:58 +0300 Subject: add a todo for future --- src/modules/serverSideStorage.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/modules/serverSideStorage.js') diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 11e66220..bff8e4ba 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -110,6 +110,9 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { } const _mergeJournal = (a, b) => uniqWith( + // TODO use groupBy to group by path, then trim them depending on operations, + // i.e. if field got removed in the end - no need to sort it beforehand, if field + // got re-added no need to remove it and add it etc. [ ...(Array.isArray(a) ? a : []), ...(Array.isArray(b) ? b : []) -- cgit v1.2.3-70-g09d2 From 6df99133548fb209bf365b77665931be477f0a30 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 11 Aug 2022 14:30:58 +0300 Subject: ability to pin items in navigation menu, initial draft version --- src/App.scss | 3 + src/components/nav_panel/nav_panel.js | 111 +++++++++++++++- src/components/nav_panel/nav_panel.vue | 140 ++++++++++----------- src/components/settings_modal/tabs/general_tab.vue | 5 - src/components/timeline_menu/timeline_menu.vue | 2 +- .../timeline_menu/timeline_menu_content.js | 27 +++- .../timeline_menu/timeline_menu_content.vue | 69 +++------- src/modules/config.js | 1 - src/modules/serverSideStorage.js | 9 +- 9 files changed, 221 insertions(+), 146 deletions(-) (limited to 'src/modules/serverSideStorage.js') diff --git a/src/App.scss b/src/App.scss index ab025d63..c75c990a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -756,6 +756,9 @@ option { padding: 0 0.3em; } } +.veryfaint { + opacity: 0.25; +} .login-hint { text-align: center; diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index abeff6bf..758f9af4 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -31,6 +31,66 @@ library.add( faList ) +export const TIMELINES = { + home: { + route: 'friends', + anonRoute: 'public-timeline', + icon: 'home', + label: 'nav.home_timeline', + criteria: ['!private'] + }, + public: { + route: 'public-timeline', + anon: true, + icon: 'users', + label: 'nav.public_tl', + criteria: ['!private'] + }, + twkn: { + route: 'public-external-timeline', + anon: true, + icon: 'globe', + label: 'nav.twkn', + criteria: ['!private', 'federating'] + }, + bookmarks: { + route: 'bookmarks', + icon: 'bookmark', + label: 'nav.bookmarks' + }, + dms: { + route: 'dms', + icon: 'envelope', + label: 'nav.dms' + } +} +export const ROOT_ITEMS = { + interactions: { + route: 'interactions', + icon: 'bell', + label: 'nav.interactions' + }, + chats: { + route: 'chats', + icon: 'comments', + label: 'nav.chats', + badgeGetter: 'unreadChatCount' + }, + friendRequests: { + route: 'friend-requests', + icon: 'user-plus', + label: 'nav.friend_requests', + criteria: ['lockedUser'], + badgeGetter: 'followRequestCount' + }, + about: { + route: 'about', + anon: true, + icon: 'info-circle', + label: 'nav.about' + } +} + const NavPanel = { created () { if (this.currentUser && this.currentUser.locked) { @@ -43,8 +103,11 @@ const NavPanel = { }, data () { return { + collapsed: false, showTimelines: false, - showLists: false + showLists: false, + timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })), + rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k })) } }, methods: { @@ -53,19 +116,57 @@ const NavPanel = { }, toggleLists () { this.showLists = !this.showLists + }, + toggleCollapse () { + this.collapsed = !this.collapsed + }, + isPinned (item) { + return this.pinnedItems.has(item) + }, + togglePin (item) { + if (this.isPinned(item)) { + this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + } else { + this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + } } }, computed: { - listsNavigation () { - return this.$store.getters.mergedConfig.listsNavigation - }, ...mapState({ currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, privateMode: state => state.instance.private, federating: state => state.instance.federating, - pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) }), + rootItems () { + return Object + .entries({ ...ROOT_ITEMS }) + .map(([k, v]) => ({ ...v, name: k })) + .filter(({ criteria, anon, anonRoute }) => { + const set = new Set(criteria || []) + if (!this.federating && set.has('federating')) return false + if (this.private && set.has('!private')) return false + if (!this.currentUser && !(anon || anonRoute)) return false + if ((!this.currentUser || !this.currentUser.locked) && set.has('lockedUser')) return false + return true + }) + }, + pinnedList () { + return Object + .entries({ ...TIMELINES, ...ROOT_ITEMS }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })) + .filter(({ criteria, anon, anonRoute }) => { + const set = new Set(criteria || []) + if (!this.federating && set.has('federating')) return false + if (this.private && set.has('!private')) return false + if (!this.currentUser && !(anon || anonRoute)) return false + if (this.currentUser && !this.currentUser.locked && set.has('locked')) return false + return true + }) + }, ...mapGetters(['unreadChatCount']) } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 9322e233..99a4571e 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -1,7 +1,33 @@