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:
parent
2d1b876acc
commit
d7e3127e65
7 changed files with 48 additions and 11 deletions
1
flow-typed/sync.js
vendored
1
flow-typed/sync.js
vendored
|
@ -11,6 +11,7 @@ declare type SyncState = {
|
||||||
setSyncIsPending: boolean,
|
setSyncIsPending: boolean,
|
||||||
prefsReady: boolean,
|
prefsReady: boolean,
|
||||||
syncLocked: boolean,
|
syncLocked: boolean,
|
||||||
|
sharedStateSyncId: number,
|
||||||
hashChanged: boolean,
|
hashChanged: boolean,
|
||||||
fatalError: boolean,
|
fatalError: boolean,
|
||||||
};
|
};
|
||||||
|
|
|
@ -451,6 +451,7 @@ export const SYNC_APPLY_BAD_PASSWORD = 'SYNC_APPLY_BAD_PASSWORD';
|
||||||
export const SYNC_RESET = 'SYNC_RESET';
|
export const SYNC_RESET = 'SYNC_RESET';
|
||||||
export const SYNC_FATAL_ERROR = 'SYNC_FATAL_ERROR';
|
export const SYNC_FATAL_ERROR = 'SYNC_FATAL_ERROR';
|
||||||
export const USER_STATE_POPULATE = 'USER_STATE_POPULATE';
|
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_STARTED = 'REACTIONS_LIST_STARTED';
|
||||||
export const REACTIONS_LIST_FAILED = 'REACTIONS_LIST_FAILED';
|
export const REACTIONS_LIST_FAILED = 'REACTIONS_LIST_FAILED';
|
||||||
|
|
|
@ -42,7 +42,7 @@ import {
|
||||||
} from 'redux/selectors/app';
|
} from 'redux/selectors/app';
|
||||||
import { selectDaemonSettings, selectClientSetting } from 'redux/selectors/settings';
|
import { selectDaemonSettings, selectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
|
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 { doAuthenticate } from 'redux/actions/user';
|
||||||
import { lbrySettings as config, version as appVersion } from 'package.json';
|
import { lbrySettings as config, version as appVersion } from 'package.json';
|
||||||
import analytics, { SHARE_INTERNAL } from 'analytics';
|
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;
|
const { SDK_SYNC_KEYS } = SHARED_PREFERENCES;
|
||||||
|
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
|
@ -615,7 +615,10 @@ export function doGetAndPopulatePreferences() {
|
||||||
const successState = getState();
|
const successState = getState();
|
||||||
const daemonSettings = selectDaemonSettings(successState);
|
const daemonSettings = selectDaemonSettings(successState);
|
||||||
if (savedPreferences !== null) {
|
if (savedPreferences !== null) {
|
||||||
|
if (!syncInvalidated(getState, syncId)) {
|
||||||
dispatch(doPopulateSharedUserState(savedPreferences));
|
dispatch(doPopulateSharedUserState(savedPreferences));
|
||||||
|
}
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
|
|
||||||
const { settings } = savedPreferences.value;
|
const { settings } = savedPreferences.value;
|
||||||
|
@ -660,11 +663,15 @@ export function doGetAndPopulatePreferences() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doHandleSyncComplete(error, hasNewData) {
|
export function doHandleSyncComplete(error, hasNewData, syncId) {
|
||||||
return (dispatch) => {
|
return (dispatch, getState) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
if (hasNewData) {
|
if (hasNewData) {
|
||||||
dispatch(doGetAndPopulatePreferences());
|
if (syncInvalidated(getState, syncId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(doGetAndPopulatePreferences(syncId));
|
||||||
// we just got sync data, better update our channels
|
// we just got sync data, better update our channels
|
||||||
dispatch(doFetchChannelListMine());
|
dispatch(doFetchChannelListMine());
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,19 @@ const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes
|
||||||
const NO_WALLET_ERROR = 'no wallet found for this user';
|
const NO_WALLET_ERROR = 'no wallet found for this user';
|
||||||
const BAD_PASSWORD_ERROR_NAME = 'InvalidPasswordError';
|
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) {
|
export function doSetDefaultAccount(success: () => void, failure: (string) => void) {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: 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) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
if (!noInterval && syncTimer) clearInterval(syncTimer);
|
if (!noInterval && syncTimer) clearInterval(syncTimer);
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -123,7 +143,7 @@ export function doSyncLoop(noInterval?: boolean) {
|
||||||
const syncEnabled = selectClientSetting(state, SETTINGS.ENABLE_SYNC);
|
const syncEnabled = selectClientSetting(state, SETTINGS.ENABLE_SYNC);
|
||||||
const syncLocked = selectSyncIsLocked(state);
|
const syncLocked = selectSyncIsLocked(state);
|
||||||
if (hasVerifiedEmail && syncEnabled && !syncLocked) {
|
if (hasVerifiedEmail && syncEnabled && !syncLocked) {
|
||||||
dispatch(doGetSyncDesktop((error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData))));
|
dispatch(doGetSyncDesktop((error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData, syncId))));
|
||||||
if (!noInterval) {
|
if (!noInterval) {
|
||||||
syncTimer = setInterval(() => {
|
syncTimer = setInterval(() => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import isEqual from 'util/deep-equal';
|
import isEqual from 'util/deep-equal';
|
||||||
import { doPreferenceSet } from 'redux/actions/sync';
|
import { doPreferenceSet } from 'redux/actions/sync';
|
||||||
|
|
||||||
|
@ -47,10 +48,12 @@ export const buildSharedStateMiddleware = (
|
||||||
|
|
||||||
if (sharedStateCb) {
|
if (sharedStateCb) {
|
||||||
// Pass dispatch to the callback to consumers can dispatch actions in response to preference set
|
// 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);
|
clearTimeout(timeout);
|
||||||
return actionResult;
|
return actionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout = setTimeout(runPreferences, RUN_PREFERENCES_DELAY_MS);
|
timeout = setTimeout(runPreferences, RUN_PREFERENCES_DELAY_MS);
|
||||||
|
dispatch({ type: ACTIONS.SHARED_STATE_SYNC_ID_CHANGED, data: timeout });
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ const defaultState: SyncState = {
|
||||||
setSyncIsPending: false,
|
setSyncIsPending: false,
|
||||||
prefsReady: false,
|
prefsReady: false,
|
||||||
syncLocked: false,
|
syncLocked: false,
|
||||||
|
sharedStateSyncId: -1,
|
||||||
hashChanged: false,
|
hashChanged: false,
|
||||||
fatalError: false,
|
fatalError: false,
|
||||||
};
|
};
|
||||||
|
@ -114,6 +115,10 @@ reducers[ACTIONS.SYNC_FATAL_ERROR] = (state: SyncState) => {
|
||||||
|
|
||||||
reducers[ACTIONS.SYNC_RESET] = () => defaultState;
|
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) {
|
export default function syncReducer(state: SyncState = defaultState, action: any) {
|
||||||
const handler = reducers[action.type];
|
const handler = reducers[action.type];
|
||||||
if (handler) return handler(state, action);
|
if (handler) return handler(state, action);
|
||||||
|
|
|
@ -185,8 +185,8 @@ const sharedStateFilters = {
|
||||||
unpublishedCollections: { source: 'collections', property: 'unpublished' },
|
unpublishedCollections: { source: 'collections', property: 'unpublished' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const sharedStateCb = ({ dispatch, getState }) => {
|
const sharedStateCb = ({ dispatch, getState, syncId }) => {
|
||||||
dispatch(doSyncLoop());
|
dispatch(doSyncLoop(false, syncId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const populateAuthTokenHeader = () => {
|
const populateAuthTokenHeader = () => {
|
||||||
|
|
Loading…
Reference in a new issue