Move the store to immutable.js
This significantly helps with reducing the number of copies we do, because in most cases a refresh will not change a journal.master
parent
6d09ea1ac6
commit
c3e686002e
|
@ -39,7 +39,7 @@ class Journal extends React.Component {
|
|||
|
||||
render() {
|
||||
const journalUid = this.props.match.params.journalUid;
|
||||
const entries = this.props.entries[journalUid];
|
||||
const entries = this.props.entries.get(journalUid);
|
||||
|
||||
if ((!entries) || (entries.value === null)) {
|
||||
return (<LoadingIndicator />);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import * as Immutable from 'immutable';
|
||||
|
||||
import * as React from 'react';
|
||||
import { List, ListItem } from 'material-ui/List';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
|
@ -23,7 +25,7 @@ class JournalEntries extends React.Component {
|
|||
|
||||
props: {
|
||||
journal: EteSync.Journal,
|
||||
entries: Array<EteSync.SyncEntry>,
|
||||
entries: Immutable.List<EteSync.SyncEntry>,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
|
|
|
@ -49,7 +49,11 @@ class Pim extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const entries = this.props.entries[journal.uid];
|
||||
const entries = this.props.entries.get(journal.uid);
|
||||
|
||||
if (!entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.value === null) {
|
||||
return;
|
||||
|
@ -91,7 +95,7 @@ class Pim extends React.Component {
|
|||
let collectionsCalendar: Array<EteSync.CollectionInfo> = [];
|
||||
const journalMap = this.props.journals.reduce(
|
||||
(ret, journal) => {
|
||||
const journalEntries = this.props.entries[journal.uid];
|
||||
const journalEntries = this.props.entries.get(journal.uid);
|
||||
const cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version);
|
||||
|
||||
let prevUid: string | null = null;
|
||||
|
|
|
@ -35,9 +35,10 @@ class SyncGate extends React.Component {
|
|||
if (nextProps.journals.value && (this.props.journals.value !== nextProps.journals.value)) {
|
||||
for (const journal of nextProps.journals.value) {
|
||||
let prevUid: string | null = null;
|
||||
const entries = this.props.entries[journal.uid];
|
||||
if (entries && entries.value && (entries.value.length > 0)) {
|
||||
prevUid = entries.value[entries.value.length - 1].uid;
|
||||
const entries = this.props.entries.get(journal.uid);
|
||||
if (entries && entries.value) {
|
||||
const last = entries.value.last();
|
||||
prevUid = (last) ? last.uid : null;
|
||||
}
|
||||
|
||||
store.dispatch(fetchEntries(this.props.etesync, journal.uid, prevUid));
|
||||
|
@ -46,14 +47,11 @@ class SyncGate extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const entryArrays = Object.keys(this.props.entries).map((key) => {
|
||||
return this.props.entries[key].value;
|
||||
});
|
||||
|
||||
const entryArrays = this.props.entries;
|
||||
const journals = this.props.journals.value;
|
||||
|
||||
if ((journals === null) ||
|
||||
(entryArrays.length === 0) || !entryArrays.every((x: any) => (x !== null))) {
|
||||
(entryArrays.size === 0) || !entryArrays.every((x: any) => (x.value !== null))) {
|
||||
return (<LoadingIndicator />);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { List } from 'immutable';
|
||||
|
||||
import * as EteSync from './api/EteSync';
|
||||
|
||||
import { CredentialsData, createEntries } from './store';
|
||||
|
@ -5,7 +7,7 @@ import { CredentialsData, createEntries } from './store';
|
|||
export function createJournalEntry(
|
||||
etesync: CredentialsData,
|
||||
journal: EteSync.Journal,
|
||||
existingEntries: Array<EteSync.Entry>,
|
||||
existingEntries: List<EteSync.Entry>,
|
||||
action: EteSync.SyncEntryAction,
|
||||
content: string) {
|
||||
|
||||
|
@ -18,8 +20,9 @@ export function createJournalEntry(
|
|||
let prevUid: string | null = null;
|
||||
|
||||
const entries = existingEntries;
|
||||
if (entries.length > 0) {
|
||||
prevUid = entries[entries.length - 1].uid;
|
||||
const last = entries.last();
|
||||
if (last) {
|
||||
prevUid = last.uid;
|
||||
}
|
||||
|
||||
const cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { List } from 'immutable';
|
||||
|
||||
import * as ICAL from 'ical.js';
|
||||
|
||||
import { EventType, ContactType } from './pim-types';
|
||||
|
||||
import * as EteSync from './api/EteSync';
|
||||
|
||||
export function syncEntriesToItemMap(collection: EteSync.CollectionInfo, entries: EteSync.SyncEntry[]) {
|
||||
export function syncEntriesToItemMap(collection: EteSync.CollectionInfo, entries: List<EteSync.SyncEntry>) {
|
||||
let items: {[key: string]: ContactType} = {};
|
||||
|
||||
for (const syncEntry of entries) {
|
||||
|
@ -47,7 +49,7 @@ function colorIntToHtml(color: number) {
|
|||
((alpha > 0) ? toHex(alpha) : '');
|
||||
}
|
||||
|
||||
export function syncEntriesToCalendarItemMap(collection: EteSync.CollectionInfo, entries: EteSync.SyncEntry[]) {
|
||||
export function syncEntriesToCalendarItemMap(collection: EteSync.CollectionInfo, entries: List<EteSync.SyncEntry>) {
|
||||
let items: {[key: string]: EventType} = {};
|
||||
|
||||
const color = colorIntToHtml(collection.color);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { entries, createEntries, fetchEntries } from './store';
|
||||
import { entries, createEntries, fetchEntries, EntriesTypeImmutable } from './store';
|
||||
|
||||
import { Map } from 'immutable';
|
||||
|
||||
import * as EteSync from './api/EteSync';
|
||||
|
||||
it('Entries reducer', () => {
|
||||
const jId = '24324324324';
|
||||
let state = {};
|
||||
let state = Map({}) as EntriesTypeImmutable;
|
||||
|
||||
let entry = new EteSync.Entry();
|
||||
entry.deserialize({
|
||||
|
@ -18,21 +20,30 @@ it('Entries reducer', () => {
|
|||
payload: [entry],
|
||||
};
|
||||
|
||||
let journal;
|
||||
let entry2;
|
||||
|
||||
state = entries(state, action as any);
|
||||
expect(state[jId].value[0].serialize()).toEqual(entry.serialize());
|
||||
journal = state.get(jId) as any;
|
||||
entry2 = journal.value.get(0);
|
||||
expect(entry2.serialize()).toEqual(entry.serialize());
|
||||
|
||||
// We replace if there's no prevUid
|
||||
state = entries(state, action as any);
|
||||
expect(state[jId].value[0].serialize()).toEqual(entry.serialize());
|
||||
expect(state[jId].value.length).toBe(1);
|
||||
journal = state.get(jId) as any;
|
||||
entry2 = journal.value.get(0);
|
||||
expect(entry2.serialize()).toEqual(entry.serialize());
|
||||
expect(journal.value.size).toBe(1);
|
||||
|
||||
// We extend if prevUid is set
|
||||
action.meta.prevUid = entry.uid;
|
||||
state = entries(state, action as any);
|
||||
expect(state[jId].value.length).toBe(2);
|
||||
journal = state.get(jId) as any;
|
||||
expect(journal.value.size).toBe(2);
|
||||
|
||||
// Creating entries should also work the same
|
||||
action.type = createEntries.toString();
|
||||
state = entries(state, action as any);
|
||||
expect(state[jId].value.length).toBe(3);
|
||||
journal = state.get(jId) as any;
|
||||
expect(journal.value.size).toBe(3);
|
||||
});
|
||||
|
|
136
src/store.tsx
136
src/store.tsx
|
@ -6,11 +6,13 @@ import session from 'redux-persist/lib/storage/session';
|
|||
import thunkMiddleware from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import { List, Map, Record } from 'immutable';
|
||||
|
||||
import promiseMiddleware from './promise-middleware';
|
||||
|
||||
import * as EteSync from './api/EteSync';
|
||||
|
||||
export interface FetchType<T> {
|
||||
interface FetchTypeInterface<T> {
|
||||
value: T | null;
|
||||
fetching?: boolean;
|
||||
error?: Error;
|
||||
|
@ -22,15 +24,27 @@ export interface CredentialsData {
|
|||
encryptionKey: string;
|
||||
}
|
||||
|
||||
type FetchType<T> = FetchTypeInterface<T>;
|
||||
|
||||
function fetchTypeRecord<T>() {
|
||||
return Record<FetchTypeInterface<T>>({
|
||||
value: null as T | null,
|
||||
});
|
||||
}
|
||||
|
||||
export type CredentialsType = FetchType<CredentialsData>;
|
||||
|
||||
export type JournalsData = Array<EteSync.Journal>;
|
||||
export type JournalsData = List<EteSync.Journal>;
|
||||
|
||||
const JournalsFetchRecord = fetchTypeRecord<JournalsData>();
|
||||
export type JournalsType = FetchType<JournalsData>;
|
||||
|
||||
export type EntriesData = Array<EteSync.Entry>;
|
||||
export type EntriesData = List<EteSync.Entry>;
|
||||
|
||||
export type EntriesType = {[key: string]: FetchType<EntriesData>};
|
||||
const EntriesFetchRecord = fetchTypeRecord<EntriesData>();
|
||||
|
||||
export type EntriesTypeImmutable = Map<string, Record<FetchType<EntriesData>>>;
|
||||
export type EntriesType = Map<string, FetchType<EntriesData>>;
|
||||
|
||||
export interface StoreState {
|
||||
fetchCount: number;
|
||||
|
@ -41,7 +55,7 @@ export interface StoreState {
|
|||
};
|
||||
}
|
||||
|
||||
function fetchTypeIdentityReducer(state: FetchType<any> = {value: null}, action: any, extend: boolean = false) {
|
||||
function credentialsIdentityReducer(state: CredentialsType = {value: null}, action: any, extend: boolean = false) {
|
||||
if (action.error) {
|
||||
return {
|
||||
value: null,
|
||||
|
@ -50,14 +64,7 @@ function fetchTypeIdentityReducer(state: FetchType<any> = {value: null}, action:
|
|||
} else {
|
||||
const fetching = (action.payload === undefined) ? true : undefined;
|
||||
const payload = (action.payload === undefined) ? null : action.payload;
|
||||
let value = state.value;
|
||||
if (extend && (value !== null)) {
|
||||
if (payload !== null) {
|
||||
value = value.concat(payload);
|
||||
}
|
||||
} else {
|
||||
value = payload;
|
||||
}
|
||||
let value = payload;
|
||||
return {
|
||||
fetching,
|
||||
value,
|
||||
|
@ -65,6 +72,27 @@ function fetchTypeIdentityReducer(state: FetchType<any> = {value: null}, action:
|
|||
}
|
||||
}
|
||||
|
||||
function fetchTypeIdentityReducer(
|
||||
state: Record<FetchType<any>> = fetchTypeRecord<any>()(), action: any, extend: boolean = false) {
|
||||
if (action.error) {
|
||||
return state.set('value', null).set('error', action.payload);
|
||||
} else {
|
||||
const fetching = (action.payload === undefined) ? true : undefined;
|
||||
const payload = (action.payload === undefined) ? null : action.payload;
|
||||
let value = state.get('value', null);
|
||||
if (extend && (value !== null)) {
|
||||
if (payload !== null) {
|
||||
value = value.concat(payload);
|
||||
}
|
||||
} else if (payload !== null) {
|
||||
value = List(payload);
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
return state.set('value', value).set('fetching', fetching);
|
||||
}
|
||||
}
|
||||
|
||||
export const { fetchCredentials, logout } = createActions({
|
||||
FETCH_CREDENTIALS: (username: string, password: string, encryptionPassword: string, server: string) => {
|
||||
const authenticator = new EteSync.Authenticator(server);
|
||||
|
@ -131,7 +159,7 @@ export const { fetchEntries, createEntries } = createActions({
|
|||
|
||||
const credentials = handleActions(
|
||||
{
|
||||
[fetchCredentials.toString()]: fetchTypeIdentityReducer,
|
||||
[fetchCredentials.toString()]: credentialsIdentityReducer,
|
||||
[logout.toString()]: (state: CredentialsType, action: any) => {
|
||||
return {out: true, value: null};
|
||||
},
|
||||
|
@ -141,20 +169,19 @@ const credentials = handleActions(
|
|||
|
||||
export const entries = handleAction(
|
||||
combineActions(fetchEntries, createEntries),
|
||||
(state: EntriesType, action: any) => {
|
||||
const prevState = state[action.meta.journal];
|
||||
(state: EntriesTypeImmutable, action: any) => {
|
||||
const prevState = state.get(action.meta.journal);
|
||||
const extend = action.meta.prevUid != null;
|
||||
return { ...state,
|
||||
[action.meta.journal]: fetchTypeIdentityReducer(prevState, action, extend)
|
||||
};
|
||||
return state.set(action.meta.journal,
|
||||
fetchTypeIdentityReducer(prevState, action, extend));
|
||||
},
|
||||
{}
|
||||
Map({})
|
||||
);
|
||||
|
||||
const journals = handleAction(
|
||||
fetchJournals,
|
||||
fetchTypeIdentityReducer,
|
||||
{value: null}
|
||||
JournalsFetchRecord(),
|
||||
);
|
||||
|
||||
const fetchCount = handleAction(
|
||||
|
@ -184,7 +211,7 @@ const journalsSerialize = (state: JournalsData) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
return state.map((x) => x.serialize());
|
||||
return state.map((x) => x.serialize()).toJS();
|
||||
};
|
||||
|
||||
const journalsDeserialize = (state: EteSync.JournalJson[]) => {
|
||||
|
@ -192,53 +219,74 @@ const journalsDeserialize = (state: EteSync.JournalJson[]) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
return state.map((x: any) => {
|
||||
return List(state.map((x: any) => {
|
||||
let ret = new EteSync.Journal(x.version);
|
||||
ret.deserialize(x);
|
||||
return ret;
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
const cacheJournalsPersistConfig = {
|
||||
key: 'journals',
|
||||
storage: localforage,
|
||||
transforms: [createTransform(journalsSerialize, journalsDeserialize)],
|
||||
whitelist: ['value'],
|
||||
};
|
||||
|
||||
const entriesSerialize = (state: FetchType<EntriesData>, key: string) => {
|
||||
const entriesSerialize = (state: FetchType<EntriesData>) => {
|
||||
if ((state === null) || (state.value == null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.value.map((x) => x.serialize());
|
||||
return state.value.map((x) => x.serialize()).toJS();
|
||||
};
|
||||
|
||||
const entriesDeserialize = (state: EteSync.EntryJson[], key: string): FetchType<EntriesData> => {
|
||||
const entriesDeserialize = (state: EteSync.EntryJson[]): FetchType<EntriesData> => {
|
||||
if (state === null) {
|
||||
return {value: null};
|
||||
return EntriesFetchRecord({value: null});
|
||||
}
|
||||
|
||||
return {value: state.map((x: any) => {
|
||||
return EntriesFetchRecord({value: List(state.map((x: any) => {
|
||||
let ret = new EteSync.Entry();
|
||||
ret.deserialize(x);
|
||||
return ret;
|
||||
})};
|
||||
}))});
|
||||
};
|
||||
|
||||
const cacheEntriesPersistConfig = {
|
||||
key: 'entries',
|
||||
const cacheSerialize = (state: any, key: string) => {
|
||||
if (key === 'entries') {
|
||||
let ret = {};
|
||||
state.forEach((value: FetchType<EntriesData>, mapKey: string) => {
|
||||
ret[mapKey] = entriesSerialize(value);
|
||||
});
|
||||
return ret;
|
||||
} else if (key === 'journals') {
|
||||
return journalsSerialize(state.value);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const cacheDeserialize = (state: any, key: string) => {
|
||||
if (key === 'entries') {
|
||||
let ret = {};
|
||||
Object.keys(state).forEach((mapKey) => {
|
||||
ret[mapKey] = entriesDeserialize(state[mapKey]);
|
||||
});
|
||||
return Map(ret);
|
||||
} else if (key === 'journals') {
|
||||
return JournalsFetchRecord({value: journalsDeserialize(state)});
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const cachePersistConfig = {
|
||||
key: 'cache',
|
||||
storage: localforage,
|
||||
transforms: [createTransform(entriesSerialize, entriesDeserialize)],
|
||||
transforms: [createTransform(cacheSerialize, cacheDeserialize)],
|
||||
};
|
||||
|
||||
const reducers = combineReducers({
|
||||
fetchCount,
|
||||
credentials: persistReducer(credentialsPersistConfig, credentials),
|
||||
cache: combineReducers({
|
||||
journals: persistReducer(cacheJournalsPersistConfig, journals),
|
||||
entries: persistReducer(cacheEntriesPersistConfig, entries),
|
||||
})
|
||||
cache: persistReducer(cachePersistConfig, combineReducers({
|
||||
entries,
|
||||
journals,
|
||||
})),
|
||||
});
|
||||
|
||||
let middleware = [
|
||||
|
|
Loading…
Reference in New Issue