diff --git a/src/Debug.tsx b/src/Debug.tsx index ab599a9..eea4ef8 100644 --- a/src/Debug.tsx +++ b/src/Debug.tsx @@ -16,7 +16,7 @@ export default function Debug(props: PropsType) { const [stateJournalUid, setJournalUid] = React.useState(''); const [entriesUids, setEntriesUids] = React.useState(''); const [result, setResult] = React.useState(''); - const journals = useSelector((state: StoreState) => state.cache.journals.value)!; + const journals = useSelector((state: StoreState) => state.cache.journals!); const journalEntries = useSelector((state: StoreState) => state.cache.entries); function handleInputChange(func: (value: string) => void) { diff --git a/src/SideMenu/index.tsx b/src/SideMenu/index.tsx index deeb080..369edd7 100644 --- a/src/SideMenu/index.tsx +++ b/src/SideMenu/index.tsx @@ -16,7 +16,7 @@ import logo from '../images/logo.svg'; import { routeResolver } from '../App'; -import { store, JournalsType, UserInfoData, StoreState, CredentialsData } from '../store'; +import { store, UserInfoData, StoreState, CredentialsData } from '../store'; import { logout } from '../store/actions'; import * as C from '../constants'; @@ -27,7 +27,6 @@ interface PropsType { } type PropsTypeInner = RouteComponentProps<{}> & PropsType & { - journals: JournalsType; userInfo: UserInfoData; theme: Theme; }; @@ -114,7 +113,6 @@ class SideMenu extends React.PureComponent { const mapStateToProps = (state: StoreState, _props: PropsType) => { return { - journals: state.cache.journals, userInfo: state.cache.userInfo, }; }; diff --git a/src/SyncGate.tsx b/src/SyncGate.tsx index a1efda3..ed8f01e 100644 --- a/src/SyncGate.tsx +++ b/src/SyncGate.tsx @@ -12,7 +12,6 @@ import { routeResolver } from './App'; import AppBarOverride from './widgets/AppBarOverride'; import LoadingIndicator from './widgets/LoadingIndicator'; -import PrettyError from './widgets/PrettyError'; import Journals from './Journals'; import Settings from './Settings'; @@ -22,7 +21,7 @@ import Pim from './Pim'; import * as EteSync from 'etesync'; import { CURRENT_VERSION } from 'etesync'; -import { store, SettingsType, JournalsType, EntriesType, StoreState, CredentialsData, UserInfoData } from './store'; +import { store, SettingsType, JournalsData, EntriesType, StoreState, CredentialsData, UserInfoData } from './store'; import { addJournal, fetchAll, fetchEntries, fetchUserInfo, createUserInfo } from './store/actions'; export interface SyncInfoJournal { @@ -40,7 +39,7 @@ interface PropsType { type PropsTypeInner = RouteComponentProps<{}> & PropsType & { settings: SettingsType; - journals: JournalsType; + journals: JournalsData; entries: EntriesType; userInfo: UserInfoData; fetchCount: number; @@ -48,7 +47,7 @@ type PropsTypeInner = RouteComponentProps<{}> & PropsType & { const syncInfoSelector = createSelector( (props: PropsTypeInner) => props.etesync, - (props: PropsTypeInner) => props.journals.value!, + (props: PropsTypeInner) => props.journals!, (props: PropsTypeInner) => props.entries, (props: PropsTypeInner) => props.userInfo, (etesync, journals, entries, userInfo) => { @@ -153,11 +152,9 @@ class SyncGate extends React.PureComponent { public render() { const entryArrays = this.props.entries; - const journals = this.props.journals.value; + const journals = this.props.journals; - if (this.props.journals.error) { - return ; - } else { + { const errors: Array<{journal: string, error: Error}> = []; this.props.entries.forEach((entry, journal) => { if (entry.error) { diff --git a/src/store/construct.ts b/src/store/construct.ts index e760df7..67848c8 100644 --- a/src/store/construct.ts +++ b/src/store/construct.ts @@ -7,8 +7,8 @@ import { List, Map as ImmutableMap } from 'immutable'; import * as EteSync from 'etesync'; import { - JournalsData, FetchType, EntriesData, EntriesFetchRecord, UserInfoData, JournalsFetchRecord, - CredentialsDataRemote, JournalsType, EntriesType, SettingsType, + JournalsData, FetchType, EntriesData, EntriesFetchRecord, UserInfoData, + CredentialsDataRemote, EntriesType, SettingsType, fetchCount, journals, entries, credentials, userInfo, settingsReducer, encryptionKeyReducer, errorsReducer, } from './reducers'; @@ -18,7 +18,7 @@ export interface StoreState { settings: SettingsType; encryptionKey: {key: string}; cache: { - journals: JournalsType; + journals: JournalsData; entries: EntriesType; userInfo: UserInfoData; }; @@ -49,19 +49,19 @@ const journalsSerialize = (state: JournalsData) => { return state.map((x, _uid) => x.serialize()).toJS(); }; -const journalsDeserialize = (state: {}) => { +const journalsDeserialize = (state: []) => { if (state === null) { return null; } - const newState = new Map(); + const newState = ImmutableMap().asMutable(); Object.keys(state).forEach((uid) => { const x = state[uid]; const ret = new EteSync.Journal({ uid }, x.version); ret.deserialize(x); newState.set(uid, ret); }); - return ImmutableMap(newState); + return newState.asImmutable(); }; const entriesSerialize = (state: FetchType) => { @@ -110,7 +110,7 @@ const cacheSerialize = (state: any, key: string) => { }); return ret; } else if (key === 'journals') { - return journalsSerialize(state.value); + return journalsSerialize(state); } else if (key === 'userInfo') { return userInfoSerialize(state); } @@ -126,7 +126,7 @@ const cacheDeserialize = (state: any, key: string) => { }); return ImmutableMap(ret); } else if (key === 'journals') { - return new JournalsFetchRecord({ value: journalsDeserialize(state) }); + return journalsDeserialize(state); } else if (key === 'userInfo') { return userInfoDeserialize(state); } diff --git a/src/store/reducers.ts b/src/store/reducers.ts index df98b19..3c5a3a0 100644 --- a/src/store/reducers.ts +++ b/src/store/reducers.ts @@ -1,4 +1,4 @@ -import { Action, ActionFunctionAny, combineActions, handleAction, handleActions } from 'redux-actions'; +import { Action, ActionMeta, ActionFunctionAny, combineActions, handleAction, handleActions } from 'redux-actions'; import { shallowEqual } from 'react-redux'; import { List, Map as ImmutableMap, Record } from 'immutable'; @@ -31,14 +31,7 @@ function fetchTypeRecord() { }); } -interface BaseModel { - uid: string; -} - export type JournalsData = ImmutableMap; -export const JournalsFetchRecord = fetchTypeRecord(); -export type JournalsType = FetchType; -export type JournalsTypeImmutable = Record; export type EntriesData = List; export const EntriesFetchRecord = fetchTypeRecord(); @@ -112,105 +105,100 @@ export const credentials = handleActions( {} as CredentialsDataRemote ); -const setMapModelReducer = , V extends BaseModel>(state: T, action: any) => { - const newState = fetchTypeIdentityReducer(state, action); - // Compare the states and see if they are really different - const newItems = newState.get('value', null); +function fetchCreateEntriesReducer(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)); +} + +export const entries = handleActions( + { + [actions.fetchEntries.toString()]: fetchCreateEntriesReducer, + [actions.addEntries.toString()]: fetchCreateEntriesReducer, + [actions.addJournal.toString()]: (state: EntriesTypeImmutable, action: any) => { + const journal = action.meta.item.uid; + const prevState = state.get(journal); + return state.set(journal, + fetchTypeIdentityReducer(prevState, { payload: [] }, false)); + }, + }, + ImmutableMap({}) +); - if (!newItems) { - return newState; +const setMapModelReducer = (state: JournalsData, action: Action) => { + if (action.error || !action.payload) { + return state; } - const ret = new Map(); + state = state ?? ImmutableMap().asMutable(); + const old = state.asMutable(); - newItems.forEach((item: V) => { - ret.set(item.uid, item); - }); + return state.withMutations((ret) => { + const items = action.payload!; + for (const item of items) { + const current = old.get(item.uid); + if (!current || !shallowEqual(current.serialize(), item.serialize())) { + ret.set(item.uid, item); + } - return newState.set('value', ImmutableMap(ret)); + if (current) { + old.delete(item.uid); + } + } + + // Delete all the items that were deleted remotely (not handled above). + for (const uid of old.keys()) { + ret.delete(uid); + } + }); }; -const addEditMapModelReducer = , V extends BaseModel>(state: T, action: any) => { +const addEditMapModelReducer = (state: JournalsData, action: ActionMeta) => { if (action.error) { - return state.set('error', action.payload); + return state; } else { let payload = (action.payload === undefined) ? null : action.payload; payload = (action.meta === undefined) ? payload : action.meta.item; - state = state.set('error', undefined); - - if (action.payload === undefined) { + if (!payload) { return state; } - const item = payload as V; - let value = state.get('value', null)!; - value = value.set(item.uid, item); - return state.set('value', value); + const item = payload; + return state.set(item.uid, item); } }; -const deleteMapModelReducer = >(state: T, action: any) => { +const deleteMapModelReducer = (state: JournalsData, action: ActionMeta) => { if (action.error) { - return state.set('error', action.payload); + return state; } else { let payload = (action.payload === undefined) ? null : action.payload; payload = (action.meta === undefined) ? payload : action.meta.item; - state = state.set('error', undefined); - - if (action.payload === undefined) { + if (!payload) { return state; } const uid = payload.uid; - let value = state.get('value', null)!; - value = value.delete(uid); - return state.set('value', value); + return state.delete(uid); } }; -const mapReducerActionsMapCreator = , V extends BaseModel>(actionName: string) => { - const setsReducer = (state: T, action: any) => setMapModelReducer(state, action); - const addEditReducer = (state: T, action: any) => addEditMapModelReducer(state, action); - const deleteReducer = (state: T, action: any) => deleteMapModelReducer(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, - }; -}; - -function fetchCreateEntriesReducer(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)); -} - -export const entries = handleActions( +export const journals = handleActions( { - [actions.fetchEntries.toString()]: fetchCreateEntriesReducer, - [actions.addEntries.toString()]: fetchCreateEntriesReducer, - [actions.addJournal.toString()]: (state: EntriesTypeImmutable, action: any) => { - const journal = action.meta.item.uid; - const prevState = state.get(journal); - return state.set(journal, - fetchTypeIdentityReducer(prevState, { payload: [] }, false)); + [actions.fetchListJournal.toString()]: setMapModelReducer as any, + [actions.addJournal.toString()]: addEditMapModelReducer, + [actions.updateJournal.toString()]: addEditMapModelReducer, + [actions.deleteJournal.toString()]: deleteMapModelReducer, + [actions.logout.toString()]: (state: JournalsData, _action: any) => { + return state.clear(); }, }, ImmutableMap({}) ); -export const journals = handleActions( - { - ...mapReducerActionsMapCreator('Journal'), - }, - new JournalsFetchRecord() -); - export const userInfo = handleActions( { [combineActions( diff --git a/tsconfig.json b/tsconfig.json index e4bdb45..01d1795 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "allowJs": true, "jsx": "preserve", "rootDir": "src", + "downlevelIteration": true, "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, "moduleResolution": "node",