-
Notifications
You must be signed in to change notification settings - Fork 17
feat(useSetting): sync values with LS and store in api #3111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,107 +6,202 @@ import type { | |
| SetSingleSettingParams, | ||
| Setting, | ||
| } from '../../../types/api/settings'; | ||
| import type {AppDispatch} from '../../defaultStore'; | ||
| import type {AppDispatch, RootState} from '../../defaultStore'; | ||
| import {api} from '../api'; | ||
|
|
||
| import {SETTINGS_OPTIONS} from './constants'; | ||
| import {getSettingValue, setSettingValue} from './settings'; | ||
| import { | ||
| getSettingDefault, | ||
| parseSettingValue, | ||
| readSettingValueFromLS, | ||
| setSettingValueToLS, | ||
| shouldSyncSettingToLS, | ||
| stringifySettingValue, | ||
| } from './utils'; | ||
|
|
||
| const invalidParamsError = | ||
| 'Missing required parameters (name, user) or MetaSettings API is not available'; | ||
|
|
||
| export const settingsApi = api.injectEndpoints({ | ||
| endpoints: (builder) => ({ | ||
| getSingleSetting: builder.query({ | ||
| queryFn: async ({name, user}: GetSingleSettingParams, baseApi) => { | ||
| getSingleSetting: builder.query<Setting | undefined, Partial<GetSingleSettingParams>>({ | ||
| queryFn: async ({name, user}) => { | ||
| try { | ||
| if (!window.api.metaSettings) { | ||
| throw new Error('MetaSettings API is not available'); | ||
| // In this case localStorage should be used for settings | ||
| // Actual value will be loaded to store in onQueryStarted | ||
| if (!name || !user || !window.api.metaSettings) { | ||
| throw new Error(invalidParamsError); | ||
| } | ||
|
|
||
| const data = await window.api.metaSettings.getSingleSetting({ | ||
| name, | ||
| user, | ||
| // Directly access options here to avoid them in cache key | ||
| preventBatching: SETTINGS_OPTIONS[name]?.preventBatching, | ||
| }); | ||
|
|
||
| const dispatch = baseApi.dispatch as AppDispatch; | ||
|
|
||
| // Try to sync local value if there is no backend value | ||
| syncLocalValueToMetaIfNoData(data, dispatch); | ||
|
|
||
| return {data}; | ||
| } catch (error) { | ||
| console.error('Cannot get setting:', error); | ||
| return {error}; | ||
| } | ||
| }, | ||
| async onQueryStarted(args, {dispatch, queryFulfilled}) { | ||
| const {name, user} = args; | ||
|
|
||
| if (!name) { | ||
| return; | ||
| } | ||
|
|
||
| const shouldUseLocalSettings = !user || !window.api.metaSettings; | ||
|
|
||
| const defaultValue = getSettingDefault(name); | ||
|
|
||
| // Preload value from LS or default to store | ||
| if (shouldUseLocalSettings || shouldSyncSettingToLS(name)) { | ||
| const savedValue = readSettingValueFromLS(name); | ||
| const value = savedValue ?? defaultValue; | ||
|
|
||
| dispatch(setSettingValue(name, value)); | ||
| } else { | ||
| dispatch(setSettingValue(name, defaultValue)); | ||
| } | ||
|
|
||
| // Do not process query result when only LS is used | ||
| // There is not actual api call in this case | ||
| if (shouldUseLocalSettings) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const {data} = await queryFulfilled; | ||
|
|
||
| // Load api value to store if present | ||
| // In case local storage should be used | ||
| // query will finish with an error and this code will not run | ||
| const parsedValue = parseSettingValue(data?.value); | ||
|
|
||
| if (isNil(data?.value)) { | ||
| // Pass setting params without value if data is empty | ||
| const newValue = data ?? {name, user}; | ||
|
|
||
| // Try to sync local value if there is no backend value | ||
| syncLocalValueToMetaIfNoData(newValue, dispatch); | ||
| } else { | ||
| dispatch(setSettingValue(name, parsedValue)); | ||
|
|
||
| if (shouldSyncSettingToLS(name)) { | ||
| setSettingValueToLS(name, data.value); | ||
| } | ||
| } | ||
| } catch { | ||
| // In case of an error there is no value to sync | ||
| // LS or default value was loaded to store | ||
| } | ||
| }, | ||
| }), | ||
| setSingleSetting: builder.mutation({ | ||
| queryFn: async (params: SetSingleSettingParams) => { | ||
| queryFn: async ({ | ||
| name, | ||
| user, | ||
| value, | ||
| }: Partial<Omit<SetSingleSettingParams, 'value'>> & {value: unknown}) => { | ||
| try { | ||
| if (!window.api.metaSettings) { | ||
| throw new Error('MetaSettings API is not available'); | ||
| if (!name || !user || !window.api.metaSettings) { | ||
| throw new Error(invalidParamsError); | ||
| } | ||
|
|
||
| const data = await window.api.metaSettings.setSingleSetting(params); | ||
| const data = await window.api.metaSettings.setSingleSetting({ | ||
| name, | ||
| user, | ||
| value: stringifySettingValue(value), | ||
| }); | ||
artemmufazalov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (data.status !== 'SUCCESS') { | ||
| throw new Error('Setting status is not SUCCESS'); | ||
| } | ||
|
|
||
| return {data}; | ||
| } catch (error) { | ||
| console.error('Cannot update setting:', error); | ||
| return {error}; | ||
| } | ||
| }, | ||
| async onQueryStarted(args, {dispatch, queryFulfilled}) { | ||
| async onQueryStarted(args, {dispatch, queryFulfilled, getState}) { | ||
| const {name, user, value} = args; | ||
|
|
||
| // Optimistically update existing cache entry | ||
| const patchResult = dispatch( | ||
| settingsApi.util.updateQueryData('getSingleSetting', {name, user}, (draft) => { | ||
| return {...draft, name, user, value}; | ||
| }), | ||
| ); | ||
| if (!name) { | ||
| return; | ||
| } | ||
|
|
||
| // Extract previous value to revert to it if set is not successful | ||
| const previousSettingValue = getSettingValue(getState() as RootState, name); | ||
|
|
||
| // Optimistically update store | ||
| dispatch(setSettingValue(name, value)); | ||
|
|
||
| const shouldUseLocalSettings = !user || !window.api.metaSettings; | ||
|
|
||
| // If local storage settings should be used | ||
| // Update LS and return early | ||
| if (shouldUseLocalSettings) { | ||
| setSettingValueToLS(name, value); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| await queryFulfilled; | ||
|
|
||
| // If mutation is successful, we can store new value in LS | ||
| if (shouldSyncSettingToLS(name)) { | ||
| setSettingValueToLS(name, value); | ||
| } | ||
| } catch { | ||
astandrik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| patchResult.undo(); | ||
| // Set previous value to store in case of error | ||
| dispatch(setSettingValue(name, previousSettingValue)); | ||
| } | ||
| }, | ||
| }), | ||
| getSettings: builder.query({ | ||
| queryFn: async ({name, user}: GetSettingsParams, baseApi) => { | ||
| queryFn: async ({name, user}: Partial<GetSettingsParams>, baseApi) => { | ||
| try { | ||
| if (!window.api.metaSettings) { | ||
| throw new Error('MetaSettings API is not available'); | ||
| if (!window.api.metaSettings || !name || !user) { | ||
| throw new Error(invalidParamsError); | ||
| } | ||
| const data = await window.api.metaSettings.getSettings({name, user}); | ||
|
|
||
| const patches: Promise<void>[] = []; | ||
| const patches: Promise<unknown>[] = []; | ||
| const dispatch = baseApi.dispatch as AppDispatch; | ||
|
|
||
| // Upsert received data in getSingleSetting cache | ||
| // Upsert received data in getSingleSetting cache to prevent further redundant requests | ||
| name.forEach((settingName) => { | ||
| const settingData = data[settingName] ?? {}; | ||
|
|
||
| const cacheEntryParams: GetSingleSettingParams = { | ||
| name: settingName, | ||
| user, | ||
| }; | ||
| const newValue = {name: settingName, user, value: settingData?.value}; | ||
| const newSetting = {name: settingName, user, value: settingData?.value}; | ||
|
|
||
| const patch = dispatch( | ||
| settingsApi.util.upsertQueryData( | ||
| 'getSingleSetting', | ||
| cacheEntryParams, | ||
| newValue, | ||
| newSetting, | ||
| ), | ||
| ).then(() => { | ||
| ); | ||
| if (isNil(newSetting.value)) { | ||
| // Try to sync local value if there is no backend value | ||
| // Do it after upsert if finished to ensure proper values update order | ||
| // 1. New entry added to cache with nil value | ||
| // 2. Positive entry update - local storage value replace nil in cache | ||
| // 3.1. Set is successful, local value in cache | ||
| // 3.2. Set is not successful, cache value reverted to previous nil | ||
| syncLocalValueToMetaIfNoData(settingData, dispatch); | ||
| }); | ||
| syncLocalValueToMetaIfNoData(newSetting, dispatch); | ||
| } else { | ||
| const parsedValue = parseSettingValue(newSetting.value); | ||
| dispatch(setSettingValue(settingName, parsedValue)); | ||
|
|
||
| if (shouldSyncSettingToLS(settingName)) { | ||
| setSettingValueToLS(settingName, newSetting.value); | ||
| } | ||
| } | ||
|
|
||
| patches.push(patch); | ||
| }); | ||
|
|
@@ -116,6 +211,7 @@ export const settingsApi = api.injectEndpoints({ | |
|
|
||
| return {data}; | ||
| } catch (error) { | ||
| console.error('Cannot get settings:', error); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can it be that error is "Cannot get setting" - but the real situation is that one of patches failed ? |
||
| return {error}; | ||
| } | ||
| }, | ||
|
|
@@ -124,7 +220,11 @@ export const settingsApi = api.injectEndpoints({ | |
| overrideExisting: 'throw', | ||
| }); | ||
|
|
||
| function syncLocalValueToMetaIfNoData(params: Setting, dispatch: AppDispatch) { | ||
| function syncLocalValueToMetaIfNoData(params: Partial<Setting>, dispatch: AppDispatch) { | ||
| if (!params.name) { | ||
| return; | ||
| } | ||
|
|
||
| const localValue = localStorage.getItem(params.name); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we use localStorage directly here but there are already functions for this like readSettingValueFromLS / shouldSyncSettingToLS these functions as I may suppose were created to make work with LS somehow guarded |
||
|
|
||
| if (isNil(params.value) && !isNil(localValue)) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it possible that null/undefined on backend is intentional?
i mean we removed some setting for example
but LS has some old value (in another browser for example) and pushes this setting to backend?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a current limitation, yes. We cannot have null or undefined as backend value, there should be at least something, otherwise defined localStorage value will be loaded to backend. Without this it's unclear, when we should sync local value to backend