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
Tom Hacohen 7 years ago
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);
});

@ -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,18 +64,32 @@ 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;
let value = payload;
return {
fetching,
value,
};
}
}
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 = payload;
value = null;
}
return {
fetching,
value,
};
return state.set('value', value).set('fetching', fetching);
}
}
@ -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 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 cacheEntriesPersistConfig = {
key: 'entries',
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…
Cancel
Save