Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85832557
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js
index da3e18768f..172be889ad 100644
--- a/src/components/update_notification/update_notification.js
+++ b/src/components/update_notification/update_notification.js
@@ -1,59 +1,59 @@
import Modal from 'src/components/modal/modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import pleromaTan from 'src/assets/pleromatan_apology.png'
import pleromaTanFox from 'src/assets/pleromatan_apology_fox.png'
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes
)
-const CURRENT_UPDATE_COUNTER = 1
+export const CURRENT_UPDATE_COUNTER = 1
const UpdateNotification = {
data () {
return {
pleromaTanVariant: Math.random() > 0.5 ? pleromaTan : pleromaTanFox,
showingMore: false,
contentHeight: 0
}
},
components: {
Modal
},
computed: {
pleromaTanStyles () {
return {
'shape-outside': 'url(' + this.pleromaTanVariant + ')'
}
},
shouldShow () {
return this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER &&
!this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs
}
},
methods: {
toggleShow () {
this.showingMore = !this.showingMore
},
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 () {
setTimeout(() => {
this.contentHeight = this.$refs.content.offsetHeight
}, 10)
}
}
export default UpdateNotification
diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js
index da90872847..fd3fe7812f 100644
--- a/src/modules/serverSideStorage.js
+++ b/src/modules/serverSideStorage.js
@@ -1,217 +1,230 @@
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
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 = {
+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) => ({
...data,
_timestamp: Date.now(),
_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
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
}
}
+ } 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 || {}))
]))
}
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]
+ 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')
// 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
- },
- 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
if (!needPush) return
state.cache = _wrapData({
flagStorage: toRaw(state.flagStorage)
})
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
diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js
new file mode 100644
index 0000000000..e06c6ada03
--- /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 })
+ })
+ })
+ })
+})
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 2, 8:37 AM (3 h, 5 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1803909
Default Alt Text
(19 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline