diff --git a/packages/app/src/profiles.ts b/packages/app/src/profiles.ts index 2972c72..ebc5093 100644 --- a/packages/app/src/profiles.ts +++ b/packages/app/src/profiles.ts @@ -1,35 +1,32 @@ import {derived, readable} from "svelte/store" import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util" -import {PublishedProfile} from "@welshman/util" -import {deriveEventsMapped, collection, withGetter} from "@welshman/store" +import {deriveItemsByKey, deriveItems, makeDeriveItem, getter} from "@welshman/store" import {repository} from "./core.js" import {makeOutboxLoaderWithIndexers} from "./relaySelections.js" -export const profiles = withGetter( - deriveEventsMapped(repository, { - filters: [{kinds: [PROFILE]}], - eventToItem: readProfile, - itemToEvent: item => item.event, - }), -) - -export const { - indexStore: profilesByPubkey, - deriveItem: deriveProfile, - loadItem: loadProfile, -} = collection({ - name: "profiles", - store: profiles, +export const profilesByPubkey = deriveItemsByKey({ + repository, + eventToItem: readProfile, + filters: [{kinds: [PROFILE]}], getKey: profile => profile.event.pubkey, - load: makeOutboxLoaderWithIndexers(PROFILE), }) -export const displayProfileByPubkey = (pubkey: string | undefined) => - pubkey ? displayProfile(profilesByPubkey.get().get(pubkey), displayPubkey(pubkey)) : "" +export const profiles = deriveItems(profilesByPubkey) -export const deriveProfileDisplay = (pubkey: string | undefined, relays: string[] = []) => +export const loadProfile = makeOutboxLoaderWithIndexers(PROFILE) + +export const deriveProfile = makeDeriveItem(profilesByPubkey, loadProfile) + +export const getProfilesByPubkey = getter(profilesByPubkey) + +export const getProfile = (pubkey: string) => getProfilesByPubkey().get(pubkey) + +export const displayProfileByPubkey = (pubkey: string | undefined) => + pubkey ? displayProfile(getProfile(pubkey), displayPubkey(pubkey)) : "" + +export const deriveProfileDisplay = (pubkey: string | undefined, ...args: any[]) => pubkey - ? derived(deriveProfile(pubkey, relays), $profile => + ? derived(deriveProfile(pubkey, ...args), $profile => displayProfile($profile, displayPubkey(pubkey)), ) : readable("") diff --git a/packages/store/src/getter.ts b/packages/store/src/getter.ts deleted file mode 100644 index 9e34395..0000000 --- a/packages/store/src/getter.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {Readable, Writable} from "svelte/store" - -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)} -} diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 766be61..fc1c073 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -1,6 +1,4 @@ export * from "./synced.js" -export * from "./getter.js" +export * from "./misc.js" export * from "./loader.js" -export * from "./throttle.js" -export * from "./memoize.js" export * from "./repository.js" diff --git a/packages/store/src/loader.ts b/packages/store/src/loader.ts index 2b4d759..f635733 100644 --- a/packages/store/src/loader.ts +++ b/packages/store/src/loader.ts @@ -1,7 +1,4 @@ -import {readable, derived, writable, Readable, Subscriber} from "svelte/store" -import {Maybe, batch, indexBy, remove, assoc, now} from "@welshman/lib" -import {withGetter, ReadableWithGetter} from "./getter.js" -import {memoized} from "./memoize.js" +import {Maybe, now} from "@welshman/lib" export type LoaderOptions = { getItem: (key: string) => T diff --git a/packages/store/src/memoize.ts b/packages/store/src/memoize.ts deleted file mode 100644 index 8274503..0000000 --- a/packages/store/src/memoize.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {derived, Readable, Subscriber, Stores, StoresValues} from "svelte/store" -import {memoize} from "@welshman/lib" - -export const memoized = (store: Readable) => { - const {subscribe} = store - - return {...store, subscribe: (f: Subscriber) => subscribe(memoize(f))} -} - -export const deriveDeduplicated = ( - stores: S, - get: (storeValues: StoresValues) => T, -): Readable => { - let prev: T - - return derived(stores, (storeValues, set) => { - const result = get(storeValues) - - if (prev !== result) { - prev = result - set(result) - } - }) -} diff --git a/packages/store/src/misc.ts b/packages/store/src/misc.ts new file mode 100644 index 0000000..6a8124a --- /dev/null +++ b/packages/store/src/misc.ts @@ -0,0 +1,99 @@ +import { + get, + derived, + Readable, + Unsubscriber, + Writable, + Subscriber, + Stores, + StoresValues, +} from "svelte/store" +import {memoize, throttle} from "@welshman/lib" + +// Smart getter that adjusts between svelte's get and aggressive subscription depending on how hot +// the path is + +export const getter = (store: Readable, {threshold = 10}: {threshold?: number} = {}) => { + const calls: number[] = [] + let unsubscribe: Unsubscriber | undefined + let offset = 0 + let value: T + + return () => { + const now = Date.now() + const cutoff = now - 1000 + + // Find the first timestamp within the window (avoid expensive shift) + while (offset < calls.length && calls[offset] < cutoff) { + offset++ + } + + // Periodically clean up old timestamps to prevent unbounded growth + if (offset > 100) { + calls.splice(0, offset) + offset = 0 + } + + // Add current timestamp + calls.push(now) + + // Check if call rate exceeds threshold and switch to more aggressive mode + if (calls.length - offset > threshold) { + if (!unsubscribe) { + unsubscribe = store.subscribe((newValue: T) => { + value = newValue + }) + } + + return value + } else { + if (unsubscribe) { + unsubscribe() + unsubscribe = undefined + } + + return get(store) + } + } +} + +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)} +} + +export const memoized = (store: Readable) => { + const {subscribe} = store + + return {...store, subscribe: (f: Subscriber) => subscribe(memoize(f))} +} + +export const throttled = >(delay: number, store: S) => { + if (delay) { + const {subscribe} = store + + store = {...store, subscribe: (f: Subscriber) => subscribe(throttle(delay, f))} + } + + return store +} + +export const deriveDeduplicated = ( + stores: S, + get: (storeValues: StoresValues) => T, +): Readable => { + let prev: T + + return derived(stores, (storeValues, set) => { + const result = get(storeValues) + + if (prev !== result) { + prev = result + set(result) + } + }) +} diff --git a/packages/store/src/repository.ts b/packages/store/src/repository.ts index b60fec2..afd027b 100644 --- a/packages/store/src/repository.ts +++ b/packages/store/src/repository.ts @@ -1,21 +1,8 @@ import {derived, readable, Readable} from "svelte/store" -import { - on, - indexBy, - mapPop, - Maybe, - call, - sortBy, - identity, - ensurePlural, - removeUndefined, - batch, - partition, - first, -} from "@welshman/lib" -import {matchFilters, getIdAndAddress, getIdFilters, Filter, TrustedEvent} from "@welshman/util" +import {on, indexBy, mapPop, Maybe, call, sortBy, first} from "@welshman/lib" +import {matchFilters, getIdFilters, Filter, TrustedEvent} from "@welshman/util" import {Repository, RepositoryUpdate, Tracker} from "@welshman/net" -import {deriveDeduplicated} from './memoize.js' +import {deriveDeduplicated} from "./misc.js" // Events by id @@ -179,9 +166,10 @@ export const deriveEventsByIdByUrl = ({ }) } -export const deriveEventsByIdForUrl = (url: string, eventsByIdByUrlStore: Readable) => - deriveDeduplicated(eventsByIdByUrlStore, eventsByIdByUrl => eventsByIdByUrl.get(url)) - +export const deriveEventsByIdForUrl = ( + url: string, + eventsByIdByUrlStore: Readable, +) => deriveDeduplicated(eventsByIdByUrlStore, eventsByIdByUrl => eventsByIdByUrl.get(url)) // Items by key @@ -274,6 +262,17 @@ export const deriveItems = (itemsByKeyStore: Readable>) => export const deriveItemsSorted = (sortFn: (item: T) => number, itemsStore: Readable) => deriveDeduplicated(itemsStore, items => sortBy(sortFn, items)) +export const makeDeriveItem = ( + itemsByKeyStore: Readable>, + onDerive?: (key: string, ...args: any[]) => void, +) => { + return (key: string, ...args: any[]) => { + onDerive?.(key, ...args) + + return deriveDeduplicated(itemsByKeyStore, itemsByKey => itemsByKey.get(key)) + } +} + // Miscellaneous other stuff export const deriveEvent = (repository: Repository, idOrAddress: string) => @@ -288,7 +287,7 @@ export const deriveEvent = (repository: Repository, idOrAddress: string) => export const deriveIsDeleted = (repository: Repository, event: TrustedEvent) => readable(repository.isDeleted(event), set => { - const unsubscribe = on(repository, 'update', ({removed}: RepositoryUpdate) => { + const unsubscribe = on(repository, "update", ({removed}: RepositoryUpdate) => { if (removed.has(event.id)) { set(true) unsubscribe()