import {derived, writable} from "svelte/store" import type {Readable, Writable, Subscriber, Unsubscriber} from "svelte/store" import { identity, throttle, ensurePlural, getJson, setJson, batch, partition, first, } from "@welshman/lib" import type {Maybe} from "@welshman/lib" import type {Repository} from "@welshman/util" import {matchFilters, getIdAndAddress, getIdFilters} from "@welshman/util" import type {Filter, TrustedEvent} from "@welshman/util" // Sync with localstorage export const synced = (key: string, defaultValue: T) => { const init = getJson(key) const store = writable(init === null ? defaultValue : init) store.subscribe((value: T) => setJson(key, value)) return store } // Getters export const getter = (store: Readable) => { let value: T store.subscribe((newValue: T) => { value = newValue }) return () => value } export type WritableWithGetter = Writable & {get: () => T} export type ReadableWithGetter = Readable & {get: () => T} export function withGetter(store: Writable): WritableWithGetter export function withGetter(store: Readable): ReadableWithGetter export function withGetter(store: Readable | Writable) { return {...store, get: getter(store)} } // Throttle export const throttled = >(delay: number, store: S) => { if (delay) { const {subscribe} = store store = {...store, subscribe: (f: Subscriber) => subscribe(throttle(delay, f))} } return store } // Custom store type Start = (set: Subscriber) => Unsubscriber export type CustomStoreOpts = { throttle?: number set?: (x: T) => void } export const custom = ( start: Start, opts: CustomStoreOpts = {}, ): WritableWithGetter => { const subs: Subscriber[] = [] let value: T let stop: () => void const set = (newValue: T) => { for (const sub of subs) { sub(newValue) } value = newValue } return { get: () => value, set: (newValue: T) => { set(newValue) opts.set?.(newValue) }, update: (f: (value: T) => T) => { const newValue = f(value) set(newValue) opts.set?.(newValue) }, subscribe: (sub: Subscriber) => { if (opts.throttle) { sub = throttle(opts.throttle, sub) } if (subs.length === 0) { stop = start(set) } subs.push(sub) sub(value) return () => { subs.splice( subs.findIndex(s => s === sub), 1, ) if (subs.length === 0) { stop() } } }, } } // Simple adapter export const adapter = ({ store, forward, backward, }: { store: Writable forward: (x: Source) => Target backward: (x: Target) => Source }) => ({ ...derived(store, forward), set: (x: Target) => store.set(backward(x)), update: (f: (x: Target) => Target) => store.update((x: Source) => backward(f(forward(x)))), }) // Event related stores export type DeriveEventsMappedOptions = { filters: Filter[] eventToItem: (event: TrustedEvent) => Maybe> itemToEvent: (item: T) => TrustedEvent throttle?: number includeDeleted?: boolean } export const deriveEventsMapped = ( repository: Repository, { filters, eventToItem, itemToEvent, throttle = 0, includeDeleted = false, }: DeriveEventsMappedOptions, ) => custom( setter => { let data: T[] = [] const deferred = new Set() const defer = (event: TrustedEvent, promise: Promise) => { deferred.add(event.id) void promise.then(items => { if (deferred.has(event.id)) { deferred.delete(event.id) for (const item of ensurePlural(items)) { data.push(item) } setter(data) } }) } for (const event of repository.query(filters, {includeDeleted})) { const items = eventToItem(event) if (!items) { continue } if (items instanceof Promise) { defer(event, items) } else { for (const item of ensurePlural(items)) { data.push(item) } } } setter(data) const onUpdate = batch(300, (updates: {added: TrustedEvent[]; 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) removed.delete(event.id) } for (const id of update.removed) { removed.add(id) added.delete(id) deferred.delete(id) } } let dirty = false for (const event of added.values()) { if (matchFilters(filters, event)) { const items = eventToItem(event) if (items instanceof Promise) { defer(event, items) } else if (items) { dirty = true for (const item of ensurePlural(items)) { data.push(item as T) } } } } 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) }, {throttle}, ) export type DeriveEventsOptions = Omit< DeriveEventsMappedOptions, "itemToEvent" | "eventToItem" > export const deriveEvents = (repository: Repository, opts: DeriveEventsOptions) => deriveEventsMapped(repository, { ...opts, eventToItem: identity, itemToEvent: identity, }) export const deriveEvent = (repository: Repository, idOrAddress: string) => derived( deriveEvents(repository, { filters: getIdFilters([idOrAddress]), includeDeleted: true, }), first, ) export const deriveIsDeleted = (repository: Repository, event: TrustedEvent) => custom(setter => { setter(repository.isDeleted(event)) const onUpdate = batch(300, () => setter(repository.isDeleted(event))) repository.on("update", onUpdate) return () => repository.off("update", onUpdate) }) export const deriveIsDeletedByAddress = (repository: Repository, event: TrustedEvent) => custom(setter => { setter(repository.isDeletedByAddress(event)) const onUpdate = batch(300, () => setter(repository.isDeletedByAddress(event))) repository.on("update", onUpdate) return () => repository.off("update", onUpdate) })