import {throttle} from "throttle-debounce" import {ensurePlural, identity} from "./Tools" export type Invalidator = (value?: T) => void export type Subscriber = (value: T) => void type Derivable = IDerivable | IDerivable[] type Unsubscriber = () => void type R = Record type M = Map export interface ISubscribable { subscribe(this: void, run: Subscriber, invalidate?: Invalidator): Unsubscriber } export interface ISettable { set: (xs: T) => void } export interface IDerivable extends ISubscribable { get: () => T } export interface IReadable extends IDerivable { derived: (f: (v: T) => U) => IReadable throttle(t: number): IReadable } export interface IWritable extends IReadable { set: (xs: T) => void } export class Writable implements IWritable { value: T subs: Subscriber[] = [] constructor(defaultValue: T, t?: number) { this.value = defaultValue if (t) { this.notify = throttle(t, this.notify) } } notify = () => { for (const sub of this.subs) { sub(this.value) } } get() { return this.value } set(newValue: T) { this.value = newValue this.notify() } update(f: (v: T) => T) { this.set(f(this.value)) } subscribe(f: Subscriber) { this.subs.push(f) f(this.value) return () => { this.subs.splice(this.subs.findIndex(x => x === f), 1) } } derived(f: (v: T) => U): Derived { return new Derived(this, f) } throttle = (t: number): Derived => { return new Derived(this, identity, t) } } export class Derived implements IReadable { callerSubs: Subscriber[] = [] mySubs: Unsubscriber[] = [] stores: Derivable getValue: (values: any) => T latestValue: T | undefined constructor(stores: Derivable, getValue: (values: any) => T, t = 0) { this.stores = stores this.getValue = getValue if (t) { this.notify = throttle(t, this.notify) } } notify = () => { this.latestValue = undefined this.callerSubs.forEach(f => f(this.get())) } getInput() { if (Array.isArray(this.stores)) { return this.stores.map(s => s.get()) } else { return this.stores.get() } } get = (): T => { // Recalculate if we're not subscribed, because we won't get notified when deps change if (this.latestValue === undefined || this.mySubs.length === 0) { this.latestValue = this.getValue(this.getInput()) } return this.latestValue } subscribe(f: Subscriber) { if (this.callerSubs.length === 0) { for (const s of ensurePlural(this.stores)) { this.mySubs.push(s.subscribe(this.notify)) } } this.callerSubs.push(f) f(this.get()) return () => { this.callerSubs.splice(this.callerSubs.findIndex(x => x === f), 1) if (this.callerSubs.length === 0) { for (const unsub of this.mySubs.splice(0)) { unsub() } } } } derived(f: (v: T) => U): IReadable { return new Derived(this, f) as IReadable } throttle = (t: number): IReadable => { return new Derived(this, identity, t) } } export class Key implements IReadable { readonly pk: string readonly key: string base: Writable> store: IReadable constructor(base: Writable>, pk: string, key: string) { if (!(base.get() instanceof Map)) { throw new Error("`key` can only be used on map collections") } this.pk = pk this.key = key this.base = base this.store = base.derived(m => m.get(key) as T) } get = () => this.base.get().get(this.key) as T subscribe = (f: Subscriber) => this.store.subscribe(f) derived = (f: (v: T) => U) => this.store.derived(f) throttle = (t: number) => this.store.throttle(t) exists = () => this.base.get().has(this.key) update = (f: (v: T) => T) => this.base.update((m: M) => { if (!this.key) { throw new Error(`Cannot set key: "${this.key}"`) } // Make sure the pk always get set on the record const {pk, key} = this const oldValue = {...m.get(key), [pk]: key} as T const newValue = {...f(oldValue), [pk]: key} m.set(this.key, newValue) return m }) set = (v: T) => this.update(() => v) merge = (d: Partial) => this.update(v => ({...v, ...d})) remove = () => this.base.update(m => { m.delete(this.key) return m }) pop = () => { const v = this.get() this.remove() return v } } export class DerivedKey implements IReadable { readonly pk: string readonly key: string base: IReadable> store: IReadable constructor(base: IReadable>, pk: string, key: string) { if (!(base.get() instanceof Map)) { throw new Error("`key` can only be used on map collections") } this.pk = pk this.key = key this.base = base this.store = base.derived(m => m.get(key) as T) } get = () => this.base.get().get(this.key) as T subscribe = (f: Subscriber) => this.store.subscribe(f) derived = (f: (v: T) => U) => this.store.derived(f) throttle = (t: number) => this.store.throttle(t) exists = () => this.base.get().has(this.key) } export class Collection implements IReadable { readonly pk: string readonly mapStore: Writable> readonly listStore: IReadable constructor(pk: string, t?: number) { this.pk = pk this.mapStore = writable(new Map()) this.listStore = this.mapStore.derived((m: M) => Array.from(m.values())) if (t) { this.mapStore.notify = throttle(t, this.mapStore.notify) } } get = () => this.listStore.get() getMap = () => this.mapStore.get() subscribe = (f: Subscriber) => this.listStore.subscribe(f) derived = (f: (v: T[]) => U) => this.listStore.derived(f) throttle = (t: number) => this.listStore.throttle(t) key = (k: string) => new Key(this.mapStore, this.pk, k) set = (xs: T[]) => { const m = new Map() for (const x of xs) { if (!x) { console.error("Empty value passed to collection store") } else if (!x[this.pk]) { console.error(`Value with empty ${this.pk} passed to collection store`, x) } else { m.set(x[this.pk], x) } } this.mapStore.set(m) } update = (f: (v: T[]) => T[]) => this.set(f(this.get())) updateAsync = async (f: (v: T[]) => Promise) => this.set(await f(this.get())) reject = (f: (v: T) => boolean) => this.update((xs: T[]) => xs.filter(x => !f(x))) filter = (f: (v: T) => boolean) => this.update((xs: T[]) => xs.filter(f)) map = (f: (v: T) => T) => this.update((xs: T[]) => xs.map(f)) } export class DerivedCollection implements IReadable { readonly listStore: Derived readonly mapStore: IReadable> constructor( readonly pk: string, stores: Derivable, getValue: (values: any) => T[], t = 0, ) { this.listStore = new Derived(stores, getValue, t) this.mapStore = new Derived(this.listStore, xs => new Map(xs.map((x: T) => [x[pk], x]))) } get = () => this.listStore.get() getMap = () => this.mapStore.get() subscribe = (f: Subscriber) => this.listStore.subscribe(f) derived = (f: (v: T[]) => U) => this.listStore.derived(f) throttle = (t: number) => this.listStore.throttle(t) key = (k: string) => new DerivedKey(this.mapStore, this.pk, k) } export const writable = (v: T) => new Writable(v) export const derived = (stores: Derivable, getValue: (values: any) => T) => new Derived(stores, getValue) export const readable = (v: T) => derived(new Writable(v), identity) as IReadable export const derivedCollection = ( pk: string, stores: Derivable, getValue: (values: any) => T[], ) => new DerivedCollection(pk, stores, getValue) export const key = (base: Writable>, pk: string, key: string) => new Key(base, pk, key) export const collection = (pk: string) => new Collection(pk) export const asReadable = (store: IDerivable) => { return { ...store, derived: (f: (v: T) => U) => new Derived(store, f), throttle: (t: number) => new Derived(store, identity, t), } }