From 470aac134262a223d45268cda3d023ee45543ad6 Mon Sep 17 00:00:00 2001 From: Jason Wall Date: Wed, 19 Aug 2020 13:11:41 -0400 Subject: [PATCH] implement a card cache, this might not be working yet --- src/src/app/common/card-operations.ts | 10 +- .../saved-page-card.component.ts | 21 +++- src/src/app/models/app-state.ts | 7 +- src/src/app/models/page-state.ts | 4 +- src/src/app/pages/search/search.page.ts | 3 +- src/src/app/services/app-state-actions.ts | 11 +++ .../app/services/app-state-initial-state.ts | 1 + .../app/services/app-state-reducer.spec.ts | 95 ++++++++++++++++++- src/src/app/services/app-state-reducer.ts | 63 +++++++++--- src/src/app/services/app.service.ts | 49 +++++++++- 10 files changed, 233 insertions(+), 31 deletions(-) diff --git a/src/src/app/common/card-operations.ts b/src/src/app/common/card-operations.ts index dd28830f..64e8c17c 100644 --- a/src/src/app/common/card-operations.ts +++ b/src/src/app/common/card-operations.ts @@ -1,7 +1,11 @@ import { BibleReference, Overlap } from './bible-reference'; -import { CardItem, CardType } from '../models/card-state'; +import { CardType, DataReference } from '../models/card-state'; -export function maybeMergeCards(leftCard: CardItem, rightCard: CardItem, strategy: Overlap): CardItem | null { +export function maybeMergeCards( + leftCard: DataReference, + rightCard: DataReference, + strategy: Overlap +): DataReference | null { if (leftCard.type === rightCard.type) { switch (strategy) { case Overlap.Equal: @@ -28,7 +32,7 @@ export function maybeMergeCards(leftCard: CardItem, rightCard: CardItem, strateg return null; } -export function mergeCardList(cardList: readonly CardItem[], strategy: Overlap): CardItem[] { +export function mergeCardList(cardList: readonly DataReference[], strategy: Overlap) { if (strategy === Overlap.None || cardList.length === 0) { return [...cardList]; } diff --git a/src/src/app/components/saved-page-card/saved-page-card.component.ts b/src/src/app/components/saved-page-card/saved-page-card.component.ts index da7a2264..8a5b055d 100644 --- a/src/src/app/components/saved-page-card/saved-page-card.component.ts +++ b/src/src/app/components/saved-page-card/saved-page-card.component.ts @@ -4,11 +4,14 @@ import { MatDialog } from '@angular/material/dialog'; import { AppService } from '../../services/app.service'; import { Observable } from 'rxjs'; import { SavedPage } from 'src/app/models/page-state'; -import { CardItem, CardType } from 'src/app/models/card-state'; +import { CardItem, CardType, DataReference } from 'src/app/models/card-state'; import { NoteItem } from 'src/app/models/note-state'; import { OkCancelModalComponent, OkCancelResult } from '../ok-cancel-modal/ok-cancel-modal.component'; import { MatSnackBar } from '@angular/material/snack-bar'; import { PageEditModalComponent } from '../page-edit-modal/page-edit-modal.component'; +import { HashTable } from 'src/app/common/hashtable'; +import { SubscriberBase } from 'src/app/common/subscriber-base'; +import { getCardFromCache } from 'src/app/services/app-state-reducer'; @Component({ selector: 'app-saved-page-card', @@ -16,19 +19,29 @@ import { PageEditModalComponent } from '../page-edit-modal/page-edit-modal.compo styleUrls: ['./saved-page-card.component.scss'], preserveWhitespaces: true, }) -export class SavedPageCardComponent implements OnInit { +export class SavedPageCardComponent extends SubscriberBase implements OnInit { icon$: Observable; + cache: HashTable; @Input() savedPage: SavedPage; constructor(protected appService: AppService, public dialog: MatDialog, private snackBar: MatSnackBar) { + super(); + this.icon$ = appService.select((state) => state.settings.value.cardIcons.savedPage); + this.addSubscription( + this.appService + .select((state) => state.cardCache) + .subscribe((cache) => { + this.cache = cache; + }) + ); } - format(item: CardItem) { + format(item: DataReference) { if (item.type === CardType.Note) { - return `Note: ${(item.data as NoteItem).title}`; + return `Note: ${(getCardFromCache(item, this.cache).data as NoteItem).title}`; } else if (item.type === CardType.Passage) { return `Passage: ${item.qry}`; } else if (item.type === CardType.Strongs) { diff --git a/src/src/app/models/app-state.ts b/src/src/app/models/app-state.ts index b222da99..51a0ab2f 100644 --- a/src/src/app/models/app-state.ts +++ b/src/src/app/models/app-state.ts @@ -1,12 +1,15 @@ import { IStorable } from '../common/storable'; import { NoteItem } from './note-state'; import { Overlap } from '../common/bible-reference'; -import { CardItem, CardIcons } from './card-state'; +import { CardItem, CardIcons, DataReference } from './card-state'; import { SavedPage } from './page-state'; +import { HashTable } from '../common/hashtable'; export interface AppState { readonly currentSavedPage: SavedPage; - readonly currentCards: readonly CardItem[]; + readonly currentCards: readonly DataReference[]; + + readonly cardCache: HashTable; readonly savedPages: IStorable; readonly notes: IStorable; diff --git a/src/src/app/models/page-state.ts b/src/src/app/models/page-state.ts index 9c4092e9..afc1bc84 100644 --- a/src/src/app/models/page-state.ts +++ b/src/src/app/models/page-state.ts @@ -1,7 +1,7 @@ -import { CardItem } from './card-state'; +import { DataReference } from './card-state'; export interface SavedPage { - readonly queries: readonly CardItem[]; + readonly queries: readonly DataReference[]; readonly title: string; readonly id: string; } diff --git a/src/src/app/pages/search/search.page.ts b/src/src/app/pages/search/search.page.ts index 9ec6efbc..43e93f19 100644 --- a/src/src/app/pages/search/search.page.ts +++ b/src/src/app/pages/search/search.page.ts @@ -9,6 +9,7 @@ import { SubscriberBase } from '../../common/subscriber-base'; import { BibleReference } from '../../common/bible-reference'; import { VersePickerModalComponent } from '../../components/verse-picker-modal/verse-picker-modal.component'; import { CardItem, CardType } from 'src/app/models/card-state'; +import { getCardFromCache } from 'src/app/services/app-state-reducer'; @Component({ selector: 'app-search-page', @@ -17,7 +18,7 @@ import { CardItem, CardType } from 'src/app/models/card-state'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class SearchPage extends SubscriberBase implements OnInit { - cards$ = this.appService.select((state) => state.currentCards); + cards$ = this.appService.select((state) => state.currentCards.map((o) => getCardFromCache(o, state.cardCache))); suggestions$ = this.appService.select((state) => state.autocomplete); savedPagedLoaded = false; diff --git a/src/src/app/services/app-state-actions.ts b/src/src/app/services/app-state-actions.ts index 3c95091d..5f42b5f6 100644 --- a/src/src/app/services/app-state-actions.ts +++ b/src/src/app/services/app-state-actions.ts @@ -96,6 +96,13 @@ export class AppActionFactory { } as AppAction; } + static newUpdateCards(cards: CardItem[]): AppAction { + return { + type: 'UPDATE_CARDS', + cards, + } as AppAction; + } + static newUpdateError(error: Error): AppAction { return { type: 'UPDATE_ERROR', @@ -236,6 +243,10 @@ export type AppAction = card: CardItem; direction: MoveDirection; } + | { + type: 'UPDATE_CARDS'; + cards: CardItem[]; + } | { type: 'UPDATE_ERROR'; error: Error; diff --git a/src/src/app/services/app-state-initial-state.ts b/src/src/app/services/app-state-initial-state.ts index 44166f1d..a283411f 100644 --- a/src/src/app/services/app-state-initial-state.ts +++ b/src/src/app/services/app-state-initial-state.ts @@ -4,6 +4,7 @@ import { Overlap } from '../common/bible-reference'; export const initialState: AppState = { user: null, currentCards: [], + cardCache: {}, autocomplete: [], currentSavedPage: null, savedPages: null, diff --git a/src/src/app/services/app-state-reducer.spec.ts b/src/src/app/services/app-state-reducer.spec.ts index 6c8aee58..069e23e6 100644 --- a/src/src/app/services/app-state-reducer.spec.ts +++ b/src/src/app/services/app-state-reducer.spec.ts @@ -1,11 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { reducer, getNewestStorable } from './app-state-reducer'; +import { reducer, getNewestStorable, updateCache, getCardItemKey, removeFromCache } from './app-state-reducer'; import { AppActionFactory } from './app-state-actions'; import { Overlap } from '../common/bible-reference'; import { Storable } from '../common/storable'; import { CardType, CardItem } from '../models/card-state'; import { SavedPage } from '../models/page-state'; import { AppState } from '../models/app-state'; +import { HashTable } from '../common/hashtable'; +import { NoteItem } from '../models/note-state'; describe('getNewestStorable', () => { it('maybeMutateStorable', () => { @@ -26,10 +28,82 @@ describe('getNewestStorable', () => { }); }); +describe('Card Cache', () => { + it('updateCache', () => { + const card1: CardItem = { + qry: 'jason', + type: CardType.Passage, + data: null, + }; + const card2: CardItem = { + qry: 'jason', + type: CardType.Passage, + data: { + id: 'adsf', + xref: '', + title: 'adsf', + content: '', + } as NoteItem, + }; + const card3: CardItem = { + qry: 'jason3', + type: CardType.Passage, + data: null, + }; + + const cache: HashTable = {}; + let newCache = updateCache(card1, cache); + + expect(newCache[getCardItemKey(card1)].qry).toBe('jason', 'Should have added the card'); + expect(newCache[getCardItemKey(card1)].data).toBe(null, 'Should have null data'); + + newCache = updateCache(card2, newCache); + expect(newCache[getCardItemKey(card2)].qry).toBe('jason', 'Should have added the card'); + expect((newCache[getCardItemKey(card1)].data as NoteItem).title).toBe('adsf', 'Should have added the card'); + expect(Object.keys(newCache).length).toBe(1, 'Should still have only 1 item.'); + + newCache = updateCache(card3, newCache); + expect(newCache[getCardItemKey(card3)].qry).toBe('jason3', 'Should have added the card'); + expect(Object.keys(newCache).length).toBe(2, 'Should have 2 items.'); + }); + + it('removeFromCache', () => { + const card1: CardItem = { + qry: 'jason', + type: CardType.Passage, + data: null, + }; + const card2: CardItem = { + qry: 'jason', + type: CardType.Passage, + data: { + id: 'adsf', + xref: '', + title: 'adsf', + content: '', + } as NoteItem, + }; + const card3: CardItem = { + qry: 'jason3', + type: CardType.Passage, + data: null, + }; + + const cache: HashTable = {}; + let newCache = updateCache(card1, cache); + newCache = updateCache(card3, newCache); + expect(Object.keys(newCache).length).toBe(2, 'Should have 2 items.'); + + newCache = removeFromCache(card1, newCache); + expect(Object.keys(newCache).length).toBe(1, 'Should remove 1 item'); + }); +}); + describe('AppService Reducer', () => { const preState = { user: null, currentCards: [], + cardCache: {}, autocomplete: [], currentSavedPage: { queries: [], @@ -109,6 +183,8 @@ describe('AppService Reducer', () => { TestBed.configureTestingModule({}); }); + //#region Settings + it('UPDATE_CARD_MERGE_STRATEGY', () => { for (const strategy of [Overlap.None, Overlap.Equal, Overlap.Subset, Overlap.Intersect]) { const action = AppActionFactory.newUpdateCardMergeStrategy(strategy); @@ -169,6 +245,8 @@ describe('AppService Reducer', () => { ); }); + //#endregion + it('UPDATE_AUTOCOMPLETE', () => { const words = ['word1', 'word2', 'word3']; @@ -177,6 +255,8 @@ describe('AppService Reducer', () => { expect(testState.autocomplete).toEqual(words, 'Failed to update the autocomplete array'); }); + //#region Saved Pages + it('UPDATE_SAVED_PAGES', () => { const savedPages = new Storable([ { @@ -253,6 +333,7 @@ describe('AppService Reducer', () => { // 'SAVE_PAGE'; // 'GET_SAVED_PAGE'; + it('MOVE_SAVED_PAGE_CARD', () => { const page = preState.savedPages.value[1]; @@ -263,6 +344,10 @@ describe('AppService Reducer', () => { // 'ADD_CARD_TO_SAVED_PAGE'; + //#endregion + + //#region Cards + it('ADD_CARD', () => { const card1: CardItem = { qry: 'H123', @@ -415,10 +500,18 @@ describe('AppService Reducer', () => { // 'UPDATE_CARD'; // 'REMOVE_CARD'; // 'MOVE_CARD'; + + //#endregion + // 'SET_USER'; + + //#region Notes + // 'FIND_NOTES'; // 'GET_NOTE'; // 'UPDATE_NOTES'; // 'SAVE_NOTE'; // 'DELETE_NOTE'; + + //#endregion }); diff --git a/src/src/app/services/app-state-reducer.ts b/src/src/app/services/app-state-reducer.ts index 60ef08e4..7f42ad03 100644 --- a/src/src/app/services/app-state-reducer.ts +++ b/src/src/app/services/app-state-reducer.ts @@ -9,8 +9,9 @@ import { mergeCardList } from '../common/card-operations'; import { AppAction, AppActionFactory } from './app-state-actions'; import { initialState } from './app-state-initial-state'; import { SavedPage } from '../models/page-state'; -import { CardType, CardItem } from '../models/card-state'; +import { CardType, CardItem, DataReference } from '../models/card-state'; import { moveItem, moveItemUpOrDown } from '../common/array-operations'; +import { HashTable } from '../common/hashtable'; export function getNewestStorable(candidate: IStorable, incumbant: IStorable): IStorable { // if the candidate is null, then return the state. @@ -27,6 +28,27 @@ export function getNewestStorable(candidate: IStorable, incumbant: IStorab return incumbant; } +export function updateCache(card: CardItem, cardCache: HashTable): HashTable { + const cache = { ...cardCache }; + cache[getCardItemKey(card)] = card; + return cache; +} + +export function removeFromCache(card: CardItem, cardCache: HashTable): HashTable { + const cache = { ...cardCache }; + delete cache[getCardItemKey(card)]; + return cache; +} + +export function getCardItemKey(card: DataReference) { + return `${card.qry}:${card.type}`; +} + +export function getCardFromCache(ref: DataReference, cardCache: HashTable) { + const key = getCardItemKey(ref); + return cardCache[key]; +} + export function reducer(state: AppState, action: AppAction): AppState { // somtimes the state is null. lets not complain if that happens. if (state === undefined) { @@ -41,7 +63,7 @@ export function reducer(state: AppState, action: AppAction): AppState { }; } - //#region Saved Page Settings + //#region Settings case 'UPDATE_CARD_MERGE_STRATEGY': { const settings = new Storable({ @@ -57,10 +79,6 @@ export function reducer(state: AppState, action: AppAction): AppState { }); } - //#endregion - - //#region Display Settings - case 'UPDATE_SETTINGS': { const item = getNewestStorable(action.settings, state.settings); @@ -218,15 +236,15 @@ export function reducer(state: AppState, action: AppAction): AppState { const savedPages = new Storable([ ...(state.savedPages ? state.savedPages.value : []).map((o) => { if (o.id.toString() === action.pageId) { - let cards = [] as CardItem[]; + let references = [] as DataReference[]; if (state.settings.value.displaySettings.appendCardToBottom) { - cards = [...o.queries, action.card]; + references = [...o.queries, action.card]; } else { - cards = [action.card, ...o.queries]; + references = [action.card, ...o.queries]; } return { ...o, - queries: mergeCardList(cards, state.settings.value.pageSettings.mergeStrategy), + queries: mergeCardList(references, state.settings.value.pageSettings.mergeStrategy), }; } return o; @@ -265,9 +283,11 @@ export function reducer(state: AppState, action: AppAction): AppState { cards = [action.card, ...state.currentCards]; } } + return { ...state, currentCards: cards, + cardCache: updateCache(action.card, state.cardCache), }; } case 'UPDATE_CARD': { @@ -279,6 +299,7 @@ export function reducer(state: AppState, action: AppAction): AppState { } return c; }), + cardCache: updateCache(action.newCard, removeFromCache(action.oldCard, state.cardCache)), }; } case 'REMOVE_CARD': { @@ -308,6 +329,7 @@ export function reducer(state: AppState, action: AppAction): AppState { currentSavedPage, savedPages, currentCards: [...state.currentCards.filter((c) => c !== action.card)], + cardCache: removeFromCache(action.card, state.cardCache), }; } case 'MOVE_CARD': { @@ -319,6 +341,17 @@ export function reducer(state: AppState, action: AppAction): AppState { }; } + case 'UPDATE_CARDS': { + let cardCache = { ...state.cardCache }; + for (const card of action.cards) { + cardCache = updateCache(card, cardCache); + } + return { + ...state, + currentCards: action.cards, + cardCache, + }; + } //#endregion case 'SET_USER': { @@ -341,7 +374,7 @@ export function reducer(state: AppState, action: AppAction): AppState { } as CardItem; }); - let cards = [] as CardItem[]; + let cards = [] as DataReference[]; if (action.nextToItem && state.settings.value.displaySettings.insertCardNextToItem) { const idx = state.currentCards.indexOf(action.nextToItem); @@ -398,7 +431,7 @@ export function reducer(state: AppState, action: AppAction): AppState { const cards = [ ...state.currentCards.map((o) => { - const n = o.data as NoteItem; + const n = getCardFromCache(o, state.cardCache).data as NoteItem; if (n && n.id === action.note.id) { return { ...o, @@ -419,7 +452,7 @@ export function reducer(state: AppState, action: AppAction): AppState { return { ...sp, queries: sp.queries.map((o) => { - const n = o.data as NoteItem; + const n = getCardFromCache(o, state.cardCache).data as NoteItem; if (n && n.id === action.note.id) { return { ...o, @@ -449,7 +482,7 @@ export function reducer(state: AppState, action: AppAction): AppState { const cards = [ ...state.currentCards.filter((o) => { - const n = o.data as NoteItem; + const n = getCardFromCache(o, state.cardCache).data as NoteItem; return !n || n.id !== action.note.id; }), ]; @@ -461,7 +494,7 @@ export function reducer(state: AppState, action: AppAction): AppState { return { ...sp, queries: sp.queries.filter((o) => { - const n = o.data as NoteItem; + const n = getCardFromCache(o, state.cardCache).data as NoteItem; return !n || n.id !== action.note.id; }), }; diff --git a/src/src/app/services/app.service.ts b/src/src/app/services/app.service.ts index 82fe9bed..25a8840b 100644 --- a/src/src/app/services/app.service.ts +++ b/src/src/app/services/app.service.ts @@ -84,12 +84,55 @@ export class AppService extends createStateService(reducer, initialState) { //#endregion + async getCardByQuery(qry: string): Promise { + if (qry.startsWith('note:')) { + const id = qry.replace('note:', ''); + const data = this.getState().notes.value.find((o) => o.id === id); + return { + qry, + type: CardType.Note, + data, + } as CardItem; + } else if (qry.search(/[0-9]/i) === -1) { + // // its a search term. + const data = await this.getWordsFromApi(qry); + return { + qry, + type: CardType.Word, + data, + } as CardItem; + } else if (qry.search(/(H|G)[0-9]/i) !== -1) { + // its a strongs lookup + const dict = qry.substring(0, 1).search(/h/i) !== -1 ? 'heb' : 'grk'; + const strongsNumber = qry.substring(1, qry.length); + return await this.getStrongsCard(strongsNumber, dict); + } else { + // its a verse reference. + if (qry !== '') { + const myref = new BibleReference(qry.trim()); + const data = await this.getPassageFromApi(myref.section); + return { + qry, + type: CardType.Passage, + data, + } as CardItem; + } + } + } + //#region Saved Pages - getSavedPage(pageid: string) { + async getSavedPage(pageid: string) { + const page = this.getState().savedPages.value.find((o) => o.id === pageid); + const cards = []; + + for (const ref of page.queries) { + cards.push(await this.getCardByQuery(ref.qry)); + } + this.dispatch({ - type: 'GET_SAVED_PAGE', - pageId: pageid, + type: 'UPDATE_CARDS', + cards, }); }