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() { render() {
const journalUid = this.props.match.params.journalUid; 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)) { if ((!entries) || (entries.value === null)) {
return (<LoadingIndicator />); return (<LoadingIndicator />);

@ -1,3 +1,5 @@
import * as Immutable from 'immutable';
import * as React from 'react'; import * as React from 'react';
import { List, ListItem } from 'material-ui/List'; import { List, ListItem } from 'material-ui/List';
import Dialog from 'material-ui/Dialog'; import Dialog from 'material-ui/Dialog';
@ -23,7 +25,7 @@ class JournalEntries extends React.Component {
props: { props: {
journal: EteSync.Journal, journal: EteSync.Journal,
entries: Array<EteSync.SyncEntry>, entries: Immutable.List<EteSync.SyncEntry>,
}; };
constructor(props: any) { constructor(props: any) {

@ -49,7 +49,11 @@ class Pim extends React.Component {
return; return;
} }
const entries = this.props.entries[journal.uid]; const entries = this.props.entries.get(journal.uid);
if (!entries) {
return;
}
if (entries.value === null) { if (entries.value === null) {
return; return;
@ -91,7 +95,7 @@ class Pim extends React.Component {
let collectionsCalendar: Array<EteSync.CollectionInfo> = []; let collectionsCalendar: Array<EteSync.CollectionInfo> = [];
const journalMap = this.props.journals.reduce( const journalMap = this.props.journals.reduce(
(ret, journal) => { (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); const cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version);
let prevUid: string | null = null; 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)) { if (nextProps.journals.value && (this.props.journals.value !== nextProps.journals.value)) {
for (const journal of nextProps.journals.value) { for (const journal of nextProps.journals.value) {
let prevUid: string | null = null; let prevUid: string | null = null;
const entries = this.props.entries[journal.uid]; const entries = this.props.entries.get(journal.uid);
if (entries && entries.value && (entries.value.length > 0)) { if (entries && entries.value) {
prevUid = entries.value[entries.value.length - 1].uid; const last = entries.value.last();
prevUid = (last) ? last.uid : null;
} }
store.dispatch(fetchEntries(this.props.etesync, journal.uid, prevUid)); store.dispatch(fetchEntries(this.props.etesync, journal.uid, prevUid));
@ -46,14 +47,11 @@ class SyncGate extends React.Component {
} }
render() { render() {
const entryArrays = Object.keys(this.props.entries).map((key) => { const entryArrays = this.props.entries;
return this.props.entries[key].value;
});
const journals = this.props.journals.value; const journals = this.props.journals.value;
if ((journals === null) || if ((journals === null) ||
(entryArrays.length === 0) || !entryArrays.every((x: any) => (x !== null))) { (entryArrays.size === 0) || !entryArrays.every((x: any) => (x.value !== null))) {
return (<LoadingIndicator />); return (<LoadingIndicator />);
} }

@ -1,3 +1,5 @@
import { List } from 'immutable';
import * as EteSync from './api/EteSync'; import * as EteSync from './api/EteSync';
import { CredentialsData, createEntries } from './store'; import { CredentialsData, createEntries } from './store';
@ -5,7 +7,7 @@ import { CredentialsData, createEntries } from './store';
export function createJournalEntry( export function createJournalEntry(
etesync: CredentialsData, etesync: CredentialsData,
journal: EteSync.Journal, journal: EteSync.Journal,
existingEntries: Array<EteSync.Entry>, existingEntries: List<EteSync.Entry>,
action: EteSync.SyncEntryAction, action: EteSync.SyncEntryAction,
content: string) { content: string) {
@ -18,8 +20,9 @@ export function createJournalEntry(
let prevUid: string | null = null; let prevUid: string | null = null;
const entries = existingEntries; const entries = existingEntries;
if (entries.length > 0) { const last = entries.last();
prevUid = entries[entries.length - 1].uid; if (last) {
prevUid = last.uid;
} }
const cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version); 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 * as ICAL from 'ical.js';
import { EventType, ContactType } from './pim-types'; import { EventType, ContactType } from './pim-types';
import * as EteSync from './api/EteSync'; 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} = {}; let items: {[key: string]: ContactType} = {};
for (const syncEntry of entries) { for (const syncEntry of entries) {
@ -47,7 +49,7 @@ function colorIntToHtml(color: number) {
((alpha > 0) ? toHex(alpha) : ''); ((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} = {}; let items: {[key: string]: EventType} = {};
const color = colorIntToHtml(collection.color); 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'; import * as EteSync from './api/EteSync';
it('Entries reducer', () => { it('Entries reducer', () => {
const jId = '24324324324'; const jId = '24324324324';
let state = {}; let state = Map({}) as EntriesTypeImmutable;
let entry = new EteSync.Entry(); let entry = new EteSync.Entry();
entry.deserialize({ entry.deserialize({
@ -18,21 +20,30 @@ it('Entries reducer', () => {
payload: [entry], payload: [entry],
}; };
let journal;
let entry2;
state = entries(state, action as any); 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 // We replace if there's no prevUid
state = entries(state, action as any); state = entries(state, action as any);
expect(state[jId].value[0].serialize()).toEqual(entry.serialize()); journal = state.get(jId) as any;
expect(state[jId].value.length).toBe(1); entry2 = journal.value.get(0);
expect(entry2.serialize()).toEqual(entry.serialize());
expect(journal.value.size).toBe(1);
// We extend if prevUid is set // We extend if prevUid is set
action.meta.prevUid = entry.uid; action.meta.prevUid = entry.uid;
state = entries(state, action as any); 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 // Creating entries should also work the same
action.type = createEntries.toString(); action.type = createEntries.toString();
state = entries(state, action as any); 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 thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger'; import { createLogger } from 'redux-logger';
import { List, Map, Record } from 'immutable';
import promiseMiddleware from './promise-middleware'; import promiseMiddleware from './promise-middleware';
import * as EteSync from './api/EteSync'; import * as EteSync from './api/EteSync';
export interface FetchType<T> { interface FetchTypeInterface<T> {
value: T | null; value: T | null;
fetching?: boolean; fetching?: boolean;
error?: Error; error?: Error;
@ -22,15 +24,27 @@ export interface CredentialsData {
encryptionKey: string; 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 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 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 { export interface StoreState {
fetchCount: number; 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) { if (action.error) {
return { return {
value: null, value: null,
@ -50,18 +64,32 @@ function fetchTypeIdentityReducer(state: FetchType<any> = {value: null}, action:
} else { } else {
const fetching = (action.payload === undefined) ? true : undefined; const fetching = (action.payload === undefined) ? true : undefined;
const payload = (action.payload === undefined) ? null : action.payload; 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 (extend && (value !== null)) {
if (payload !== null) { if (payload !== null) {
value = value.concat(payload); value = value.concat(payload);
} }
} else if (payload !== null) {
value = List(payload);
} else { } else {
value = payload; value = null;
} }
return { return state.set('value', value).set('fetching', fetching);
fetching,
value,
};
} }
} }
@ -131,7 +159,7 @@ export const { fetchEntries, createEntries } = createActions({
const credentials = handleActions( const credentials = handleActions(
{ {
[fetchCredentials.toString()]: fetchTypeIdentityReducer, [fetchCredentials.toString()]: credentialsIdentityReducer,
[logout.toString()]: (state: CredentialsType, action: any) => { [logout.toString()]: (state: CredentialsType, action: any) => {
return {out: true, value: null}; return {out: true, value: null};
}, },
@ -141,20 +169,19 @@ const credentials = handleActions(
export const entries = handleAction( export const entries = handleAction(
combineActions(fetchEntries, createEntries), combineActions(fetchEntries, createEntries),
(state: EntriesType, action: any) => { (state: EntriesTypeImmutable, action: any) => {
const prevState = state[action.meta.journal]; const prevState = state.get(action.meta.journal);
const extend = action.meta.prevUid != null; const extend = action.meta.prevUid != null;
return { ...state, return state.set(action.meta.journal,
[action.meta.journal]: fetchTypeIdentityReducer(prevState, action, extend) fetchTypeIdentityReducer(prevState, action, extend));
};
}, },
{} Map({})
); );
const journals = handleAction( const journals = handleAction(
fetchJournals, fetchJournals,
fetchTypeIdentityReducer, fetchTypeIdentityReducer,
{value: null} JournalsFetchRecord(),
); );
const fetchCount = handleAction( const fetchCount = handleAction(
@ -184,7 +211,7 @@ const journalsSerialize = (state: JournalsData) => {
return null; return null;
} }
return state.map((x) => x.serialize()); return state.map((x) => x.serialize()).toJS();
}; };
const journalsDeserialize = (state: EteSync.JournalJson[]) => { const journalsDeserialize = (state: EteSync.JournalJson[]) => {
@ -192,53 +219,74 @@ const journalsDeserialize = (state: EteSync.JournalJson[]) => {
return null; return null;
} }
return state.map((x: any) => { return List(state.map((x: any) => {
let ret = new EteSync.Journal(x.version); let ret = new EteSync.Journal(x.version);
ret.deserialize(x); ret.deserialize(x);
return ret; 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)) { if ((state === null) || (state.value == null)) {
return 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) { 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(); let ret = new EteSync.Entry();
ret.deserialize(x); ret.deserialize(x);
return ret; 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 = { const cachePersistConfig = {
key: 'entries', key: 'cache',
storage: localforage, storage: localforage,
transforms: [createTransform(entriesSerialize, entriesDeserialize)], transforms: [createTransform(cacheSerialize, cacheDeserialize)],
}; };
const reducers = combineReducers({ const reducers = combineReducers({
fetchCount, fetchCount,
credentials: persistReducer(credentialsPersistConfig, credentials), credentials: persistReducer(credentialsPersistConfig, credentials),
cache: combineReducers({ cache: persistReducer(cachePersistConfig, combineReducers({
journals: persistReducer(cacheJournalsPersistConfig, journals), entries,
entries: persistReducer(cacheEntriesPersistConfig, entries), journals,
}) })),
}); });
let middleware = [ let middleware = [

Loading…
Cancel
Save