Skip to content

Instantly share code, notes, and snippets.

@MikeRyanDev
Created April 8, 2019 20:00
Show Gist options
  • Save MikeRyanDev/6cabbf3adee62fe0d052be5f4af5950f to your computer and use it in GitHub Desktop.
Save MikeRyanDev/6cabbf3adee62fe0d052be5f4af5950f to your computer and use it in GitHub Desktop.
import { Observable, Observer } from 'rxjs';
import { createSelector, MemoizedSelector } from '@ngrx/store';
import { createEntityAdapter } from '@ngrx/entity';
export interface View<T> extends Observable<T> {
isView: true;
release(): void;
}
export interface Value<T> extends View<T> {
update(updater: (previousValue: T) => T): void;
update(newValue: T): void;
}
type Dict<T, Key extends string | number> = Key extends number
? { [key: number]: T }
: { [key: string]: T };
type Update<T, Key extends string | number> = { id: Key; changes: Partial<T> };
export interface Collection<T, Key extends string | number> extends View<T[]> {
addOne(entity: T): void;
addMany(entities: T[]): void;
addAll(entities: T[]): void;
updateOne(update: Update<T, Key>): void;
updateMany(updates: Update<T, Key>[]): void;
upsertOne(entity: T): void;
upsertMany(entities: T[]): void;
removeOne(id: Key): void;
removeMany(ids: Key[]): void;
removeAll(): void;
viewIds: View<Key[]>;
viewDict: View<Dict<T, Key>>;
viewTotal: View<number>;
}
const state: { [ref: number]: any } = {};
const callbacks = new Set<() => void>();
const selectorMap = new WeakMap<View<any>, (state: any) => any>();
let nextRef: number = 0;
let locked: boolean = false;
function emitStateChange() {
if (locked) return;
for (const callback of callbacks) {
callback();
}
}
function lock() {
locked = true;
}
function unlock() {
locked = false;
}
function observeState<T>(selector: (state: any) => T): Observable<T> {
return new Observable((observer: Observer<T>) => {
let lastValue = selector(state);
observer.next(lastValue);
function onStateChange() {
const nextValue = selector(state);
if (nextValue === lastValue) return;
observer.next(nextValue);
lastValue = nextValue;
}
callbacks.add(onStateChange);
return () => callbacks.delete(onStateChange);
});
}
function makeView<T>(selector: MemoizedSelector<any, T>): View<T> {
const source$ = observeState(selector);
function release() {
selector.release();
}
const view$: View<any> = Object.assign(source$, { release, isView: true as true });
selectorMap.set(view$, selector);
return view$;
}
export function updateMany(update: () => void): void {
if (locked) return update();
lock();
update();
unlock();
emitStateChange();
}
export function value<T>(initialValue: T): Value<T> {
let ref = ++nextRef;
let released: boolean = false;
state[ref] = initialValue;
const selector = (state: any) => {
if (released) throw new Error('Cannot access a released value');
return state[ref];
};
const source$ = observeState(selector);
function release() {
delete state[ref];
released = true;
}
function update(value: T): void;
function update(updater: (oldValue: T) => T): void;
function update(valueOrUpdater: T | ((oldValue: T) => T)) {
state[ref] =
valueOrUpdater instanceof Function
? valueOrUpdater(selector(state))
: valueOrUpdater;
emitStateChange();
}
const value$ = Object.assign(source$, { release, update, isView: true as true });
selectorMap.set(value$, selector);
return value$;
}
export function view<T1, R>(t1: View<T1>, projector: (t1: T1) => R): View<R>;
export function view<T1, T2, R>(
t1: View<T1>,
t2: View<T2>,
projector: (t1: T1, t2: T2) => R,
): View<R>;
export function view<T1, T2, T3, R>(
t1: View<T1>,
t2: View<T2>,
t3: View<T3>,
projector: (t1: T1, t2: T2, t3: T3) => R,
): View<R>;
export function view<T1, T2, T3, T4, R>(
t1: View<T1>,
t2: View<T2>,
t3: View<T3>,
t4: View<T4>,
projector: (t1: T1, t2: T2, t3: T3, t4: T4) => R,
): View<R>;
export function view<T1, T2, T3, T4, T5, R>(
t1: View<T1>,
t2: View<T2>,
t3: View<T3>,
t4: View<T4>,
t5: View<T5>,
projector: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R,
): View<R>;
export function view<T1, T2, T3, T4, T5, T6, R>(
t1: View<T1>,
t2: View<T2>,
t3: View<T3>,
t4: View<T4>,
t5: View<T5>,
t6: View<T6>,
projector: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => R,
): View<R>;
export function view<T1, T2, T3, T4, T5, T6, T7, R>(
t1: View<T1>,
t2: View<T2>,
t3: View<T3>,
t4: View<T4>,
t5: View<T5>,
t6: View<T6>,
t7: View<T7>,
projector: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7) => R,
): View<R>;
export function view<T1, T2, T3, T4, T5, T6, T7, T8, R>(
t1: View<T1>,
t2: View<T2>,
t3: View<T3>,
t4: View<T4>,
t5: View<T5>,
t6: View<T6>,
t7: View<T7>,
t8: View<T8>,
projector: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8) => R,
): View<R>;
export function view(...args: any[]): View<any> {
const views: View<any>[] = args.slice(0, args.length - 1);
const projector: (...args: any[]) => any = args[args.length - 1];
const viewSelectors = views.map(view => selectorMap.get(view)!);
const selector: MemoizedSelector<any, any> = (createSelector as any)(
...viewSelectors,
projector,
);
return makeView(selector);
}
export function collection<T extends { id: string }>(
initialValues: T[],
options?: { sortComparer?: (a: T, b: T) => number },
): Collection<T, string>;
export function collection<T extends { id: number }>(
initialValues: T[],
options?: { sortComparer?: (a: T, b: T) => number },
): Collection<T, number>;
export function collection<T, V extends string | number>(
initialValues: T[],
options?: { selectId: (entity: T) => V; sortComparer?: (a: T, b: T) => number },
): Collection<T, V>;
export function collection<T, V extends string | number>(
initialValues: T[],
{
selectId = (item: any) => item.id,
sortComparer,
}: { selectId?: (entity: T) => V; sortComparer?: (a: T, b: T) => number } = {},
): Collection<T, V> {
const a = createEntityAdapter<T>({ selectId: selectId as any, sortComparer });
const initialValue = a.addAll(initialValues, a.getInitialState());
const value$ = value(initialValue);
const u = value$.update;
const addOne = (entity: T) => u(state => a.addOne(entity, state));
const addMany = (entities: T[]) => u(state => a.addMany(entities, state));
const addAll = (entities: T[]) => u(state => a.addAll(entities, state));
const updateOne = (update: Update<T, V>) =>
u(state => a.updateOne(update as any, state));
const updateMany = (updates: Update<T, V>[]) =>
u(state => a.updateMany(updates as any, state));
const upsertOne = (entity: T) => u(state => a.upsertOne(entity, state));
const upsertMany = (entities: T[]) => u(state => a.upsertMany(entities, state));
const removeOne = (id: V) => u(state => a.removeOne(id as any, state));
const removeMany = (ids: V[]) => u(state => a.removeMany(ids as any, state));
const removeAll = () => u(state => a.removeAll(state));
const rootSelector = selectorMap.get(value$)!;
const { selectAll, selectEntities, selectIds, selectTotal } = a.getSelectors(
rootSelector,
);
const viewIds = makeView<V[]>(selectIds as any);
const viewDict = makeView<Dict<T, V>>(selectEntities as any);
const viewTotal = makeView<number>(selectTotal as any);
const viewAll = makeView<T[]>(selectAll as any);
function release() {
viewIds.release();
viewDict.release();
viewTotal.release();
viewAll.release();
value$.release();
}
return Object.assign(viewAll, {
addOne,
addMany,
addAll,
updateOne,
updateMany,
upsertOne,
upsertMany,
removeOne,
removeMany,
removeAll,
viewIds,
viewDict,
viewTotal,
release,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment