From 72e238ceb34304cb023a01a84c3f453aadaa775c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 11 Aug 2022 01:07:51 +0300 Subject: [PATCH] server side storage support for collections + fixes --- src/modules/serverSideStorage.js | 111 +++++++++++++++--- .../specs/modules/serverSideStorage.spec.js | 20 +++- 2 files changed, 108 insertions(+), 23 deletions(-) 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 })