lbry-desktop/ui/store.js
infinite-persistence d7e3127e65 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.
2021-12-29 10:32:38 -05:00

233 lines
7.1 KiB
JavaScript

import * as ACTIONS from 'constants/action_types';
import { persistStore, persistReducer } from 'redux-persist';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
import createCompressor from 'redux-persist-transform-compress';
import { createFilter, createBlacklistFilter } from 'redux-persist-transform-filter';
import localForage from 'localforage';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { createMemoryHistory, createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import createRootReducer from './reducers';
import Lbry from 'lbry';
import { createAnalyticsMiddleware } from 'redux/middleware/analytics';
import { buildSharedStateMiddleware } from 'redux/middleware/shared-state';
import { doSyncLoop } from 'redux/actions/sync';
import { getAuthToken } from 'util/saved-passwords';
import { generateInitialUrl } from 'util/url';
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
function isFunction(object) {
return typeof object === 'function';
}
function isNotFunction(object) {
return !isFunction(object);
}
function createBulkThunkMiddleware() {
return ({ dispatch, getState }) => (next) => (action) => {
if (action.type === 'BATCH_ACTIONS') {
action.actions.filter(isFunction).map((actionFn) => actionFn(dispatch, getState));
}
return next(action);
};
}
function enableBatching(reducer) {
return function batchingReducer(state, action) {
switch (action.type) {
case 'BATCH_ACTIONS':
return action.actions.filter(isNotFunction).reduce(batchingReducer, state);
default:
return reducer(state, action);
}
};
}
const contentFilter = createFilter('content', ['positions', 'history']);
const fileInfoFilter = createFilter('fileInfo', [
'fileListPublishedSort',
'fileListDownloadedSort',
'fileListSubscriptionSort',
]);
const appFilter = createFilter('app', [
'hasClickedComment',
'searchOptionsExpanded',
'volume',
'muted',
'allowAnalytics',
'welcomeVersion',
'interestedInYoutubeSync',
'splashAnimationEnabled',
'activeChannel',
]);
const claimsFilter = createFilter('claims', ['pendingById']);
// We only need to persist the receiveAddress for the wallet
const walletFilter = createFilter('wallet', ['receiveAddress']);
const searchFilter = createFilter('search', ['options']);
const tagsFilter = createFilter('tags', ['followedTags']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
const blockedFilter = createFilter('blocked', ['blockedChannels']);
const coinSwapsFilter = createFilter('coinSwap', ['coinSwaps']);
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
const collectionsFilter = createFilter('collections', ['builtin', 'saved', 'unpublished', 'edited', 'pending']);
const whiteListedReducers = [
'claims',
'fileInfo',
'publish',
'wallet',
'tags',
'content',
'app',
'search',
'blocked',
'coinSwap',
'settings',
'subscriptions',
'collections',
];
const transforms = [
claimsFilter,
fileInfoFilter,
walletFilter,
blockedFilter,
coinSwapsFilter,
tagsFilter,
appFilter,
searchFilter,
tagsFilter,
contentFilter,
subscriptionsFilter,
settingsFilter,
collectionsFilter,
createCompressor(),
];
const persistOptions = {
key: 'v0',
storage: localForage,
stateReconciler: autoMergeLevel2,
whitelist: whiteListedReducers,
// Order is important. Needs to be compressed last or other transforms can't
// read the data
transforms,
};
let history;
// @if TARGET='app'
history = createMemoryHistory({
initialEntries: [generateInitialUrl(window.location.hash)],
initialIndex: 0,
});
// @endif
// @if TARGET='web'
history = createBrowserHistory();
// @endif
const triggerSharedStateActions = [
ACTIONS.CHANNEL_SUBSCRIBE,
ACTIONS.CHANNEL_UNSUBSCRIBE,
ACTIONS.TOGGLE_BLOCK_CHANNEL,
ACTIONS.ADD_COIN_SWAP,
ACTIONS.REMOVE_COIN_SWAP,
ACTIONS.TOGGLE_TAG_FOLLOW,
ACTIONS.CREATE_CHANNEL_COMPLETED,
ACTIONS.SYNC_CLIENT_SETTINGS,
// Disabled until we can overwrite preferences
ACTIONS.SHARED_PREFERENCE_SET,
ACTIONS.COLLECTION_EDIT,
ACTIONS.COLLECTION_DELETE,
ACTIONS.COLLECTION_NEW,
ACTIONS.COLLECTION_PENDING,
// MAYBE COLLECTOIN SAVE
// ACTIONS.SET_WELCOME_VERSION,
// ACTIONS.SET_ALLOW_ANALYTICS,
];
/**
* source: the reducer name
* property: the property in the reducer-specific state
* transform: optional method to modify the value to be stored
*
* See https://github.com/lbryio/lbry-redux/blob/master/src/redux/middleware/shared-state.js for the source
* This is based off v0.1
* If lbry-redux changes to another version, this code will need to be changed when upgrading
*/
const sharedStateFilters = {
tags: { source: 'tags', property: 'followedTags' },
subscriptions: {
source: 'subscriptions',
property: 'subscriptions',
transform: (value) => {
return value.map(({ uri }) => uri);
},
},
following: {
source: 'subscriptions',
property: 'following',
},
blocked: { source: 'blocked', property: 'blockedChannels' },
coin_swap_codes: {
source: 'coinSwap',
property: 'coinSwaps',
transform: (coinSwaps) => {
return coinSwaps.map((coinSwapInfo) => coinSwapInfo.chargeCode);
},
},
settings: { source: 'settings', property: 'sharedPreferences' },
app_welcome_version: { source: 'app', property: 'welcomeVersion' },
sharing_3P: { source: 'app', property: 'allowAnalytics' },
builtinCollections: { source: 'collections', property: 'builtin' },
editedCollections: { source: 'collections', property: 'edited' },
// savedCollections: { source: 'collections', property: 'saved' },
unpublishedCollections: { source: 'collections', property: 'unpublished' },
};
const sharedStateCb = ({ dispatch, getState, syncId }) => {
dispatch(doSyncLoop(false, syncId));
};
const populateAuthTokenHeader = () => {
return (next) => (action) => {
if (
(action.type === ACTIONS.USER_FETCH_SUCCESS || action.type === ACTIONS.AUTHENTICATION_SUCCESS) &&
action.data.user.has_verified_email === true
) {
const authToken = getAuthToken();
Lbry.setApiHeader(X_LBRY_AUTH_TOKEN, authToken);
}
return next(action);
};
};
const sharedStateMiddleware = buildSharedStateMiddleware(triggerSharedStateActions, sharedStateFilters, sharedStateCb);
const rootReducer = createRootReducer(history);
const persistedReducer = persistReducer(persistOptions, rootReducer);
const bulkThunk = createBulkThunkMiddleware();
const analyticsMiddleware = createAnalyticsMiddleware();
const middleware = [
sharedStateMiddleware,
// @if TARGET='web'
populateAuthTokenHeader,
// @endif
routerMiddleware(history),
thunk,
bulkThunk,
analyticsMiddleware,
];
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
enableBatching(persistedReducer),
{}, // initial state
composeEnhancers(applyMiddleware(...middleware))
);
const persistor = persistStore(store);
window.persistor = persistor;
export { store, persistor, history, whiteListedReducers };