From 8ee71cdffff2da4ae295c4cb376b4332db2fc42f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 23 Mar 2026 15:17:48 +0200 Subject: [PATCH] update tests, add journal trimming --- .../quick_filter_settings.js | 2 +- .../settings_modal/tabs/filtering_tab.js | 3 +- src/stores/sync_config.js | 37 +++--- src/sw.js | 2 +- test/unit/specs/stores/sync_config.spec.js | 112 ++++++++++++++++-- 5 files changed, 124 insertions(+), 32 deletions(-) diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js index ad28febe2..4a8aa34b8 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.js +++ b/src/components/quick_filter_settings/quick_filter_settings.js @@ -3,8 +3,8 @@ import { mapState } from 'pinia' import Popover from '../popover/popover.vue' import { useInterfaceStore } from 'src/stores/interface.js' -import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js' +import { useSyncConfigStore } from 'src/stores/sync_config.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index a0e03d850..067248a30 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -93,7 +93,8 @@ const FilteringTab = { computed: { ...SharedComputedObject(), ...mapState(useSyncConfigStore, { - muteFilters: (store) => Object.entries(store.prefsStorage.simple.muteFilters), + muteFilters: (store) => + Object.entries(store.prefsStorage.simple.muteFilters), muteFiltersObject: (store) => store.prefsStorage.simple.muteFilters, }), ...mapState(useInstanceCapabilitiesStore, ['blockExpiration']), diff --git a/src/stores/sync_config.js b/src/stores/sync_config.js index ba4031e9a..7a7afe36c 100644 --- a/src/stores/sync_config.js +++ b/src/stores/sync_config.js @@ -9,6 +9,7 @@ import { groupBy, isEqual, set, + take, takeRight, uniqWith, unset, @@ -219,7 +220,7 @@ export const _mergeFlags = (recent, stale, allFlagKeys) => { ) } -const _mergeJournal = (...journals) => { +export const _mergeJournal = (...journals) => { // Ignore invalid journal entries const allJournals = flatten( journals.map((j) => (Array.isArray(j) ? j : [])), @@ -267,9 +268,11 @@ const _mergeJournal = (...journals) => { return journal } }) - return flatten(trimmedGrouped).sort((a, b) => + + const flat = flatten(trimmedGrouped).sort((a, b) => a.timestamp > b.timestamp ? 1 : -1, ) + return take(flat, 500) } export const _mergePrefs = (recent, stale) => { @@ -621,12 +624,26 @@ export const useSyncConfigStore = defineStore('sync_config', { 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 }, + prefsStorage: { ...defaultState.prefsStorage }, + }) + } + + recent = recent && (await _doMigrations(recent, this.setPreference)) + stale = stale && (await _doMigrations(stale, this.setPreference)) + + // Various migrations console.debug('Migrating from old config') const vuexState = (await storage.getItem('vuex-lz')) ?? {} vuexState.config = vuexState.config ?? {} const migratedEntries = new Set(vuexState.config._syncMigration ?? []) - console.debug(`Already migrated Values: ${[...migratedEntries].join()}`) + console.debug(`Already migrated Values: ${[...migratedEntries].join() || '[none]'}`) Object.entries(oldDefaultConfigSync).forEach(([key, value]) => { const oldValue = vuexState.config[key] @@ -670,19 +687,6 @@ export const useSyncConfigStore = defineStore('sync_config', { vuexState.config._syncMigration = [...migratedEntries] storage.setItem('vuex-lz', vuexState) - if (recent === null) { - console.debug( - `Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`, - ) - recent = _wrapData({ - flagStorage: { ...flagsTemplate }, - prefsStorage: { ...defaultState.prefsStorage }, - }) - } - - recent = recent && (await _doMigrations(recent, this.setPreference)) - stale = stale && (await _doMigrations(stale, this.setPreference)) - if (!needUpload && recent && stale) { console.debug('Checking if data needs merging...') // discarding timestamps and versions @@ -722,6 +726,7 @@ export const useSyncConfigStore = defineStore('sync_config', { this.flagStorage = this.cache.flagStorage this.prefsStorage = this.cache.prefsStorage this.pushSyncConfig() + console.log('C', this.cache) }, pushSyncConfig({ force = false } = {}) { const needPush = this.dirty || force diff --git a/src/sw.js b/src/sw.js index 18c74cae8..b7c551480 100644 --- a/src/sw.js +++ b/src/sw.js @@ -5,10 +5,10 @@ import 'virtual:pleroma-fe/service_worker_env' import { createI18n } from 'vue-i18n' import { storage } from 'src/lib/storage.js' +import { instanceDefaultConfig } from 'src/modules/default_config_state.js' import { parseNotification } from 'src/services/entity_normalizer/entity_normalizer.service.js' import { prepareNotificationObject } from 'src/services/notification_utils/notification_utils.js' import { cacheKey, emojiCacheKey, shouldCache } from 'src/services/sw/sw.js' -import { instanceDefaultConfig } from 'src/modules/default_config_state.js' // Collects all messages for service workers // Needed because service workers cannot use dynamic imports diff --git a/test/unit/specs/stores/sync_config.spec.js b/test/unit/specs/stores/sync_config.spec.js index 9aed6193b..8078b5949 100644 --- a/test/unit/specs/stores/sync_config.spec.js +++ b/test/unit/specs/stores/sync_config.spec.js @@ -15,6 +15,7 @@ import { useSyncConfigStore, VERSION, } from 'src/stores/sync_config.js' +import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' describe('The SyncConfig store', () => { beforeEach(() => { @@ -28,40 +29,52 @@ describe('The SyncConfig store', () => { storage: {}, } - it('should initialize storage if none present', () => { + it('should initialize storage if none present', async () => { const store = useSyncConfigStore() - store.initSyncConfig({ ...user }) + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } + await store.initSyncConfig({ ...user }) expect(store.cache._version).to.eql(VERSION) expect(store.cache._timestamp).to.be.a('number') expect(store.cache.flagStorage).to.eql(defaultState.flagStorage) expect(store.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) - it('should initialize storage with proper flags for new users if none present', () => { + it('should initialize storage with proper flags for new users if none present', async () => { const store = useSyncConfigStore() - store.initSyncConfig({ ...user, created_at: new Date() }) + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } + await store.initSyncConfig({ ...user, created_at: new Date() }) expect(store.cache._version).to.eql(VERSION) expect(store.cache._timestamp).to.be.a('number') expect(store.cache.flagStorage).to.eql(newUserFlags) expect(store.cache.prefsStorage).to.eql(defaultState.prefsStorage) }) - it('should merge flags even if remote timestamp is older', () => { + it('should merge flags even if remote timestamp is older', async () => { const store = useSyncConfigStore() + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } store.cache = { _timestamp: Date.now(), _version: VERSION, ...cloneDeep(defaultState), } - store.initSyncConfig({ + await store.initSyncConfig({ ...user, storage: { _timestamp: 123, _version: VERSION, flagStorage: { ...defaultState.flagStorage, - updateCounter: 1, + updateCounter: CURRENT_UPDATE_COUNTER, }, prefsStorage: { ...defaultState.prefsStorage, @@ -69,17 +82,62 @@ describe('The SyncConfig store', () => { }, }) - expect(store.cache.flagStorage).to.eql({ + expect(store.flagStorage).to.eql({ ...defaultState.flagStorage, - updateCounter: 1, + updateCounter: CURRENT_UPDATE_COUNTER, }) }) - it('should reset local timestamp to remote if contents are the same', () => { + it('should trim journal to 500 entries', async () => { + const store = useSyncConfigStore() + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } + store.cache = { + _timestamp: Date.now(), + _version: VERSION, + ...cloneDeep(defaultState), + } + const largeJournal = [] + for (let value = 0; value < 1000; value++) { + largeJournal.push({ + path: 'simple.testing' + value, + operation: 'set', + args: [value], + // should have A timestamp, we don't really care what it is + timestamp: 123456, + }) + } + + await store.initSyncConfig({ + ...user, + storage: { + _timestamp: 123, + _version: VERSION, + flagStorage: { + ...defaultState.flagStorage, + updateCounter: CURRENT_UPDATE_COUNTER, + }, + prefsStorage: { + ...defaultState.prefsStorage, + _journal: largeJournal, + }, + }, + }) + + expect(store.prefsStorage._journal.length).to.eql(500) + }) + + it('should reset local timestamp to remote if contents are the same', async () => { const store = useSyncConfigStore() store.cache = null + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } - store.initSyncConfig({ + await store.initSyncConfig({ ...user, storage: { _timestamp: 123, @@ -95,9 +153,13 @@ describe('The SyncConfig store', () => { expect(store.cache.flagStorage.updateCounter).to.eql(999) }) - it('should use remote version if local missing', () => { + it('should use remote version if local missing', async () => { const store = useSyncConfigStore() - store.initSyncConfig(store, user) + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } + await store.initSyncConfig(store, user) expect(store.cache._version).to.eql(VERSION) expect(store.cache._timestamp).to.be.a('number') expect(store.cache.flagStorage).to.eql(defaultState.flagStorage) @@ -106,6 +168,10 @@ describe('The SyncConfig store', () => { describe('setPreference', () => { it('should set preference and update journal log accordingly', () => { const store = useSyncConfigStore() + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } store.setPreference({ path: 'simple.testing', value: 1 }) expect(store.prefsStorage.simple.testing).to.eql(1) expect(store.prefsStorage._journal.length).to.eql(1) @@ -120,6 +186,10 @@ describe('The SyncConfig store', () => { it('should keep journal to a minimum', () => { const store = useSyncConfigStore() + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } store.setPreference({ path: 'simple.testing', value: 1 }) store.setPreference({ path: 'simple.testing', value: 2 }) store.addCollectionPreference({ path: 'collections.testing', value: 2 }) @@ -149,6 +219,10 @@ describe('The SyncConfig store', () => { it('should remove duplicate entries from journal', () => { const store = useSyncConfigStore() + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } store.setPreference({ path: 'simple.testing', value: 1 }) store.setPreference({ path: 'simple.testing', value: 1 }) store.addCollectionPreference({ path: 'collections.testing', value: 2 }) @@ -161,6 +235,10 @@ describe('The SyncConfig store', () => { it('should remove depth = 3 set/unset entries from journal', () => { const store = useSyncConfigStore() + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } store.setPreference({ path: 'simple.object.foo', value: 1 }) store.unsetPreference({ path: 'simple.object.foo' }) store.updateCache(store, { username: 'test' }) @@ -170,6 +248,10 @@ describe('The SyncConfig store', () => { it('should not allow unsetting depth <= 2', () => { const store = useSyncConfigStore() + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } store.setPreference({ path: 'simple.object.foo', value: 1 }) expect(() => store.unsetPreference({ path: 'simple' })).to.throw() expect(() => @@ -179,6 +261,10 @@ describe('The SyncConfig store', () => { it('should not allow (un)setting depth > 3', () => { const store = useSyncConfigStore() + // PushSyncConfig is very simple but uses vuex to push data + store.pushSyncConfig = () => { + /* no-op */ + } store.setPreference({ path: 'simple.object', value: {} }) expect(() => store.setPreference({ path: 'simple.object.lv3', value: 1 }),