fix issues with initial state being stored then retrieved overwriting state from previous session.

fix issue with getState() not returning the initial state.
fix issue with excluded search terms not being ignored.
This commit is contained in:
Jason Wall 2020-11-25 10:47:33 -05:00
parent d93400b911
commit ff33cdaf34
6 changed files with 300 additions and 85 deletions

View File

@ -44,7 +44,7 @@ class StateService<TState, TAction extends { type: string }> {
* derived service class.
*/
protected getState(): TState {
return this.store.getState();
return this.internalState$.value;
}
/**

View File

@ -1,14 +1,17 @@
export interface IStorable<T> {
readonly createdOn: string;
readonly type: StorableType;
readonly value: T;
}
export class Storable<T> implements IStorable<T> {
constructor(v: T) {
constructor(v: T, type: StorableType = StorableType.modified) {
this.value = v;
this.type = type;
this.createdOn = new Date().toISOString();
}
type: StorableType;
createdOn: string;
value: T;
}
@ -16,3 +19,8 @@ export class Storable<T> implements IStorable<T> {
export interface UserVersion {
version: number;
}
export enum StorableType {
initial,
modified
}

View File

@ -1,9 +1,11 @@
import { AppState } from '../models/app-state';
import { Overlap } from '../common/bible-reference';
import { StorableType } from '../common/storable';
export const initialState: AppState = {
user: null,
currentCards: {
type: StorableType.initial,
createdOn: new Date(0).toISOString(),
value: [],
},
@ -12,12 +14,14 @@ export const initialState: AppState = {
currentSavedPage: null,
savedPages: null,
notes: {
type: StorableType.initial,
createdOn: new Date(0).toISOString(),
value: [],
},
savedPagesLoaded: false,
error: null,
settings: {
type: StorableType.initial,
createdOn: new Date(0).toISOString(),
value: {
displaySettings: {

View File

@ -40,7 +40,73 @@ export class AppService extends createStateService(reducer, initialState) {
private paragraphs: HashTable<Paragraph>;
private searchIndexArray: string[];
private autocomplete: string[];
private excludedWords: Map<string, number> = new Map([
['us', 0],
['these', 0],
['her', 0],
['saith', 0],
['shalt', 0],
['let', 0],
['do', 0],
['your', 0],
['we', 0],
['no', 0],
['go', 0],
['if', 0],
['at', 0],
['an', 0],
['so', 0],
['before', 0],
['also', 0],
['on', 0],
['had', 0],
['you', 0],
['there', 0],
['then', 0],
['up', 0],
['by', 0],
['upon', 0],
['were', 0],
['are', 0],
['this', 0],
['when', 0],
['thee', 0],
['their', 0],
['ye', 0],
['will', 0],
['as', 0],
['thy', 0],
['my', 0],
['me', 0],
['have', 0],
['from', 0],
['was', 0],
['but', 0],
['which', 0],
['thou', 0],
['all', 0],
['it', 0],
['with', 0],
['them', 0],
['him', 0],
['they', 0],
['is', 0],
['be', 0],
['not', 0],
['his', 0],
['i', 0],
['shall', 0],
['a', 0],
['for', 0],
['unto', 0],
['he', 0],
['in', 0],
['to', 0],
['that', 0],
['of', 0],
['and', 0],
['the', 0],
]);
private readonly dataPath = 'assets/data';
constructor(private http: HttpClient, private localStorageService: StorageMap, private db: AngularFireDatabase) {
@ -82,6 +148,9 @@ export class AppService extends createStateService(reducer, initialState) {
async addCard(qry: string, nextToItem: CardItem = null) {
const card = await this.getCardByQuery(qry);
if (!card) {
return;
}
this.dispatch(AppActionFactory.newAddCard(card, nextToItem));
}
@ -93,6 +162,7 @@ export class AppService extends createStateService(reducer, initialState) {
}
this.dispatch(
AppActionFactory.newUpdateCards({
type: queries.type,
createdOn: queries.createdOn,
value: cards,
})
@ -103,6 +173,9 @@ export class AppService extends createStateService(reducer, initialState) {
if (qry.startsWith('note:')) {
const id = qry.replace('note:', '');
const data = this.getState().notes.value.find((o) => o.id === id);
if (!data) {
return;
}
return {
qry,
type: CardType.Note,
@ -111,6 +184,9 @@ export class AppService extends createStateService(reducer, initialState) {
} else if (qry.search(/[0-9]/i) === -1) {
// // its a search term.
const data = await this.getWordsFromApi(qry);
if (!data) {
return;
}
return {
qry,
type: CardType.Word,
@ -126,6 +202,9 @@ export class AppService extends createStateService(reducer, initialState) {
if (qry !== '') {
const myref = new BibleReference(qry.trim());
const data = await this.getPassageFromApi(myref.section);
if (!data) {
return;
}
return {
qry,
type: CardType.Passage,
@ -263,6 +342,9 @@ export class AppService extends createStateService(reducer, initialState) {
async getStrongsCard(strongsNumber: string, dict: StrongsDictionary) {
const result = await this.getStrongsFromApi(strongsNumber, dict);
if (!result) {
return;
}
const d = dict === 'grk' ? 'G' : 'H';
const card: CardItem = {
@ -558,12 +640,19 @@ export class AppService extends createStateService(reducer, initialState) {
await this.getStemWordIndex();
}
const excluded: string[] = [];
// now carry on...
const qs = this.normalizeQueryString(qry);
const results: (readonly string[])[] = [];
// Loop through each query term.
for (const q of qs) {
if (this.excludedWords.has(q)) {
excluded.push(q);
continue; // skip this word.
}
if (!this.wordToStem.has(q)) {
this.dispatch({
type: 'UPDATE_ERROR',
@ -601,6 +690,18 @@ export class AppService extends createStateService(reducer, initialState) {
// Now we need to test results. If there is more than one item in the array, we need to find the set
// that is shared by all of them. IF not, we can just return those refs.
if (results.length === 0) {
if (excluded.length > 0) {
this.dispatch({
type: 'UPDATE_ERROR',
error: {
msg: `The ${excluded.length > 1 ? 'words' : 'word'} "${excluded.reduce((prev, curr, idx, arr) => {
return `${prev}, ${curr}`;
})}" ${excluded.length > 1 ? 'have' : 'has'} been excluded from search because it is too common.`,
},
});
return;
}
this.dispatch({
type: 'UPDATE_ERROR',
error: {
@ -610,6 +711,18 @@ export class AppService extends createStateService(reducer, initialState) {
return;
}
// // in this case, let the user know, but continue on because a result was found.
// if (excluded.length > 0) {
// this.dispatch({
// type: 'UPDATE_ERROR',
// error: {
// msg: `The ${excluded.length > 1 ? 'words' : 'word'} "${excluded.reduce((prev, curr, idx, arr) => {
// return `${prev}, ${curr}`;
// })}" ${excluded.length > 1 ? 'have' : 'has'} been excluded from search because it is too common.`,
// },
// });
// }
if (results.length === 1) {
// we go down this path if only one word was searched for
return {

View File

@ -8,7 +8,7 @@ import { Settings, User } from '../models/app-state';
import { SavedPage } from '../models/page-state';
import { CardType, DataReference } from '../models/card-state';
import { StorageService } from './storage.service';
import { Storable } from '../common/storable';
import { Storable, StorableType } from '../common/storable';
import { NoteItem } from '../models/note-state';
@Injectable({
@ -39,6 +39,7 @@ export class MigrationVersion0to1 {
settings,
savedPages,
{
type: StorableType.modified,
createdOn: new Date(0).toISOString(),
value: [],
},

View File

@ -4,7 +4,7 @@ import { AngularFireDatabase, AngularFireObject } from '@angular/fire/database';
import { DataSnapshot } from '@angular/fire/database/interfaces';
import { SubscriberBase } from '../common/subscriber-base';
import { IStorable, UserVersion } from '../common/storable';
import { IStorable, StorableType, UserVersion } from '../common/storable';
import { AppService } from './app.service';
import { MigrationVersion0to1 } from './migration0to1.service';
@ -16,6 +16,36 @@ import { isNullOrUndefined } from '../common/helpers';
import { DataReference } from '../models/card-state';
import { createSelector } from 'reselect';
/**
* This class handles all the storage needs of the application. It handles both
* local and remote storage and syncing between the two.
*
* There are three components to the system that are important.
*
* 1) Listening for changes to the local data store (see the Local //#region)
*
* When data is written to the local data store, that data needs to be sent to
* the application state so the running storage reflects the local storage.
*
* The local storage is initialized in the app.component.ts constructor.
*
* 2) Listening for changes to the remote data store (see the Remote //#region)
*
* When data is written to the remote data store, that data needs to be sent to
* the application state so the running storage reflects the remote storage.
*
* The remote storage is initialized in the app.component.ts constructor once a user
* is available indicating that a connection has been established with the remote data
* store.
*
* 3) Listening for changes from the application state.
*
* When something happens in the application state, that state needs to be stored
* locally so the changes aren't lost, and remotely so the changes can be shared
* and persisten across devices and different installs.
*
* The listeners to the app state are initialized in the constructor.
*/
@Injectable({
providedIn: 'root',
})
@ -48,6 +78,7 @@ export class StorageService extends SubscriberBase {
(sync, cards) => ({
syncCardsAcrossDevices: sync,
currentCards: {
type: cards.type,
createdOn: cards.createdOn,
value: cards.value.map((o) => {
return {
@ -55,7 +86,7 @@ export class StorageService extends SubscriberBase {
type: o.type,
} as DataReference;
}),
},
} as IStorable<DataReference[]>,
})
)
);
@ -67,73 +98,6 @@ export class StorageService extends SubscriberBase {
private v0to1: MigrationVersion0to1
) {
super();
// handle remote and local storage
// when the specific items change in the state,
// store them locally and remotely, if you're logged in.
this.observeStorable('Display Settings', this.settingsState$, this.settingsPath, this.settingsRemoteObject);
this.observeStorable('Page', this.savedPagesState$, this.savedPagesPath, this.savedPagesRemoteObject);
this.observeStorable('Note', this.noteItemsState$, this.noteItemsPath, this.noteItemsRemoteObject);
// whenever the user setting or the current cards change, save the cards
// and if syncing is turned on, send them to the remote store.
this.addSubscription(
this.syncCurrentItems$.subscribe((v) => {
// set a local sync variable, so that the remote events know whether to
// accept remote state changes.
this.syncCurrentItems = v.syncCardsAcrossDevices;
// update local
this.local.set(this.cardsPath, v.currentCards).subscribe(
() => {
// nop
},
// error
() => {
// tslint:disable-next-line: quotemark
this.appService.dispatchError(`Something went wrong and the current items weren't saved. :(`);
}
);
// since you updated the local variable above, this remote update will
// get picked up by the remote subscription below.
if (v.syncCardsAcrossDevices) {
this.cardsRemoteObject.set(v.currentCards);
}
})
);
}
observeStorable<T>(
name: string,
state$: Observable<IStorable<T>>,
path: string,
remoteObject: AngularFireObject<IStorable<T>>
) {
this.addSubscription(
state$.subscribe((data) => {
if (!data) {
return;
}
// update local
this.local.set(path, data).subscribe(
() => {
// nop
},
// error
() => {
// tslint:disable-next-line: quotemark
this.appService.dispatchError(`Something went wrong and the ${name} wasn't saved. :(`);
}
);
// update remote
if (remoteObject) {
remoteObject.set(data);
}
})
);
}
//#region Remote
@ -142,8 +106,9 @@ export class StorageService extends SubscriberBase {
// do the necessary migration steps.
this.handleMigration(user);
// initialize the remote store monitoring
this.updateRemote<Settings>(
// initialize the remote store monitoring. This is where we listen for changes on the remote location.
this.monitorRemote<Settings>(
this.settingsPath,
user,
(ro) => {
@ -154,7 +119,7 @@ export class StorageService extends SubscriberBase {
}
);
this.updateRemote<SavedPage[]>(
this.monitorRemote<SavedPage[]>(
this.savedPagesPath,
user,
(ro) => {
@ -165,7 +130,7 @@ export class StorageService extends SubscriberBase {
}
);
this.updateRemote<NoteItem[]>(
this.monitorRemote<NoteItem[]>(
this.noteItemsPath,
user,
(ro) => {
@ -176,6 +141,8 @@ export class StorageService extends SubscriberBase {
}
);
// handle cards differently, because there is a setting that changes the behavior of how you handle
// new data from the remote.
this.cardsRemoteObject = this.remote.object<IStorable<DataReference[]>>(`/${this.cardsPath}/${user.uid}`);
this.addSubscription(
this.cardsRemoteObject
@ -189,7 +156,7 @@ export class StorageService extends SubscriberBase {
);
}
private updateRemote<T>(
private monitorRemote<T>(
path: string,
user: User,
setRemoteObject: (remoteObject: AngularFireObject<IStorable<T>>) => void,
@ -205,6 +172,7 @@ export class StorageService extends SubscriberBase {
.subscribe((remoteData) => {
if (!isNullOrUndefined(remoteData) && !isNullOrUndefined(remoteData.value)) {
// update the app state with remote data if it isn't null
console.log('Data recieved from remote store', remoteData);
action(remoteData);
}
})
@ -219,28 +187,149 @@ export class StorageService extends SubscriberBase {
initialize the local stores
*/
async initLocal() {
this.updateLocal<Settings>(this.settingsPath, (data) => {
this.getFromLocal<Settings>(this.settingsPath, (data) => {
this.appService.updateSettings(data);
});
this.updateLocal<SavedPage[]>(this.savedPagesPath, (data) => {
this.getFromLocal<SavedPage[]>(this.savedPagesPath, (data) => {
this.appService.updateSavedPages(data);
});
this.updateLocal<NoteItem[]>(this.noteItemsPath, (data) => {
this.getFromLocal<NoteItem[]>(this.noteItemsPath, (data) => {
this.appService.updateNotes(data);
});
this.updateLocal<DataReference[]>(this.cardsPath, (data) => {
this.getFromLocal<DataReference[]>(this.cardsPath, (data) => {
this.appService.updateCards(data);
});
// we start listening after we get the local storage, to avoid overwriting local storage with the initial app state.
this.addAppStateListeners();
}
private async updateLocal<T>(path: string, action: (storable: IStorable<T>) => void) {
private async getFromLocal<T>(path: string, action: (storable: IStorable<T>) => void) {
const hasStorable = await this.local.has(path).toPromise();
if (hasStorable) {
const storable = (await this.local.get(path).toPromise()) as IStorable<T>;
action(storable);
const localData = (await this.local.get(path).toPromise()) as IStorable<T>;
console.log('Data recieved from local store', localData);
action(localData);
}
}
private addAppStateListeners() {
// handle remote and local storage
// when the specific items change in the state,
// store them locally and remotely, if you're logged in.
this.addSubscription(
this.settingsState$.subscribe((data) => {
if (!data || data.type === StorableType.initial) {
return;
}
console.log('Data saved to local store', data);
// update local
this.local.set(this.settingsPath, data).subscribe(
() => {
// nop
},
// error
() => {
// tslint:disable-next-line: quotemark
this.appService.dispatchError(`Something went wrong and the Settings weren't saved. :(`);
}
);
// update remote
if (this.settingsRemoteObject) {
console.log('Data sent to remote store', data);
this.settingsRemoteObject.set(data);
}
})
);
this.addSubscription(
this.savedPagesState$.subscribe((data) => {
if (!data || data.type === StorableType.initial) {
return;
}
console.log('Data saved to local store', data);
// update local
this.local.set(this.savedPagesPath, data).subscribe(
() => {
// nop
},
// error
() => {
// tslint:disable-next-line: quotemark
this.appService.dispatchError(`Something went wrong and the Page wasn't saved. :(`);
}
);
// update remote
if (this.savedPagesRemoteObject) {
console.log('Data sent to remote store', data);
this.savedPagesRemoteObject.set(data);
}
})
);
this.addSubscription(
this.noteItemsState$.subscribe((data) => {
if (!data || data.type === StorableType.initial) {
return;
}
console.log('Data saved to local store', data);
// update local
this.local.set(this.noteItemsPath, data).subscribe(
() => {
// nop
},
// error
() => {
// tslint:disable-next-line: quotemark
this.appService.dispatchError(`Something went wrong and the Note wasn't saved. :(`);
}
);
// update remote
if (this.noteItemsRemoteObject) {
console.log('Data sent to remote store', data);
this.noteItemsRemoteObject.set(data);
}
})
);
// whenever the user setting or the current cards change, save the cards
// and if syncing is turned on, send them to the remote store.
this.addSubscription(
this.syncCurrentItems$.subscribe((v) => {
// set a local sync variable, so that the remote events know whether to
// accept remote state changes.
this.syncCurrentItems = v.syncCardsAcrossDevices;
if (!v.currentCards || v.currentCards.type === StorableType.initial) {
return;
}
// update local
this.local.set(this.cardsPath, v.currentCards).subscribe(
() => {
// nop
},
// error
() => {
// tslint:disable-next-line: quotemark
this.appService.dispatchError(`Something went wrong and the current cards weren't saved. :(`);
}
);
// since you updated the local variable above, this remote update will
// get picked up by the remote subscription below.
if (v.syncCardsAcrossDevices) {
this.cardsRemoteObject.set(v.currentCards);
}
})
);
}
//#endregion
//#region Migrations