Revert "[MM-23490] Save state to file via async middleware vs store subscription (#4059)"

This reverts commit 1d7149a26d.
This commit is contained in:
Amit Uttam 2020-04-17 20:59:46 -03:00
parent 1d7149a26d
commit 844d81072f
No known key found for this signature in database
GPG key ID: B3D4CC224111BC5D
5 changed files with 247 additions and 282 deletions

View file

@ -143,6 +143,7 @@ const state = {
root: {
deepLinkURL: '',
hydrationComplete: false,
purge: false,
},
selectServer: {
serverUrl: Config.DefaultServerUrl,

View file

@ -26,7 +26,17 @@ function hydrationComplete(state = false, action) {
}
}
function purge(state = false, action) {
switch (action.type) {
case General.OFFLINE_STORE_PURGE:
return true;
default:
return state;
}
}
export default combineReducers({
deepLinkURL,
hydrationComplete,
purge,
});

View file

@ -1,23 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {batchActions} from 'redux-batched-actions';
import AsyncStorage from '@react-native-community/async-storage';
import {purgeStoredState} from 'redux-persist';
import {NavigationTypes, ViewTypes} from 'app/constants';
import {ViewTypes} from 'app/constants';
import initialState from 'app/initial_state';
import {throttle} from 'app/utils/general';
import {General} from '@mm-redux/constants';
import {ErrorTypes, GeneralTypes} from '@mm-redux/action_types';
import EventEmitter from '@mm-redux/utils/event_emitter';
import mattermostBucket from 'app/mattermost_bucket';
import {getStateForReset} from './utils';
import EphemeralStore from './ephemeral_store';
@ -26,124 +13,7 @@ import {
LOGGER_JAVASCRIPT_WARNING,
} from 'app/utils/sentry';
const SAVE_STATE_ACTIONS = [
'CONNECTION_CHANGED',
'DATA_CLEANUP',
'LOGIN',
'Offline/STATUS_CHANGED',
'persist/REHYDRATE',
'RECEIVED_APP_STATE',
'WEBSOCKET_CLOSED',
'WEBSOCKET_SUCCESS',
];
// This middleware stores key parts of state entities into a file (in the App Group container) on certain actions.
// iOS only. Allows the share extension to work, without having access available to the redux store object.
// Remove this middleware if/when state is moved to a persisted solution.
const saveShareExtensionState = (store) => {
return (next) => (action) => {
if (SAVE_STATE_ACTIONS.includes(action.type)) {
throttle(saveStateToFile(store));
}
return next(action);
};
};
const saveStateToFile = async (store) => {
if (Platform.OS === 'ios') {
const state = store.getState();
if (state.entities) {
const channelsInTeam = {...state.entities.channels.channelsInTeam};
Object.keys(channelsInTeam).forEach((teamId) => {
channelsInTeam[teamId] = Array.from(channelsInTeam[teamId]);
});
const profilesInChannel = {...state.entities.users.profilesInChannel};
Object.keys(profilesInChannel).forEach((channelId) => {
profilesInChannel[channelId] = Array.from(profilesInChannel[channelId]);
});
let url;
if (state.entities.users.currentUserId) {
url = state.entities.general.credentials.url || state.views.selectServer.serverUrl;
}
const entities = {
...state.entities,
general: {
...state.entities.general,
credentials: {
url,
},
},
channels: {
...state.entities.channels,
channelsInTeam,
},
users: {
...state.entities.users,
profilesInChannel,
profilesNotInTeam: [],
profilesWithoutTeam: [],
profilesNotInChannel: [],
},
};
mattermostBucket.writeToFile('entities', JSON.stringify(entities));
}
}
};
const purgeAppCacheWrapper = (persistConfig) => (store) => {
return (next) => (action) => {
if (action.type === General.OFFLINE_STORE_PURGE) {
purgeStoredState({...persistConfig, storage: AsyncStorage});
const state = store.getState();
const resetState = getStateForReset(initialState, state);
store.dispatch(batchActions([
{
type: General.OFFLINE_STORE_RESET,
data: resetState,
},
{
type: ErrorTypes.RESTORE_ERRORS,
data: [...state.errors],
},
{
type: GeneralTypes.RECEIVED_APP_DEVICE_TOKEN,
data: state.entities.general.deviceToken,
},
{
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {
url: state.entities.general.credentials.url,
},
},
{
type: ViewTypes.SERVER_URL_CHANGED,
serverUrl: state.entities.general.credentials.url || state.views.selectServer.serverUrl,
},
{
type: GeneralTypes.RECEIVED_SERVER_VERSION,
data: state.entities.general.serverVersion,
},
{
type: General.STORE_REHYDRATION_COMPLETE,
},
], 'BATCH_FOR_RESTART'));
setTimeout(() => {
EventEmitter.emit(NavigationTypes.RESTART_APP);
}, 500);
}
return next(action);
};
};
const messageRetention = (store) => {
export function messageRetention(store) {
return (next) => (action) => {
if (action.type === 'persist/REHYDRATE') {
const {app} = action.payload;
@ -188,7 +58,7 @@ const messageRetention = (store) => {
return next(action);
};
};
}
function resetStateForNewVersion(action) {
const {payload} = action;
@ -564,17 +434,3 @@ function removePendingPost(pendingPostIds, id) {
pendingPostIds.splice(pendingIndex, 1);
}
}
export const middlewares = (persistConfig) => {
const middlewareFunctions = [
messageRetention,
purgeAppCacheWrapper(persistConfig),
];
if (Platform.OS === 'ios') {
middlewareFunctions.push(saveShareExtensionState);
}
return middlewareFunctions;
};

View file

@ -10,12 +10,10 @@ import {
cleanUpPostsInChannel,
cleanUpState,
getAllFromPostsInChannel,
middlewares,
messageRetention,
} from 'app/store/middleware';
describe('messageRetention', () => {
const messageRetention = middlewares()[0];
describe('should chain the same incoming action type', () => {
const actions = [
{

View file

@ -1,21 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {Platform} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import {createBlacklistFilter} from 'redux-persist-transform-filter';
import {createTransform, persistStore} from 'redux-persist';
import {General} from '@mm-redux/constants';
import {getConfig} from '@mm-redux/selectors/entities/general';
import configureStore from '@mm-redux/store';
import {ErrorTypes, GeneralTypes} from 'mattermost-redux/action_types';
import {General} from 'mattermost-redux/constants';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import configureStore from 'mattermost-redux/store';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {NavigationTypes, ViewTypes} from 'app/constants';
import appReducer from 'app/reducers';
import {throttle} from 'app/utils/general';
import {getSiteUrl, setSiteUrl} from 'app/utils/image_cache_manager';
import {createSentryMiddleware} from 'app/utils/sentry/middleware';
import {middlewares} from './middleware';
import mattermostBucket from 'app/mattermost_bucket';
import {messageRetention} from './middleware';
import {createThunkMiddleware} from './thunk';
import {transformSet} from './utils';
import {transformSet, getStateForReset} from './utils';
function getAppReducer() {
return require('../../app/reducers'); // eslint-disable-line global-require
@ -42,140 +50,232 @@ const setTransforms = [
...rolesSetTransform,
];
const viewsBlackListFilter = createBlacklistFilter(
'views',
['extension', 'root'],
);
const typingBlackListFilter = createBlacklistFilter(
'entities',
['typing'],
);
const channelViewBlackList = {loading: true, refreshing: true, loadingPosts: true, retryFailed: true, loadMorePostsVisible: true};
const channelViewBlackListFilter = createTransform(
(inboundState) => {
const channel = {};
for (const channelKey of Object.keys(inboundState.channel)) {
if (!channelViewBlackList[channelKey]) {
channel[channelKey] = inboundState.channel[channelKey];
}
}
return {
...inboundState,
channel,
};
},
null,
{whitelist: ['views']}, // Only run this filter on the views state (or any other entry that ends up being named views)
);
const emojiBlackList = {nonExistentEmoji: true};
const emojiBlackListFilter = createTransform(
(inboundState) => {
const emojis = {};
for (const emojiKey of Object.keys(inboundState.emojis)) {
if (!emojiBlackList[emojiKey]) {
emojis[emojiKey] = inboundState.emojis[emojiKey];
}
}
return {
...inboundState,
emojis,
};
},
null,
{whitelist: ['entities']}, // Only run this filter on the entities state (or any other entry that ends up being named entities)
);
const setTransformer = createTransform(
(inboundState, key) => {
if (key === 'entities') {
const state = {...inboundState};
for (const prop in state) {
if (state.hasOwnProperty(prop)) {
state[prop] = transformSet(state[prop], setTransforms);
}
}
return state;
}
return inboundState;
},
(outboundState, key) => {
if (key === 'entities') {
const state = {...outboundState};
for (const prop in state) {
if (state.hasOwnProperty(prop)) {
state[prop] = transformSet(state[prop], setTransforms, false);
}
}
return state;
}
return outboundState;
},
);
const persistConfig = {
effect: (effect, action) => {
if (typeof effect !== 'function') {
throw new Error('Offline Action: effect must be a function.');
} else if (!action.meta.offline.commit) {
throw new Error('Offline Action: commit action must be present.');
}
return effect();
},
persist: (store, options) => {
const persistor = persistStore(store, {storage: AsyncStorage, ...options}, () => {
store.dispatch({
type: General.STORE_REHYDRATION_COMPLETE,
});
});
store.subscribe(async () => {
const state = store.getState();
const config = getConfig(state);
if (getSiteUrl() !== config?.SiteURL) {
setSiteUrl(config.SiteURL);
}
});
return persistor;
},
persistOptions: {
autoRehydrate: {
log: false,
},
blacklist: ['device', 'navigation', 'offline', 'requests'],
debounce: 500,
transforms: [
setTransformer,
viewsBlackListFilter,
typingBlackListFilter,
channelViewBlackListFilter,
emojiBlackListFilter,
],
},
};
export default function configureAppStore(initialState) {
const viewsBlackListFilter = createBlacklistFilter(
'views',
['extension', 'login', 'root'],
);
const typingBlackListFilter = createBlacklistFilter(
'entities',
['typing'],
);
const channelViewBlackList = {loading: true, refreshing: true, loadingPosts: true, retryFailed: true, loadMorePostsVisible: true};
const channelViewBlackListFilter = createTransform(
(inboundState) => {
const channel = {};
for (const channelKey of Object.keys(inboundState.channel)) {
if (!channelViewBlackList[channelKey]) {
channel[channelKey] = inboundState.channel[channelKey];
}
}
return {
...inboundState,
channel,
};
},
null,
{whitelist: ['views']}, // Only run this filter on the views state (or any other entry that ends up being named views)
);
const emojiBlackList = {nonExistentEmoji: true};
const emojiBlackListFilter = createTransform(
(inboundState) => {
const emojis = {};
for (const emojiKey of Object.keys(inboundState.emojis)) {
if (!emojiBlackList[emojiKey]) {
emojis[emojiKey] = inboundState.emojis[emojiKey];
}
}
return {
...inboundState,
emojis,
};
},
null,
{whitelist: ['entities']}, // Only run this filter on the entities state (or any other entry that ends up being named entities)
);
const setTransformer = createTransform(
(inboundState, key) => {
if (key === 'entities') {
const state = {...inboundState};
for (const prop in state) {
if (state.hasOwnProperty(prop)) {
state[prop] = transformSet(state[prop], setTransforms);
}
}
return state;
}
return inboundState;
},
(outboundState, key) => {
if (key === 'entities') {
const state = {...outboundState};
for (const prop in state) {
if (state.hasOwnProperty(prop)) {
state[prop] = transformSet(state[prop], setTransforms, false);
}
}
return state;
}
return outboundState;
},
);
const offlineOptions = {
effect: (effect, action) => {
if (typeof effect !== 'function') {
throw new Error('Offline Action: effect must be a function.');
} else if (!action.meta.offline.commit) {
throw new Error('Offline Action: commit action must be present.');
}
return effect();
},
persist: (store, options) => {
const persistor = persistStore(store, {storage: AsyncStorage, ...options}, () => {
store.dispatch({
type: General.STORE_REHYDRATION_COMPLETE,
});
});
let purging = false;
// for iOS write the entities to a shared file
if (Platform.OS === 'ios') {
store.subscribe(throttle(() => {
const state = store.getState();
if (state.entities) {
const channelsInTeam = {...state.entities.channels.channelsInTeam};
Object.keys(channelsInTeam).forEach((teamId) => {
channelsInTeam[teamId] = Array.from(channelsInTeam[teamId]);
});
const profilesInChannel = {...state.entities.users.profilesInChannel};
Object.keys(profilesInChannel).forEach((channelId) => {
profilesInChannel[channelId] = Array.from(profilesInChannel[channelId]);
});
let url;
if (state.entities.users.currentUserId) {
url = state.entities.general.credentials.url || state.views.selectServer.serverUrl;
}
const entities = {
...state.entities,
general: {
...state.entities.general,
credentials: {
url,
},
},
channels: {
...state.entities.channels,
channelsInTeam,
},
users: {
...state.entities.users,
profilesInChannel,
profilesNotInTeam: [],
profilesWithoutTeam: [],
profilesNotInChannel: [],
},
};
mattermostBucket.writeToFile('entities', JSON.stringify(entities));
}
}, 1000));
}
// check to see if the logout request was successful
store.subscribe(async () => {
const state = store.getState();
const config = getConfig(state);
if (getSiteUrl() !== config?.SiteURL) {
setSiteUrl(config.SiteURL);
}
if (state.views.root.purge && !purging) {
purging = true;
await persistor.purge();
const resetState = getStateForReset(initialState, state);
store.dispatch(batchActions([
{
type: General.OFFLINE_STORE_RESET,
data: resetState,
},
{
type: ErrorTypes.RESTORE_ERRORS,
data: [...state.errors],
},
{
type: GeneralTypes.RECEIVED_APP_DEVICE_TOKEN,
data: state.entities.general.deviceToken,
},
{
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {
url: state.entities.general.credentials.url,
},
},
{
type: ViewTypes.SERVER_URL_CHANGED,
serverUrl: state.entities.general.credentials.url || state.views.selectServer.serverUrl,
},
{
type: GeneralTypes.RECEIVED_SERVER_VERSION,
data: state.entities.general.serverVersion,
},
{
type: General.STORE_REHYDRATION_COMPLETE,
},
], 'BATCH_FOR_RESTART'));
setTimeout(() => {
purging = false;
EventEmitter.emit(NavigationTypes.RESTART_APP);
}, 500);
}
});
return persistor;
},
persistOptions: {
autoRehydrate: {
log: false,
},
blacklist: ['device', 'navigation', 'offline', 'requests'],
transforms: [
setTransformer,
viewsBlackListFilter,
typingBlackListFilter,
channelViewBlackListFilter,
emojiBlackListFilter,
],
},
};
const clientOptions = {
additionalMiddleware: [
createThunkMiddleware(),
createSentryMiddleware(),
...middlewares(persistConfig),
messageRetention,
],
enableThunk: false, // We override the default thunk middleware
};
return configureStore(initialState, appReducer, persistConfig, getAppReducer, clientOptions);
return configureStore(initialState, appReducer, offlineOptions, getAppReducer, clientOptions);
}