import {throttle} from "throttle-debounce" import {derived} from "svelte/store" import type {Readable, Writable} from "svelte/store" import {identity, batch, partition, first} from "@welshman/lib" import type {Repository} from "@welshman/util" import {matchFilters, getIdAndAddress, getIdFilters} from "@welshman/util" import type {Filter, ExtensibleTrustedEvent} from "@welshman/util" export const getter = (store: Readable) => { let value: T store.subscribe((newValue: T) => { value = newValue }) return () => value } type Stop = () => void type Sub = (x: T) => void type Start = (set: Sub) => Stop export const custom = (start: Start, opts: {throttle?: number} = {}) => { const subs: Sub[] = [] let value: T let stop: () => void return { subscribe: (sub: Sub) => { if (opts.throttle) { sub = throttle(opts.throttle, sub) } if (subs.length === 0) { stop = start((newValue: T) => { for (const sub of subs) { sub(newValue) } value = newValue }) } subs.push(sub) sub(value) return () => { subs.splice( subs.findIndex(s => s === sub), 1, ) if (subs.length === 0) { stop() } } }, } } export function withGetter(store: Writable): Writable & {get: () => T} export function withGetter(store: Readable): Readable & {get: () => T} export function withGetter(store: Readable | Writable) { return {...store, get: getter(store)} } export const throttled = (delay: number, store: Readable) => custom(set => store.subscribe(throttle(delay, set))) export const createEventStore = (repository: Repository) => { let subs: Sub[] = [] const onUpdate = throttle(300, () => { const $events = repository.dump() for (const sub of subs) { sub($events) } }) return { get: () => repository.dump(), set: (events: ExtensibleTrustedEvent[]) => repository.load(events), subscribe: (f: Sub) => { f(repository.dump()) subs.push(f) if (subs.length === 1) { repository.on("update", onUpdate) } return () => { subs = subs.filter(x => x !== f) if (subs.length === 0) { repository.off("update", onUpdate) } } }, } } export const deriveEventsMapped = ({ filters, repository, eventToItem, itemToEvent, includeDeleted = false, }: { filters: Filter[] repository: Repository, eventToItem: (event: ExtensibleTrustedEvent) => T itemToEvent: (item: T) => ExtensibleTrustedEvent includeDeleted?: boolean }) => custom(setter => { let data = repository.query(filters, {includeDeleted}).map(eventToItem).filter(identity) setter(data) const onUpdate = batch(300, (updates: {added: ExtensibleTrustedEvent[]; removed: Set}[]) => { const removed = new Set() const added = new Map() // Apply updates in order for (const update of updates) { for (const event of update.added.values()) { added.set(event.id, event) } for (const id of update.removed) { removed.add(id) added.delete(id) } } let dirty = false for (const event of added.values()) { if (matchFilters(filters, event)) { const item = eventToItem(event) if (item) { dirty = true data.push(item) } } } if (!includeDeleted && removed.size > 0) { const [deleted, ok] = partition( (item: T) => getIdAndAddress(itemToEvent(item)).some((id: string) => removed.has(id)), data, ) if (deleted.length > 0) { dirty = true data = ok } } if (dirty) { setter(data) } }) repository.on("update", onUpdate) return () => repository.off("update", onUpdate) }) export const deriveEvents = (repository: Repository, opts: {filters: Filter[]; includeDeleted?: boolean}) => deriveEventsMapped({ ...opts, repository, eventToItem: identity, itemToEvent: identity, }) export const deriveEvent = (repository: Repository, idOrAddress: string) => derived( deriveEvents(repository, { filters: getIdFilters([idOrAddress]), includeDeleted: true, }), first ) export const deriveIsDeletedByAddress = (repository: Repository, event: ExtensibleTrustedEvent) => custom(setter => { setter(repository.isDeletedByAddress(event)) const onUpdate = batch(300, () => setter(repository.isDeletedByAddress(event))) repository.on("update", onUpdate) return () => repository.off("update", onUpdate) })