Skip to content

Instantly share code, notes, and snippets.

@jimitndiaye
Last active April 16, 2021 14:24
Show Gist options
  • Save jimitndiaye/3d655a42cb8bc83994f5490c48da5fdd to your computer and use it in GitHub Desktop.
Save jimitndiaye/3d655a42cb8bc83994f5490c48da5fdd to your computer and use it in GitHub Desktop.
RXJS, @ngrx/store - Memoizing a tree search
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;
}
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