Last active
April 16, 2021 14:24
-
-
Save jimitndiaye/3d655a42cb8bc83994f5490c48da5fdd to your computer and use it in GitHub Desktop.
RXJS, @ngrx/store - Memoizing a tree search
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Map, Record, List } from 'immutable'; | |
export interface FolderTree { | |
id: string; | |
name: string; | |
subFolders: FolderTree[]; | |
dialogs: DialogSummary[]; | |
} | |
export interface DialogSummary { | |
id: string; | |
name: string; | |
folderId?: string; | |
keywords?: Array<string> | Set<string>; | |
lastModified?: Date; | |
published: boolean; | |
} | |
export interface DialogFolder { | |
id: string; | |
name: string; | |
parentId?: string; | |
lastModified?: Date; | |
} | |
export interface FolderState extends DialogFolder, Map<string, any> { | |
} | |
export interface DialogState extends DialogSummary, Map<string, any> { | |
} | |
export enum StorageErrorCode { | |
None, | |
NotFound, | |
NameMustNotBeEmpty, | |
DuplicateName, | |
ServerError | |
} | |
export interface ErrorState extends Map<string, any> { | |
action: Action; | |
errorCode: StorageErrorCode; | |
errorDetail: any; | |
} | |
export interface EntityState<T> extends Map<string, any> { | |
ids: List<string>; | |
entities: Map<string, T>; | |
} | |
export interface Folders extends EntityState<FolderState> { } | |
export interface Dialogs extends EntityState<DialogState> { } | |
export interface ExplorerState extends Map<string, any> { | |
folders: Folders; | |
dialogs: Dialogs; | |
error?: ErrorState; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import '@ngrx/core/add/operator/select'; | |
import { Observable } from 'rxjs'; | |
import 'rxjs/add/observable/combineLatest'; | |
import 'rxjs/add/operator/map'; | |
import 'rxjs/add/operator/concatMap'; | |
import 'rxjs/add/operator/let'; | |
import 'rxjs/add/operator/filter'; | |
import 'rxjs/add/operator/expand'; | |
import { isBlank, isPresent, isString } from '@angular/core/src/facade/lang'; | |
import { List, Map } from 'immutable'; | |
import { compose } from '@ngrx/core/compose'; | |
import { | |
Dialog, | |
DialogSummary, | |
DialogFolder, | |
EntityState, | |
ExplorerState, | |
FolderState, | |
DialogState, | |
FolderContents, | |
FolderTree | |
} from './explorer.model'; | |
import { FolderStateRecord } from './explorer.reducer'; | |
export function getFolderEntities() { | |
return (state$: Observable<ExplorerState>) => state$ | |
.select(state => state.folders.entities); | |
} | |
export function getFolderIds() { | |
return (state$: Observable<ExplorerState>) => state$ | |
.select(state => state.folders.ids.toArray()); | |
} | |
export function getDialogIds() { | |
return (state$: Observable<ExplorerState>) => state$ | |
.select(state => state.dialogs.ids.toArray()); | |
} | |
export function getDialogEntities() { | |
return (state$: Observable<ExplorerState>) => state$ | |
.select(state => state.dialogs.entities); | |
} | |
export function getFolder(folderId: string) { | |
console.debug('Getting folder...', folderId); | |
return (state$: Observable<ExplorerState>) => state$ | |
.select(state => { | |
return isString(folderId) && folderId.length > 0 | |
? state.folders.entities.get(folderId) | |
: new FolderStateRecord({ | |
name: '/', | |
lastModified: new Date() | |
}) as FolderState; | |
}); | |
} | |
export function getFolders(folderIds: string[]) { | |
return (state$: Observable<ExplorerState>) => state$ | |
.let(getFolderEntities()) | |
.map(entities => folderIds.map(id => entities.get(id))); | |
} | |
export function hasFolder(folderId: string) { | |
return (state$: Observable<ExplorerState>) => state$ | |
.select(state => state.folders.ids.includes(folderId)); | |
} | |
export function getContainedFolders(folderId: string) { | |
return (state$: Observable<ExplorerState>) => state$ | |
.let(getFolderIds()) | |
.flatMap(ids => ids) | |
.concatMap(id => state$.let(getFolder(id))) | |
.filter(folder => folder.parentId === folderId) | |
.toArray(); | |
} | |
export function getDialog(dialogId: string) { | |
return (state$: Observable<ExplorerState>) => state$ | |
.select(state => state.dialogs.entities.get(dialogId)); | |
} | |
export function getContainedDialogs(folderId: string) { | |
return (state$: Observable<ExplorerState>) => state$ | |
.let(getDialogIds()) | |
.flatMap(ids => ids) | |
.concatMap(id => state$.let(getDialog(id))) | |
.filter(dialog => dialog.folderId === folderId) | |
.toArray(); | |
} | |
export function getFolderContents(folderId: string) { | |
return (state$: Observable<ExplorerState>) => | |
Observable.combineLatest( | |
state$.let(getContainedFolders(folderId)), | |
state$.let(getContainedDialogs(folderId)), | |
(folders, dialogs) => Object.assign({}, { folders, dialogs }) as FolderContents | |
); | |
} | |
export function getFolderPath(folderId: string) { | |
return (state$: Observable<ExplorerState>) => state$ | |
.let(getFolder(folderId)) | |
.expand(folder => folder.parentId | |
? state$.let(getFolder(folder.parentId)) | |
: Observable.empty<FolderState>()) | |
.toArray() | |
.map(folders => folders.map(folder => folder.name).reverse()) | |
.map(folderNames => '/' + folderNames.join('/')); | |
} | |
export function getDialogPath(dialogId: string) { | |
return (state$: Observable<ExplorerState>) => | |
state$.let(getDialog(dialogId)) | |
.concatMap(dialog => | |
state$.let(getFolderPath(dialog.id)) | |
.map(path => path + '/' + dialog.name)); | |
} | |
export function getFolderTree(folderId: string) | |
: (state$: Observable<ExplorerState>) => Observable<FolderTree> { | |
return (state$: Observable<ExplorerState>) => state$ | |
.let(getFolder(folderId)) | |
.concatMap(folder => | |
Observable.combineLatest( | |
state$.let(getContainedFolders(folderId)) | |
.flatMap(subFolders => subFolders.map(sf => sf.id)) | |
.concatMap(id => state$.let(getFolderTree(id))) | |
.toArray(), | |
state$.let(getContainedDialogs(folderId)), | |
(folders: FolderTree[], dialogs: DialogSummary[]) => Object.assign({}, { | |
id: folder.id, | |
name: folder.name, | |
subFolders: folders.sort((a, b) => a.name.localeCompare(b.name)), | |
dialogs: dialogs.map(dialog => dialog as DialogSummary) | |
.sort((a, b) => a.name.localeCompare(b.name)) | |
}) as FolderTree | |
)); | |
} | |
export function getSearchResults2(searchText: string = '', folderId = null) | |
: (state$: Observable<ExplorerState>) => Observable<FolderTree> { | |
console.debug('Searching folder tree', { searchText, folderId }); | |
const isMatch = (text: string) => | |
!!text && text.search(new RegExp(searchText, 'i')) >= 0; | |
return (state$: Observable<ExplorerState>) => | |
Observable.combineLatest( | |
state$.let(getFolder(folderId)), | |
state$.let(getContainedFolders(folderId)) | |
.flatMap(subFolders => subFolders.map(sf => sf.id)) | |
.flatMap(id => state$.let(getSearchResults2(searchText, id))) | |
.toArray(), | |
state$.let(getContainedDialogs(folderId)), | |
(folder: FolderState, folders: FolderTree[], dialogs: DialogSummary[]) => { | |
console.debug('Search complete. constructing tree...', { | |
id: folder.id, | |
name: folder.name, | |
subFolders: folders, | |
dialogs | |
}); | |
return Object.assign({}, { | |
id: folder.id, | |
name: folder.name, | |
subFolders: folders | |
.filter(subFolder => | |
subFolder.dialogs.length > 0 || isMatch(subFolder.name)) | |
.sort((a, b) => a.name.localeCompare(b.name)), | |
dialogs: dialogs | |
.map(dialog => dialog as DialogSummary) | |
.filter(dialog => | |
isMatch(folder.name) | |
|| isMatch(dialog.name)) | |
.sort((a, b) => a.name.localeCompare(b.name)) | |
}) as FolderTree; | |
} | |
); | |
} | |
export function getSearchResults(searchText: string = '') { | |
return (state$: Observable<ExplorerState>) => | |
Observable.combineLatest( | |
state$.let(getFolder(undefined)), | |
state$.let(getFolderEntities()), | |
state$.let(getDialogEntities()), | |
(root, folders, dialogs) => | |
searchFolder( | |
root, | |
id => folders ? folders.get(id) : null, | |
id => folders ? folders.filter(f => f.parentId === id).toArray() : null, | |
id => dialogs ? dialogs.filter(d => d.folderId === id).toArray() : null, | |
searchText | |
) | |
); | |
} | |
function searchFolder( | |
folder: FolderState, | |
getFolder: (id: string) => FolderState, | |
getSubFolders: (id: string) => FolderState[], | |
getSubDialogs: (id: string) => DialogSummary[], | |
searchText: string | |
): FolderTree { | |
console.log('searching folder', folder ? folder.toJS() : folder); | |
const {id, name } = folder; | |
const isMatch = (text: string) => !!text && text.toLowerCase().indexOf(searchText) > -1; | |
return { | |
id, | |
name, | |
subFolders: getSubFolders(folder.id) | |
.map(subFolder => searchFolder( | |
subFolder, | |
getFolder, | |
getSubFolders, | |
getSubDialogs, | |
searchText)) | |
.filter(subFolder => subFolder && (!!subFolder.dialogs.length || isMatch(subFolder.name))), | |
dialogs: getSubDialogs(id) | |
.filter(dialog => dialog && (isMatch(folder.name) || isMatch(dialog.name))) | |
} as FolderTree; | |
} | |
// export const searchFolder = (folderId: string, searchText: string) => | |
// (state$: Observable<ExplorerState>) => state$ | |
// .let(getFolderContents(folderId)) | |
// .map(contents => Object.assign({}, { | |
// folders: contents.folders.filter(folder => | |
// folder.name.search(new RegExp(searchText, 'i')) >= 0) | |
// ) | |
// })) | |
// export function getSearchResults() { | |
// return (state$: Observable<ExplorerState>) => | |
// Observable.combineLatest( | |
// state$.select(state => state.get('searchText') as string || ''), | |
// state$.select(state => state.get('currentDialog') as string || ''), | |
// state$.select(state => | |
// state.getIn(['entities', 'folders']) as Map<string, FolderState>) | |
// .filter(folders => !!folders), | |
// state$.select(state => | |
// state.getIn(['entities', 'dialogs']) as Map<string, DialogState>) | |
// .filter(dialogs => dialogs !== null), | |
// (searchText, currentDialog, folders, dialogs) => | |
// matchesSearchText( | |
// folders.find(folder => folder.isRoot), | |
// id => folders ? folders.get(id) : null, | |
// id => dialogs ? dialogs.get(id) : null, | |
// id => id === currentDialog, | |
// searchText | |
// ) | |
// ); | |
// } | |
function matchesSearchText( | |
folder: FolderState, | |
getSubFolders: (folderId: string) => FolderState[], | |
getDialogs: (folderId: string) => DialogState[], | |
searchText: string): boolean { | |
const {id, name} = folder; | |
const isMatch = (text: string) => !!text && text.search(new RegExp(searchText, 'i')) >= 0; | |
return isMatch(name) | |
|| getDialogs(folder.id).some(dialog => isMatch(dialog.name)) | |
|| getSubFolders(folder.id).some(subFolder => | |
isMatch(subFolder.name) | |
|| matchesSearchText(subFolder, getSubFolders, getDialogs, searchText)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment