From c5fc6f23f5196f411c14a5ad4b91a93b8492cbe5 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 12 Feb 2019 18:07:11 +0000 Subject: [PATCH] Jounal store: simplify the store functions and change the list to a hash It's always been a massive mess, this improves it. --- src/Journals/index.tsx | 4 +- src/SyncGate.tsx | 6 +- src/store/actions.ts | 36 +++++-- src/store/reducers.ts | 223 ++++++++++++++++++++++------------------- 4 files changed, 150 insertions(+), 119 deletions(-) diff --git a/src/Journals/index.tsx b/src/Journals/index.tsx index 00a976a..7fb97a7 100644 --- a/src/Journals/index.tsx +++ b/src/Journals/index.tsx @@ -10,7 +10,7 @@ import AppBarOverride from '../widgets/AppBarOverride'; import { routeResolver } from '../App'; import { store, JournalsData, UserInfoData, CredentialsData } from '../store'; -import { createJournal, updateJournal } from '../store/actions'; +import { addJournal, updateJournal } from '../store/actions'; import { SyncInfo } from '../SyncGate'; import * as EteSync from '../api/EteSync'; @@ -102,7 +102,7 @@ class Journals extends React.PureComponent { this.props.history.goBack() ); } else { - store.dispatch(createJournal(this.props.etesync, journal)).then(() => + store.dispatch(addJournal(this.props.etesync, journal)).then(() => this.props.history.goBack() ); } diff --git a/src/SyncGate.tsx b/src/SyncGate.tsx index a4b8ae9..32db328 100644 --- a/src/SyncGate.tsx +++ b/src/SyncGate.tsx @@ -19,7 +19,7 @@ import * as EteSync from './api/EteSync'; import { CURRENT_VERSION } from './api/Constants'; 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 { journal: EteSync.Journal; @@ -43,7 +43,7 @@ type PropsTypeInner = RouteComponentProps<{}> & PropsType & { const syncInfoSelector = createSelector( (props: PropsTypeInner) => props.etesync, - (props: PropsTypeInner) => props.journals.value as List, + (props: PropsTypeInner) => props.journals.value!, (props: PropsTypeInner) => props.entries, (props: PropsTypeInner) => props.userInfo.value!, (etesync, journals, entries, userInfo) => { @@ -115,7 +115,7 @@ class SyncGate extends React.PureComponent { const journal = new EteSync.Journal(); const cryptoManager = new EteSync.CryptoManager(this.props.etesync.encryptionKey, collection.uid); journal.setInfo(cryptoManager, collection); - store.dispatch(createJournal(this.props.etesync, journal)).then( + store.dispatch(addJournal(this.props.etesync, journal)).then( (journalAction: Action) => { // FIXME: Limit based on error code to only do it for associates. if (!journalAction.error) { diff --git a/src/store/actions.ts b/src/store/actions.ts index 1c12bba..aa7e112 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -3,7 +3,7 @@ import { createAction, createActions } from 'redux-actions'; import * as EteSync from '../api/EteSync'; import { UserInfo } from '../api/EteSync'; -import { CredentialsData, EntriesType } from './'; +import { CredentialsData, EntriesType, JournalsData } from './'; export const { fetchCredentials, logout } = createActions({ 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({ - FETCH_JOURNALS: (etesync: CredentialsData) => { +export const { fetchListJournal } = createActions({ + FETCH_LIST_JOURNAL: (etesync: CredentialsData) => { const creds = etesync.credentials; const apiBase = etesync.serviceApiUrl; let journalManager = new EteSync.JournalManager(creds, apiBase); @@ -54,8 +54,8 @@ export const { fetchJournals } = createActions({ }, }); -export const createJournal = createAction( - 'CREATE_JOURNAL', +export const addJournal = createAction( + 'ADD_JOURNAL', (etesync: CredentialsData, journal: EteSync.Journal) => { const creds = etesync.credentials; const apiBase = etesync.serviceApiUrl; @@ -64,7 +64,7 @@ export const createJournal = createAction( return journalManager.create(journal); }, (etesync: CredentialsData, journal: EteSync.Journal) => { - return { journal }; + return { item: journal }; }, ); @@ -78,7 +78,21 @@ export const updateJournal = createAction( return journalManager.update(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) { return (dispatch: any) => { - return dispatch(fetchJournals(etesync)).then((journalsAction: any) => { - const journals: Array = journalsAction.payload; - if (!journals || (journals.length === 0)) { + return dispatch(fetchListJournal(etesync)).then((journalsAction: any) => { + const journals: JournalsData = journalsAction.payload; + if (!journals || journals.isEmpty) { return false; } - journals.forEach((journal) => { + journals.forEach((journal, uid) => { let prevUid: string | null = null; const entries = currentEntries.get(journal.uid); if (entries && entries.value) { diff --git a/src/store/reducers.ts b/src/store/reducers.ts index d4a3dd8..6865ec7 100644 --- a/src/store/reducers.ts +++ b/src/store/reducers.ts @@ -1,11 +1,11 @@ import { combineReducers } from 'redux'; -import { persistReducer, createTransform } from 'redux-persist'; -import { handleAction, handleActions, combineActions } from 'redux-actions'; +import { createMigrate, persistReducer, createTransform } from 'redux-persist'; +import { Action, ActionFunctionAny, combineActions, handleAction, handleActions } from 'redux-actions'; import * as localforage from 'localforage'; 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'; @@ -35,11 +35,14 @@ function fetchTypeRecord() { }); } +interface BaseModel { + uid: string; +} + export type CredentialsType = FetchType; export type CredentialsTypeRemote = FetchType; -export type JournalsData = List; - +export type JournalsData = ImmutableMap; const JournalsFetchRecord = fetchTypeRecord(); export type JournalsType = FetchType; export type JournalsTypeImmutable = Record; @@ -48,8 +51,8 @@ export type EntriesData = List; const EntriesFetchRecord = fetchTypeRecord(); -export type EntriesTypeImmutable = Map>>; -export type EntriesType = Map>; +export type EntriesTypeImmutable = ImmutableMap>>; +export type EntriesType = ImmutableMap>; export type UserInfoData = EteSync.UserInfo; @@ -127,6 +130,77 @@ const credentials = handleActions( {value: null} ); +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); + + if (!newItems) { + return newState; + } + + const ret = new Map(); + + newItems.forEach((item: V) => { + ret.set(item.uid, item); + }); + + return newState.set('value', ImmutableMap(ret)); +}; + +const addEditMapModelReducer = , V extends BaseModel>(state: T, action: any) => { + if (action.error) { + return state.set('error', action.payload); + } 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) { + 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 deleteMapModelReducer = >(state: T, action: any) => { + if (action.error) { + return state.set('error', action.payload); + } 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) { + return state; + } + + const id = payload as number; + let value = state.get('value', null)!; + value = value.delete(id); + return state.set('value', value); + } +}; + +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, + }; +}; + export const entries = handleAction( combineActions(actions.fetchEntries, actions.createEntries), (state: EntriesTypeImmutable, action: any) => { @@ -135,95 +209,12 @@ export const entries = handleAction( return state.set(action.meta.journal, fetchTypeIdentityReducer(prevState, action, extend)); }, - Map({}) + ImmutableMap({}) ); const journals = handleActions( { - [actions.fetchJournals.toString()]: (state: JournalsTypeImmutable, action: any) => { - const newState = fetchTypeIdentityReducer(state, action); - // 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)) { - return newState; - } - - let oldJournalHash = {}; - oldJournals.forEach((x) => { - oldJournalHash[x.uid] = x.serialize(); - }); - - if (newJournals.every((journal: EteSync.Journal) => ( - (journal.uid in oldJournalHash) && - (journal.serialize().content === oldJournalHash[journal.uid].content) - ))) { - return state; - } else { - return newState; - } - }, - [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); - // 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)) { - return newState; - } - - let oldJournalHash = {}; - oldJournals.forEach((x) => { - oldJournalHash[x.uid] = x.serialize(); - }); - - if (newJournals.every((journal: EteSync.Journal) => ( - (journal.uid in oldJournalHash) && - (journal.serialize().content === oldJournalHash[journal.uid].content) - ))) { - return state; - } else { - return newState; - } - }, - [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); - // 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)) { - return newState; - } - - let oldJournalHash = {}; - oldJournals.forEach((x) => { - oldJournalHash[x.uid] = x.serialize(); - }); - - if (newJournals.every((journal: EteSync.Journal) => ( - (journal.uid in oldJournalHash) && - (journal.serialize().content === oldJournalHash[journal.uid].content) - ))) { - return state; - } else { - return newState; - } - }, + ...mapReducerActionsMapCreator('Journal'), }, new JournalsFetchRecord(), ); @@ -253,11 +244,23 @@ const userInfo = handleAction( new JournalsFetchRecord(), ); +const fetchActions = [ +] as Array>>; + +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( combineActions( - actions.fetchCredentials, - actions.fetchJournals, - actions.fetchEntries + ...fetchActions, ), (state: number, action: any) => { if (action.payload === undefined) { @@ -266,7 +269,7 @@ const fetchCount = handleAction( return state - 1; } }, - 0 + 0, ); const credentialsPersistConfig = { @@ -285,19 +288,22 @@ const journalsSerialize = (state: JournalsData) => { 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) { return null; } - return List(state.map((x: any) => { - let ret = new EteSync.Journal(x.version); + const newState = new Map(); + Object.keys(state).forEach((uid) => { + const x = state[uid]; + const ret = new EteSync.Journal(x.version); ret.deserialize(x); - return ret; - })); + newState.set(uid, ret); + }); + return ImmutableMap(newState); }; const entriesSerialize = (state: FetchType) => { @@ -360,7 +366,7 @@ const cacheDeserialize = (state: any, key: string) => { Object.keys(state).forEach((mapKey) => { ret[mapKey] = entriesDeserialize(state[mapKey]); }); - return Map(ret); + return ImmutableMap(ret); } else if (key === 'journals') { return new JournalsFetchRecord({value: journalsDeserialize(state)}); } else if (key === 'userInfo') { @@ -370,10 +376,21 @@ const cacheDeserialize = (state: any, key: string) => { return state; }; +const cacheMigrations = { + 0: (state: any) => { + return { + ...state, + journals: undefined + }; + }, +}; + const cachePersistConfig = { key: 'cache', + version: 1, storage: localforage, transforms: [createTransform(cacheSerialize, cacheDeserialize)], + migrate: createMigrate(cacheMigrations, { debug: false}), }; const reducers = combineReducers({