import {throttle} from "throttle-debounce" import {ensurePlural, identity} from "./Tools" type Invalidator = (value?: T) => void type Derivable = Readable | Readable[] type Subscriber = (value: T) => void type Unsubscriber = () => void type R = Record type M = Map export interface Readable { get: () => T subscribe(this: void, run: Subscriber, invalidate?: Invalidator): Unsubscriber derived: (f: (v: T) => U) => Readable throttle(t: number): Readable } export class Writable implements Readable { private value: T private 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)) } async updateAsync(f: (v: T) => Promise) { this.set(await 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 Readable { private callerSubs: Subscriber[] = [] private mySubs: Unsubscriber[] = [] private stores: Derivable private getValue: (values: any) => T private 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): Readable { return new Derived(this, f) as Readable } throttle = (t: number): Readable => { return new Derived(this, identity, t) } } export class Key implements Readable { readonly pk: string readonly key: string private base: Writable> private store: Readable 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 Readable { readonly pk: string readonly key: string private base: Readable> private store: Readable constructor(base: Readable>, 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 Readable { readonly pk: string readonly mapStore: Writable> readonly listStore: Readable 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 Readable { readonly listStore: Derived readonly mapStore: Readable> 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 Readable 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)