diff --git a/src/Journal.tsx b/src/Journal.tsx index e7cd9ef..da434b2 100644 --- a/src/Journal.tsx +++ b/src/Journal.tsx @@ -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 (); diff --git a/src/JournalEntries.tsx b/src/JournalEntries.tsx index f33c64b..5170bfd 100644 --- a/src/JournalEntries.tsx +++ b/src/JournalEntries.tsx @@ -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, + entries: Immutable.List, }; constructor(props: any) { diff --git a/src/Pim.tsx b/src/Pim.tsx index ed77426..83d1b2c 100644 --- a/src/Pim.tsx +++ b/src/Pim.tsx @@ -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 = []; 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; diff --git a/src/SyncGate.tsx b/src/SyncGate.tsx index b5748f3..ea2eb39 100644 --- a/src/SyncGate.tsx +++ b/src/SyncGate.tsx @@ -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 (); } diff --git a/src/etesync-helpers.tsx b/src/etesync-helpers.tsx index 17384ac..c3dd3be 100644 --- a/src/etesync-helpers.tsx +++ b/src/etesync-helpers.tsx @@ -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, + existingEntries: List, 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); diff --git a/src/journal-processors.tsx b/src/journal-processors.tsx index 8c47c8a..3b3d6a9 100644 --- a/src/journal-processors.tsx +++ b/src/journal-processors.tsx @@ -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) { 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) { let items: {[key: string]: EventType} = {}; const color = colorIntToHtml(collection.color); diff --git a/src/store.test.tsx b/src/store.test.tsx index c6a0e43..bc88d1a 100644 --- a/src/store.test.tsx +++ b/src/store.test.tsx @@ -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); }); diff --git a/src/store.tsx b/src/store.tsx index 3b67d95..c28c9a7 100644 --- a/src/store.tsx +++ b/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 { +interface FetchTypeInterface { value: T | null; fetching?: boolean; error?: Error; @@ -22,15 +24,27 @@ export interface CredentialsData { encryptionKey: string; } +type FetchType = FetchTypeInterface; + +function fetchTypeRecord() { + return Record>({ + value: null as T | null, + }); +} + export type CredentialsType = FetchType; -export type JournalsData = Array; +export type JournalsData = List; +const JournalsFetchRecord = fetchTypeRecord(); export type JournalsType = FetchType; -export type EntriesData = Array; +export type EntriesData = List; -export type EntriesType = {[key: string]: FetchType}; +const EntriesFetchRecord = fetchTypeRecord(); + +export type EntriesTypeImmutable = Map>>; +export type EntriesType = Map>; export interface StoreState { fetchCount: number; @@ -41,7 +55,7 @@ export interface StoreState { }; } -function fetchTypeIdentityReducer(state: FetchType = {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 = {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> = fetchTypeRecord()(), 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, key: string) => { +const entriesSerialize = (state: FetchType) => { 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 => { +const entriesDeserialize = (state: EteSync.EntryJson[]): FetchType => { 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, 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 = [