Jounal store: simplify the store functions and change the list to a hash

It's always been a massive mess, this improves it.
master
Tom Hacohen 6 years ago
parent 5b79e0f107
commit c5fc6f23f5

@ -10,7 +10,7 @@ import AppBarOverride from '../widgets/AppBarOverride';
import { routeResolver } from '../App'; import { routeResolver } from '../App';
import { store, JournalsData, UserInfoData, CredentialsData } from '../store'; import { store, JournalsData, UserInfoData, CredentialsData } from '../store';
import { createJournal, updateJournal } from '../store/actions'; import { addJournal, updateJournal } from '../store/actions';
import { SyncInfo } from '../SyncGate'; import { SyncInfo } from '../SyncGate';
import * as EteSync from '../api/EteSync'; import * as EteSync from '../api/EteSync';
@ -102,7 +102,7 @@ class Journals extends React.PureComponent {
this.props.history.goBack() this.props.history.goBack()
); );
} else { } else {
store.dispatch<any>(createJournal(this.props.etesync, journal)).then(() => store.dispatch<any>(addJournal(this.props.etesync, journal)).then(() =>
this.props.history.goBack() this.props.history.goBack()
); );
} }

@ -19,7 +19,7 @@ import * as EteSync from './api/EteSync';
import { CURRENT_VERSION } from './api/Constants'; import { CURRENT_VERSION } from './api/Constants';
import { store, JournalsType, EntriesType, StoreState, CredentialsData, UserInfoType } from './store'; import { store, JournalsType, EntriesType, StoreState, CredentialsData, UserInfoType } from './store';
import { createJournal, fetchAll, fetchEntries, fetchUserInfo, createUserInfo } from './store/actions'; import { addJournal, fetchAll, fetchEntries, fetchUserInfo, createUserInfo } from './store/actions';
export interface SyncInfoJournal { export interface SyncInfoJournal {
journal: EteSync.Journal; journal: EteSync.Journal;
@ -43,7 +43,7 @@ type PropsTypeInner = RouteComponentProps<{}> & PropsType & {
const syncInfoSelector = createSelector( const syncInfoSelector = createSelector(
(props: PropsTypeInner) => props.etesync, (props: PropsTypeInner) => props.etesync,
(props: PropsTypeInner) => props.journals.value as List<EteSync.Journal>, (props: PropsTypeInner) => props.journals.value!,
(props: PropsTypeInner) => props.entries, (props: PropsTypeInner) => props.entries,
(props: PropsTypeInner) => props.userInfo.value!, (props: PropsTypeInner) => props.userInfo.value!,
(etesync, journals, entries, userInfo) => { (etesync, journals, entries, userInfo) => {
@ -115,7 +115,7 @@ class SyncGate extends React.PureComponent<PropsTypeInner> {
const journal = new EteSync.Journal(); const journal = new EteSync.Journal();
const cryptoManager = new EteSync.CryptoManager(this.props.etesync.encryptionKey, collection.uid); const cryptoManager = new EteSync.CryptoManager(this.props.etesync.encryptionKey, collection.uid);
journal.setInfo(cryptoManager, collection); journal.setInfo(cryptoManager, collection);
store.dispatch<any>(createJournal(this.props.etesync, journal)).then( store.dispatch<any>(addJournal(this.props.etesync, journal)).then(
(journalAction: Action<EteSync.Journal>) => { (journalAction: Action<EteSync.Journal>) => {
// FIXME: Limit based on error code to only do it for associates. // FIXME: Limit based on error code to only do it for associates.
if (!journalAction.error) { if (!journalAction.error) {

@ -3,7 +3,7 @@ import { createAction, createActions } from 'redux-actions';
import * as EteSync from '../api/EteSync'; import * as EteSync from '../api/EteSync';
import { UserInfo } from '../api/EteSync'; import { UserInfo } from '../api/EteSync';
import { CredentialsData, EntriesType } from './'; import { CredentialsData, EntriesType, JournalsData } from './';
export const { fetchCredentials, logout } = createActions({ export const { fetchCredentials, logout } = createActions({
FETCH_CREDENTIALS: (username: string, password: string, server: string) => { FETCH_CREDENTIALS: (username: string, password: string, server: string) => {
@ -44,8 +44,8 @@ export const login = (username: string, password: string, encryptionPassword: st
}; };
}; };
export const { fetchJournals } = createActions({ export const { fetchListJournal } = createActions({
FETCH_JOURNALS: (etesync: CredentialsData) => { FETCH_LIST_JOURNAL: (etesync: CredentialsData) => {
const creds = etesync.credentials; const creds = etesync.credentials;
const apiBase = etesync.serviceApiUrl; const apiBase = etesync.serviceApiUrl;
let journalManager = new EteSync.JournalManager(creds, apiBase); let journalManager = new EteSync.JournalManager(creds, apiBase);
@ -54,8 +54,8 @@ export const { fetchJournals } = createActions({
}, },
}); });
export const createJournal = createAction( export const addJournal = createAction(
'CREATE_JOURNAL', 'ADD_JOURNAL',
(etesync: CredentialsData, journal: EteSync.Journal) => { (etesync: CredentialsData, journal: EteSync.Journal) => {
const creds = etesync.credentials; const creds = etesync.credentials;
const apiBase = etesync.serviceApiUrl; const apiBase = etesync.serviceApiUrl;
@ -64,7 +64,7 @@ export const createJournal = createAction(
return journalManager.create(journal); return journalManager.create(journal);
}, },
(etesync: CredentialsData, journal: EteSync.Journal) => { (etesync: CredentialsData, journal: EteSync.Journal) => {
return { journal }; return { item: journal };
}, },
); );
@ -78,7 +78,21 @@ export const updateJournal = createAction(
return journalManager.update(journal); return journalManager.update(journal);
}, },
(etesync: CredentialsData, journal: EteSync.Journal) => { (etesync: CredentialsData, journal: EteSync.Journal) => {
return { journal }; return { item: journal };
},
);
export const deleteJournal = createAction(
'DELETE_JOURNAL',
(etesync: CredentialsData, journal: EteSync.Journal) => {
const creds = etesync.credentials;
const apiBase = etesync.serviceApiUrl;
let journalManager = new EteSync.JournalManager(creds, apiBase);
return journalManager.delete(journal);
},
(etesync: CredentialsData, journal: EteSync.Journal) => {
return { item: journal };
}, },
); );
@ -135,13 +149,13 @@ export const createUserInfo = createAction(
export function fetchAll(etesync: CredentialsData, currentEntries: EntriesType) { export function fetchAll(etesync: CredentialsData, currentEntries: EntriesType) {
return (dispatch: any) => { return (dispatch: any) => {
return dispatch(fetchJournals(etesync)).then((journalsAction: any) => { return dispatch(fetchListJournal(etesync)).then((journalsAction: any) => {
const journals: Array<EteSync.Journal> = journalsAction.payload; const journals: JournalsData = journalsAction.payload;
if (!journals || (journals.length === 0)) { if (!journals || journals.isEmpty) {
return false; return false;
} }
journals.forEach((journal) => { journals.forEach((journal, uid) => {
let prevUid: string | null = null; let prevUid: string | null = null;
const entries = currentEntries.get(journal.uid); const entries = currentEntries.get(journal.uid);
if (entries && entries.value) { if (entries && entries.value) {

@ -1,11 +1,11 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { persistReducer, createTransform } from 'redux-persist'; import { createMigrate, persistReducer, createTransform } from 'redux-persist';
import { handleAction, handleActions, combineActions } from 'redux-actions'; import { Action, ActionFunctionAny, combineActions, handleAction, handleActions } from 'redux-actions';
import * as localforage from 'localforage'; import * as localforage from 'localforage';
import session from 'redux-persist/lib/storage/session'; import session from 'redux-persist/lib/storage/session';
import { List, Map, Record } from 'immutable'; import { List, Map as ImmutableMap, Record } from 'immutable';
import * as EteSync from '../api/EteSync'; import * as EteSync from '../api/EteSync';
@ -35,11 +35,14 @@ function fetchTypeRecord<T>() {
}); });
} }
interface BaseModel {
uid: string;
}
export type CredentialsType = FetchType<CredentialsData>; export type CredentialsType = FetchType<CredentialsData>;
export type CredentialsTypeRemote = FetchType<CredentialsDataRemote>; export type CredentialsTypeRemote = FetchType<CredentialsDataRemote>;
export type JournalsData = List<EteSync.Journal>; export type JournalsData = ImmutableMap<string, EteSync.Journal>;
const JournalsFetchRecord = fetchTypeRecord<JournalsData>(); const JournalsFetchRecord = fetchTypeRecord<JournalsData>();
export type JournalsType = FetchType<JournalsData>; export type JournalsType = FetchType<JournalsData>;
export type JournalsTypeImmutable = Record<JournalsType>; export type JournalsTypeImmutable = Record<JournalsType>;
@ -48,8 +51,8 @@ export type EntriesData = List<EteSync.Entry>;
const EntriesFetchRecord = fetchTypeRecord<EntriesData>(); const EntriesFetchRecord = fetchTypeRecord<EntriesData>();
export type EntriesTypeImmutable = Map<string, Record<FetchType<EntriesData>>>; export type EntriesTypeImmutable = ImmutableMap<string, Record<FetchType<EntriesData>>>;
export type EntriesType = Map<string, FetchType<EntriesData>>; export type EntriesType = ImmutableMap<string, FetchType<EntriesData>>;
export type UserInfoData = EteSync.UserInfo; export type UserInfoData = EteSync.UserInfo;
@ -127,103 +130,91 @@ const credentials = handleActions(
{value: null} {value: null}
); );
export const entries = handleAction( const setMapModelReducer = <T extends Record<any>, V extends BaseModel>(state: T, action: any) => {
combineActions(actions.fetchEntries, actions.createEntries),
(state: EntriesTypeImmutable, action: any) => {
const prevState = state.get(action.meta.journal);
const extend = action.meta.prevUid != null;
return state.set(action.meta.journal,
fetchTypeIdentityReducer(prevState, action, extend));
},
Map({})
);
const journals = handleActions(
{
[actions.fetchJournals.toString()]: (state: JournalsTypeImmutable, action: any) => {
const newState = fetchTypeIdentityReducer(state, action); const newState = fetchTypeIdentityReducer(state, action);
// Compare the states and see if they are really different // Compare the states and see if they are really different
const oldJournals = state.get('value', null); const newItems = newState.get('value', null);
const newJournals = newState.get('value', null);
if (!oldJournals || !newJournals || (oldJournals.size !== newJournals.size)) { if (!newItems) {
return newState; return newState;
} }
let oldJournalHash = {}; const ret = new Map<string, V>();
oldJournals.forEach((x) => {
oldJournalHash[x.uid] = x.serialize(); newItems.forEach((item: V) => {
ret.set(item.uid, item);
}); });
if (newJournals.every((journal: EteSync.Journal) => ( return newState.set('value', ImmutableMap(ret));
(journal.uid in oldJournalHash) && };
(journal.serialize().content === oldJournalHash[journal.uid].content)
))) { const addEditMapModelReducer = <T extends Record<any>, V extends BaseModel>(state: T, action: any) => {
return state; if (action.error) {
return state.set('error', action.payload);
} else { } else {
return newState; let payload = (action.payload === undefined) ? null : action.payload;
} payload = (action.meta === undefined) ? payload : action.meta.item;
},
[actions.createJournal.toString()]: (state: JournalsTypeImmutable, _action: any) => {
const action = { ..._action };
if (action.payload) {
action.payload = (action.meta === undefined) ? action.payload : action.meta.journal;
action.payload = [ action.payload ];
}
const newState = fetchTypeIdentityReducer(state, action, true); state = state.set('error', undefined);
// Compare the states and see if they are really different
const oldJournals = state.get('value', null);
const newJournals = newState.get('value', null);
if (!oldJournals || !newJournals || (oldJournals.size !== newJournals.size)) { if (action.payload === undefined) {
return newState; return state;
} }
let oldJournalHash = {}; const item = payload as V;
oldJournals.forEach((x) => { let value = state.get('value', null)!;
oldJournalHash[x.uid] = x.serialize(); value = value.set(item.uid, item);
}); return state.set('value', value);
}
};
if (newJournals.every((journal: EteSync.Journal) => ( const deleteMapModelReducer = <T extends Record<any>>(state: T, action: any) => {
(journal.uid in oldJournalHash) && if (action.error) {
(journal.serialize().content === oldJournalHash[journal.uid].content) return state.set('error', action.payload);
))) {
return state;
} else { } else {
return newState; let payload = (action.payload === undefined) ? null : action.payload;
} payload = (action.meta === undefined) ? payload : action.meta.item;
},
[actions.updateJournal.toString()]: (state: JournalsTypeImmutable, _action: any) => {
const action = { ..._action };
if (action.payload) {
action.payload = (action.meta === undefined) ? action.payload : action.meta.journal;
action.payload = [ action.payload ];
}
const newState = fetchTypeIdentityReducer(state, action, true); state = state.set('error', undefined);
// Compare the states and see if they are really different
const oldJournals = state.get('value', null);
const newJournals = newState.get('value', null);
if (!oldJournals || !newJournals || (oldJournals.size !== newJournals.size)) { if (action.payload === undefined) {
return newState; return state;
} }
let oldJournalHash = {}; const id = payload as number;
oldJournals.forEach((x) => { let value = state.get('value', null)!;
oldJournalHash[x.uid] = x.serialize(); value = value.delete(id);
}); return state.set('value', value);
if (newJournals.every((journal: EteSync.Journal) => (
(journal.uid in oldJournalHash) &&
(journal.serialize().content === oldJournalHash[journal.uid].content)
))) {
return state;
} else {
return newState;
} }
};
const mapReducerActionsMapCreator = <T extends Record<any>, V extends BaseModel>(actionName: string) => {
const setsReducer = (state: T, action: any) => setMapModelReducer<T, V>(state, action);
const addEditReducer = (state: T, action: any) => addEditMapModelReducer<T, V>(state, action);
const deleteReducer = (state: T, action: any) => deleteMapModelReducer<T>(state, action);
return {
[actions['fetchList' + actionName].toString() as string]: setsReducer,
[actions['add' + actionName].toString() as string]: addEditReducer,
[actions['update' + actionName].toString() as string]: addEditReducer,
[actions['delete' + actionName].toString() as string]: deleteReducer,
};
};
export const entries = handleAction(
combineActions(actions.fetchEntries, actions.createEntries),
(state: EntriesTypeImmutable, action: any) => {
const prevState = state.get(action.meta.journal);
const extend = action.meta.prevUid != null;
return state.set(action.meta.journal,
fetchTypeIdentityReducer(prevState, action, extend));
}, },
ImmutableMap({})
);
const journals = handleActions(
{
...mapReducerActionsMapCreator<JournalsTypeImmutable, EteSync.Journal>('Journal'),
}, },
new JournalsFetchRecord(), new JournalsFetchRecord(),
); );
@ -253,11 +244,23 @@ const userInfo = handleAction(
new JournalsFetchRecord(), new JournalsFetchRecord(),
); );
const fetchActions = [
] as Array<ActionFunctionAny<Action<any>>>;
for (const func in actions) {
if (func.startsWith('fetchList') ||
func.startsWith('add') ||
func.startsWith('update') ||
func.startsWith('delete')) {
fetchActions.push(actions[func]);
}
}
// Indicates network activity, not just fetch
const fetchCount = handleAction( const fetchCount = handleAction(
combineActions( combineActions(
actions.fetchCredentials, ...fetchActions,
actions.fetchJournals,
actions.fetchEntries
), ),
(state: number, action: any) => { (state: number, action: any) => {
if (action.payload === undefined) { if (action.payload === undefined) {
@ -266,7 +269,7 @@ const fetchCount = handleAction(
return state - 1; return state - 1;
} }
}, },
0 0,
); );
const credentialsPersistConfig = { const credentialsPersistConfig = {
@ -285,19 +288,22 @@ const journalsSerialize = (state: JournalsData) => {
return null; return null;
} }
return state.map((x) => x.serialize()).toJS(); return state.map((x, uid) => x.serialize()).toJS();
}; };
const journalsDeserialize = (state: EteSync.JournalJson[]) => { const journalsDeserialize = (state: {}) => {
if (state === null) { if (state === null) {
return null; return null;
} }
return List(state.map((x: any) => { const newState = new Map<string, EteSync.Journal>();
let ret = new EteSync.Journal(x.version); Object.keys(state).forEach((uid) => {
const x = state[uid];
const ret = new EteSync.Journal(x.version);
ret.deserialize(x); ret.deserialize(x);
return ret; newState.set(uid, ret);
})); });
return ImmutableMap(newState);
}; };
const entriesSerialize = (state: FetchType<EntriesData>) => { const entriesSerialize = (state: FetchType<EntriesData>) => {
@ -360,7 +366,7 @@ const cacheDeserialize = (state: any, key: string) => {
Object.keys(state).forEach((mapKey) => { Object.keys(state).forEach((mapKey) => {
ret[mapKey] = entriesDeserialize(state[mapKey]); ret[mapKey] = entriesDeserialize(state[mapKey]);
}); });
return Map(ret); return ImmutableMap(ret);
} else if (key === 'journals') { } else if (key === 'journals') {
return new JournalsFetchRecord({value: journalsDeserialize(state)}); return new JournalsFetchRecord({value: journalsDeserialize(state)});
} else if (key === 'userInfo') { } else if (key === 'userInfo') {
@ -370,10 +376,21 @@ const cacheDeserialize = (state: any, key: string) => {
return state; return state;
}; };
const cacheMigrations = {
0: (state: any) => {
return {
...state,
journals: undefined
};
},
};
const cachePersistConfig = { const cachePersistConfig = {
key: 'cache', key: 'cache',
version: 1,
storage: localforage, storage: localforage,
transforms: [createTransform(cacheSerialize, cacheDeserialize)], transforms: [createTransform(cacheSerialize, cacheDeserialize)],
migrate: createMigrate(cacheMigrations, { debug: false}),
}; };
const reducers = combineReducers({ const reducers = combineReducers({

Loading…
Cancel
Save