Sync: handle fast-actions being reverted

## Repro
1. Follow a channel.
2. When `preference_set` is sent, unfollow the channel.
3. A few seconds later, the final setting reflects (1) instead of (2).

The current sync loop involves doing a final `sync/get` at the end. While not necessary for the scenario above, the code flow covers various cases, so it's still needed for now.

## Approach
Implement an abort mechanism to the sync-loop.

When syncing from the `buildSharedStateMiddleware` loop, generate an ID for each sync session, and only store the latest one. Pass the ID to the completion-callback (and other places as needed), so we can check if our session is still the latest one before executing the callback.

The check for invalidation can also be placed in more places to cut off the sync process earlier, but it's only done for 2 critical places for now.
This commit is contained in:
infinite-persistence 2021-12-23 16:27:03 +08:00 committed by Thomas Zarebczan
parent 2d1b876acc
commit d7e3127e65
7 changed files with 48 additions and 11 deletions

1
flow-typed/sync.js vendored
View file

@ -11,6 +11,7 @@ declare type SyncState = {
setSyncIsPending: boolean,
prefsReady: boolean,
syncLocked: boolean,
sharedStateSyncId: number,
hashChanged: boolean,
fatalError: boolean,
};

View file

@ -451,6 +451,7 @@ export const SYNC_APPLY_BAD_PASSWORD = 'SYNC_APPLY_BAD_PASSWORD';
export const SYNC_RESET = 'SYNC_RESET';
export const SYNC_FATAL_ERROR = 'SYNC_FATAL_ERROR';
export const USER_STATE_POPULATE = 'USER_STATE_POPULATE';
export const SHARED_STATE_SYNC_ID_CHANGED = 'SHARED_STATE_SYNC_ID_CHANGED';
export const REACTIONS_LIST_STARTED = 'REACTIONS_LIST_STARTED';
export const REACTIONS_LIST_FAILED = 'REACTIONS_LIST_FAILED';

View file

@ -42,7 +42,7 @@ import {
} from 'redux/selectors/app';
import { selectDaemonSettings, selectClientSetting } from 'redux/selectors/settings';
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
import { doSetPrefsReady, doPreferenceGet, doPopulateSharedUserState } from 'redux/actions/sync';
import { doSetPrefsReady, doPreferenceGet, doPopulateSharedUserState, syncInvalidated } from 'redux/actions/sync';
import { doAuthenticate } from 'redux/actions/user';
import { lbrySettings as config, version as appVersion } from 'package.json';
import analytics, { SHARE_INTERNAL } from 'analytics';
@ -596,7 +596,7 @@ export function doToggle3PAnalytics(allowParam, doNotDispatch) {
};
}
export function doGetAndPopulatePreferences() {
export function doGetAndPopulatePreferences(syncId /* ?: number */) {
const { SDK_SYNC_KEYS } = SHARED_PREFERENCES;
return (dispatch, getState) => {
@ -615,7 +615,10 @@ export function doGetAndPopulatePreferences() {
const successState = getState();
const daemonSettings = selectDaemonSettings(successState);
if (savedPreferences !== null) {
if (!syncInvalidated(getState, syncId)) {
dispatch(doPopulateSharedUserState(savedPreferences));
}
// @if TARGET='app'
const { settings } = savedPreferences.value;
@ -660,11 +663,15 @@ export function doGetAndPopulatePreferences() {
};
}
export function doHandleSyncComplete(error, hasNewData) {
return (dispatch) => {
export function doHandleSyncComplete(error, hasNewData, syncId) {
return (dispatch, getState) => {
if (!error) {
if (hasNewData) {
dispatch(doGetAndPopulatePreferences());
if (syncInvalidated(getState, syncId)) {
return;
}
dispatch(doGetAndPopulatePreferences(syncId));
// we just got sync data, better update our channels
dispatch(doFetchChannelListMine());
}

View file

@ -21,6 +21,19 @@ const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes
const NO_WALLET_ERROR = 'no wallet found for this user';
const BAD_PASSWORD_ERROR_NAME = 'InvalidPasswordError';
/**
* Checks if there is a newer sync session, indicating that fetched data from
* the current one can be dropped.
*
* @param getState
* @param syncId [Optional] The id of the current sync session. If not given, assume not invalidated.
* @returns {boolean}
*/
export function syncInvalidated(getState: GetState, syncId?: number) {
const state = getState();
return syncId && state.sync.sharedStateSyncId !== syncId;
}
export function doSetDefaultAccount(success: () => void, failure: (string) => void) {
return (dispatch: Dispatch) => {
dispatch({
@ -115,7 +128,14 @@ export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) =>
});
};
export function doSyncLoop(noInterval?: boolean) {
/**
* doSyncLoop
*
* @param noInterval
* @param syncId Optional ID to identify a specific loop. Can be used to abort the loop, for example.
* @returns {(function(Dispatch, GetState): void)|*}
*/
export function doSyncLoop(noInterval?: boolean, syncId?: number) {
return (dispatch: Dispatch, getState: GetState) => {
if (!noInterval && syncTimer) clearInterval(syncTimer);
const state = getState();
@ -123,7 +143,7 @@ export function doSyncLoop(noInterval?: boolean) {
const syncEnabled = selectClientSetting(state, SETTINGS.ENABLE_SYNC);
const syncLocked = selectSyncIsLocked(state);
if (hasVerifiedEmail && syncEnabled && !syncLocked) {
dispatch(doGetSyncDesktop((error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData))));
dispatch(doGetSyncDesktop((error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData, syncId))));
if (!noInterval) {
syncTimer = setInterval(() => {
const state = getState();

View file

@ -1,4 +1,5 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import isEqual from 'util/deep-equal';
import { doPreferenceSet } from 'redux/actions/sync';
@ -47,10 +48,12 @@ export const buildSharedStateMiddleware = (
if (sharedStateCb) {
// Pass dispatch to the callback to consumers can dispatch actions in response to preference set
sharedStateCb({ dispatch, getState });
sharedStateCb({ dispatch, getState, syncId: timeout });
}
clearTimeout(timeout);
return actionResult;
}
timeout = setTimeout(runPreferences, RUN_PREFERENCES_DELAY_MS);
dispatch({ type: ACTIONS.SHARED_STATE_SYNC_ID_CHANGED, data: timeout });
};

View file

@ -16,6 +16,7 @@ const defaultState: SyncState = {
setSyncIsPending: false,
prefsReady: false,
syncLocked: false,
sharedStateSyncId: -1,
hashChanged: false,
fatalError: false,
};
@ -114,6 +115,10 @@ reducers[ACTIONS.SYNC_FATAL_ERROR] = (state: SyncState) => {
reducers[ACTIONS.SYNC_RESET] = () => defaultState;
reducers[ACTIONS.SHARED_STATE_SYNC_ID_CHANGED] = (state: SyncState, action: any) => {
return { ...state, sharedStateSyncId: action.data };
};
export default function syncReducer(state: SyncState = defaultState, action: any) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -185,8 +185,8 @@ const sharedStateFilters = {
unpublishedCollections: { source: 'collections', property: 'unpublished' },
};
const sharedStateCb = ({ dispatch, getState }) => {
dispatch(doSyncLoop());
const sharedStateCb = ({ dispatch, getState, syncId }) => {
dispatch(doSyncLoop(false, syncId));
};
const populateAuthTokenHeader = () => {