From 888d2e9ccebeaf5a12e1333358279253a454fb2b Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins Date: Mon, 7 Oct 2024 18:07:31 +0100 Subject: [PATCH] :zap: (synced-prefs) preloading in redux state to improve perf (#3544) --- .../desktop-client/src/hooks/useSyncedPref.ts | 27 +++++-------- .../src/hooks/useSyncedPrefs.ts | 39 ++++++------------- .../loot-core/src/client/actions/prefs.ts | 25 +++++++++++- packages/loot-core/src/client/actions/sync.ts | 8 ++-- packages/loot-core/src/client/constants.ts | 1 + .../loot-core/src/client/reducers/prefs.ts | 18 +++++++-- .../src/client/state-types/prefs.d.ts | 16 +++++++- .../loot-core/src/server/preferences/app.ts | 13 +++++++ .../server/preferences/types/handlers.d.ts | 2 + upcoming-release-notes/3544.md | 6 +++ 10 files changed, 100 insertions(+), 55 deletions(-) create mode 100644 upcoming-release-notes/3544.md diff --git a/packages/desktop-client/src/hooks/useSyncedPref.ts b/packages/desktop-client/src/hooks/useSyncedPref.ts index 5e226e60692..67808eeb97a 100644 --- a/packages/desktop-client/src/hooks/useSyncedPref.ts +++ b/packages/desktop-client/src/hooks/useSyncedPref.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; -import { useQuery } from 'loot-core/client/query-hooks'; -import { send } from 'loot-core/platform/client/fetch'; -import { q } from 'loot-core/shared/query'; +import { saveSyncedPrefs } from 'loot-core/client/actions'; +import { type State } from 'loot-core/client/state-types'; import { type SyncedPrefs } from 'loot-core/src/types/prefs'; type SetSyncedPrefAction = ( @@ -12,21 +12,14 @@ type SetSyncedPrefAction = ( export function useSyncedPref( prefName: K, ): [SyncedPrefs[K], SetSyncedPrefAction] { - const { data: queryData, overrideData: setQueryData } = useQuery< - [{ value: string | undefined }] - >( - () => q('preferences').filter({ id: prefName }).select('value'), - [prefName], - ); - - const setLocalPref = useCallback>( - newValue => { - const value = String(newValue); - setQueryData([{ value }]); - send('preferences/save', { id: prefName, value }); + const dispatch = useDispatch(); + const setPref = useCallback>( + value => { + dispatch(saveSyncedPrefs({ [prefName]: value })); }, - [prefName, setQueryData], + [prefName, dispatch], ); + const pref = useSelector((state: State) => state.prefs.synced[prefName]); - return [queryData?.[0]?.value, setLocalPref]; + return [pref, setPref]; } diff --git a/packages/desktop-client/src/hooks/useSyncedPrefs.ts b/packages/desktop-client/src/hooks/useSyncedPrefs.ts index ca8788ecb34..57493794892 100644 --- a/packages/desktop-client/src/hooks/useSyncedPrefs.ts +++ b/packages/desktop-client/src/hooks/useSyncedPrefs.ts @@ -1,39 +1,22 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; -import { useQuery } from 'loot-core/client/query-hooks'; -import { send } from 'loot-core/platform/client/fetch'; -import { q } from 'loot-core/shared/query'; +import { saveSyncedPrefs } from 'loot-core/client/actions'; +import { type State } from 'loot-core/client/state-types'; import { type SyncedPrefs } from 'loot-core/src/types/prefs'; type SetSyncedPrefsAction = (value: Partial) => void; /** @deprecated: please use `useSyncedPref` (singular) */ export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] { - const { data: queryData } = useQuery<{ id: string; value: string }[]>( - () => q('preferences').select(['id', 'value']), - [], + const dispatch = useDispatch(); + const setPrefs = useCallback( + newValue => { + dispatch(saveSyncedPrefs(newValue)); + }, + [dispatch], ); - - const prefs = useMemo( - () => - (queryData ?? []).reduce( - (carry, { id, value }) => ({ - ...carry, - [id]: value, - }), - {}, - ), - [queryData], - ); - - const setPrefs = useCallback(newValue => { - Object.entries(newValue).forEach(([id, value]) => { - send('preferences/save', { - id: id as keyof SyncedPrefs, - value: String(value), - }); - }); - }, []); + const prefs = useSelector((state: State) => state.prefs.synced); return [prefs, setPrefs]; } diff --git a/packages/loot-core/src/client/actions/prefs.ts b/packages/loot-core/src/client/actions/prefs.ts index c77be9c1ead..cc690f21520 100644 --- a/packages/loot-core/src/client/actions/prefs.ts +++ b/packages/loot-core/src/client/actions/prefs.ts @@ -1,5 +1,9 @@ import { send } from '../../platform/client/fetch'; -import { type GlobalPrefs, type MetadataPrefs } from '../../types/prefs'; +import { + type GlobalPrefs, + type MetadataPrefs, + type SyncedPrefs, +} from '../../types/prefs'; import * as constants from '../constants'; import { closeModal } from './modals'; @@ -19,6 +23,7 @@ export function loadPrefs() { type: constants.SET_PREFS, prefs, globalPrefs: await send('load-global-prefs'), + syncedPrefs: await send('preferences/get'), }); return prefs; @@ -42,6 +47,7 @@ export function loadGlobalPrefs() { type: constants.SET_PREFS, prefs: getState().prefs.local, globalPrefs, + syncedPrefs: getState().prefs.synced, }); return globalPrefs; }; @@ -60,3 +66,20 @@ export function saveGlobalPrefs( onSaveGlobalPrefs?.(); }; } + +export function saveSyncedPrefs(prefs: SyncedPrefs) { + return async (dispatch: Dispatch) => { + await Promise.all( + Object.entries(prefs).map(([prefName, value]) => + send('preferences/save', { + id: prefName as keyof SyncedPrefs, + value, + }), + ), + ); + dispatch({ + type: constants.MERGE_SYNCED_PREFS, + syncedPrefs: prefs, + }); + }; +} diff --git a/packages/loot-core/src/client/actions/sync.ts b/packages/loot-core/src/client/actions/sync.ts index 77838897fca..0d55026f9eb 100644 --- a/packages/loot-core/src/client/actions/sync.ts +++ b/packages/loot-core/src/client/actions/sync.ts @@ -1,4 +1,3 @@ -// @ts-strict-ignore import { send } from '../../platform/client/fetch'; import { getUploadError } from '../../shared/errors'; @@ -32,7 +31,6 @@ export function resetSync() { } } else { await dispatch(sync()); - await dispatch(loadPrefs()); } }; } @@ -45,8 +43,12 @@ export function sync() { if ('error' in result) { return { error: result.error }; } - return {}; + + // Update the prefs + await dispatch(loadPrefs()); } + + return {}; }; } diff --git a/packages/loot-core/src/client/constants.ts b/packages/loot-core/src/client/constants.ts index e7c59748c76..e4f968a22e5 100644 --- a/packages/loot-core/src/client/constants.ts +++ b/packages/loot-core/src/client/constants.ts @@ -10,6 +10,7 @@ export const LOAD_PAYEES = 'LOAD_PAYEES'; export const SET_PREFS = 'SET_PREFS'; export const MERGE_LOCAL_PREFS = 'MERGE_LOCAL_PREFS'; export const MERGE_GLOBAL_PREFS = 'MERGE_GLOBAL_PREFS'; +export const MERGE_SYNCED_PREFS = 'MERGE_SYNCED_PREFS'; export const SET_BUDGETS = 'SET_BUDGETS'; export const SET_REMOTE_FILES = 'SET_REMOTE_FILES'; export const SET_ALL_FILES = 'SET_ALL_FILES'; diff --git a/packages/loot-core/src/client/reducers/prefs.ts b/packages/loot-core/src/client/reducers/prefs.ts index 5523f5d552c..108c9eb551d 100644 --- a/packages/loot-core/src/client/reducers/prefs.ts +++ b/packages/loot-core/src/client/reducers/prefs.ts @@ -1,17 +1,22 @@ -// @ts-strict-ignore import * as constants from '../constants'; import type { Action } from '../state-types'; import type { PrefsState } from '../state-types/prefs'; const initialState: PrefsState = { - local: null, - global: null, + local: {}, + global: {}, + synced: {}, }; export function update(state = initialState, action: Action): PrefsState { switch (action.type) { case constants.SET_PREFS: - return { local: action.prefs, global: action.globalPrefs }; + return { + ...state, + local: action.prefs, + global: action.globalPrefs, + synced: action.syncedPrefs, + }; case constants.MERGE_LOCAL_PREFS: return { ...state, @@ -22,6 +27,11 @@ export function update(state = initialState, action: Action): PrefsState { ...state, global: { ...state.global, ...action.globalPrefs }, }; + case constants.MERGE_SYNCED_PREFS: + return { + ...state, + synced: { ...state.synced, ...action.syncedPrefs }, + }; default: } diff --git a/packages/loot-core/src/client/state-types/prefs.d.ts b/packages/loot-core/src/client/state-types/prefs.d.ts index 4c5e13c133a..184dcdc5f44 100644 --- a/packages/loot-core/src/client/state-types/prefs.d.ts +++ b/packages/loot-core/src/client/state-types/prefs.d.ts @@ -1,15 +1,21 @@ -import type { GlobalPrefs, MetadataPrefs } from '../../types/prefs'; +import type { + GlobalPrefs, + MetadataPrefs, + SyncedPrefs, +} from '../../types/prefs'; import type * as constants from '../constants'; export type PrefsState = { local: MetadataPrefs; global: GlobalPrefs; + synced: SyncedPrefs; }; export type SetPrefsAction = { type: typeof constants.SET_PREFS; prefs: MetadataPrefs; globalPrefs: GlobalPrefs; + syncedPrefs: SyncedPrefs; }; export type MergeLocalPrefsAction = { @@ -22,7 +28,13 @@ export type MergeGlobalPrefsAction = { globalPrefs: GlobalPrefs; }; +export type MergeSyncedPrefsAction = { + type: typeof constants.MERGE_SYNCED_PREFS; + syncedPrefs: SyncedPrefs; +}; + export type PrefsActions = | SetPrefsAction | MergeLocalPrefsAction - | MergeGlobalPrefsAction; + | MergeGlobalPrefsAction + | MergeSyncedPrefsAction; diff --git a/packages/loot-core/src/server/preferences/app.ts b/packages/loot-core/src/server/preferences/app.ts index 7d4f6517e8d..d1c73344e70 100644 --- a/packages/loot-core/src/server/preferences/app.ts +++ b/packages/loot-core/src/server/preferences/app.ts @@ -18,4 +18,17 @@ const savePreferences = async ({ await db.update('preferences', { id, value }); }; +const getPreferences = async (): Promise => { + const prefs = (await db.all('SELECT id, value FROM preferences')) as Array<{ + id: string; + value: string; + }>; + + return prefs.reduce((carry, { value, id }) => { + carry[id as keyof SyncedPrefs] = value; + return carry; + }, {}); +}; + app.method('preferences/save', mutator(undoable(savePreferences))); +app.method('preferences/get', getPreferences); diff --git a/packages/loot-core/src/server/preferences/types/handlers.d.ts b/packages/loot-core/src/server/preferences/types/handlers.d.ts index 78e1a811867..8af5516bd1f 100644 --- a/packages/loot-core/src/server/preferences/types/handlers.d.ts +++ b/packages/loot-core/src/server/preferences/types/handlers.d.ts @@ -5,4 +5,6 @@ export interface PreferencesHandlers { id: keyof SyncedPrefs; value: string | undefined; }) => Promise; + + 'preferences/get': () => Promise; } diff --git a/upcoming-release-notes/3544.md b/upcoming-release-notes/3544.md new file mode 100644 index 00000000000..6971c0f66e3 --- /dev/null +++ b/upcoming-release-notes/3544.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +SyncedPrefs: preload in redux state and fetch from there; improved performance.