Clean up store semantics

This commit is contained in:
Jon Staab
2026-06-18 08:25:23 -07:00
parent f5124a6c4e
commit aae201414d
20 changed files with 187 additions and 227 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ import {
removeFromList,
} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Collection} from "./collection.js"
import {DerivedData} from "./clientData.js"
import {Network} from "./network.js"
import {Router} from "./router.js"
import {User} from "./user.js"
@@ -21,7 +21,7 @@ import type {IClient} from "./client.js"
* so it depends on the relay-list collection. Feeds `RelayStats.getQuality` so
* blocked relays are never selected.
*/
export class BlockedRelayLists extends Collection<ReturnType<typeof readList>> {
export class BlockedRelayLists extends DerivedData<ReturnType<typeof readList>> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [BLOCKED_RELAYS]}],
+2 -2
View File
@@ -1,6 +1,6 @@
import {BLOSSOM_SERVERS, asDecryptedEvent, readList} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Collection} from "./collection.js"
import {DerivedData} from "./clientData.js"
import {Network} from "./network.js"
import type {IClient} from "./client.js"
@@ -8,7 +8,7 @@ import type {IClient} from "./client.js"
* Blossom server lists (kind 10063), keyed by pubkey. Loaded via the outbox
* model (the author's write relays), so it depends on the relay-list collection.
*/
export class BlossomServerLists extends Collection<ReturnType<typeof readList>> {
export class BlossomServerLists extends DerivedData<ReturnType<typeof readList>> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [BLOSSOM_SERVERS]}],
+79 -28
View File
@@ -1,38 +1,37 @@
import {writable} from "svelte/store"
import type {Readable, Unsubscriber} from "svelte/store"
import type {Maybe} from "@welshman/lib"
import {getter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
import type {MakeLoadItemOptions} from "@welshman/store"
import type {Filter} from "@welshman/util"
import {deriveItems, withGetter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
import type {
ReadableWithGetter,
EventToItem,
ItemsByKey,
MakeLoadItemOptions,
} from "@welshman/store"
import type {IClient} from "./client.js"
import {Stores} from "./stores.js"
/**
* Base class for a reactive, keyed collection of "local" (non-event) data —
* things like relay stats or NIP-11 profiles that aren't backed by the
* repository. The collection owns its own map and is its own Svelte store: its
* `subscribe` emits the underlying `Map`.
*
* Subclasses reach the client through the `IClient` seam, never the
* concrete `Client`, so they never create a dependency cycle.
* repository. The collection owns its own map.
*/
export class ClientData<T> {
protected index = writable(new Map<string, T>())
protected getIndex = getter(this.index)
protected itemSubscribers: ((key: string, value: Maybe<T>) => void)[] = []
public derived: (key?: string, ...args: any[]) => Readable<Maybe<T>>
index = withGetter(writable(new Map<string, T>()))
all = withGetter(deriveItems(this.index))
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
subs: ((key: string, value: Maybe<T>) => void)[] = []
constructor(protected readonly ctx: IClient) {
this.derived = makeDeriveItem(this.index)
this.one = makeDeriveItem(this.index)
}
subscribe = this.index.subscribe
keys = () => this.index.get().keys()
get = (key: string): Maybe<T> => this.getIndex().get(key)
values = () => this.index.get().values()
getAll = (): T[] => Array.from(this.getIndex().values())
keys = () => this.getIndex().keys()
values = () => this.getIndex().values()
get = (key: string) => this.index.get().get(key)
set = (key: string, value: T) => {
this.index.update($items => {
@@ -55,7 +54,7 @@ export class ClientData<T> {
}
clear = () => {
const keys = Array.from(this.getIndex().keys())
const keys = Array.from(this.index.get().keys())
this.index.set(new Map())
@@ -65,17 +64,17 @@ export class ClientData<T> {
}
onItem = (subscriber: (key: string, value: Maybe<T>) => void): Unsubscriber => {
this.itemSubscribers.push(subscriber)
this.subs.push(subscriber)
return () => {
const i = this.itemSubscribers.indexOf(subscriber)
const i = this.subs.indexOf(subscriber)
if (i !== -1) this.itemSubscribers.splice(i, 1)
if (i !== -1) this.subs.splice(i, 1)
}
}
protected emitItem = (key: string, value: Maybe<T>) => {
for (const subscriber of this.itemSubscribers) {
for (const subscriber of this.subs) {
subscriber(key, value)
}
}
@@ -83,7 +82,7 @@ export class ClientData<T> {
/**
* A `ClientData` collection that knows how to lazily load items by key from the
* network. Subclasses implement `fetch`; `load`/`forceLoad`/`derived` are derived
* network. Subclasses implement `fetch`; `load`/`forceLoad`/`one` are derived
* from it (with per-key caching and backoff via `makeLoadItem`).
*/
export abstract class LoadableData<T> extends ClientData<T> {
@@ -99,9 +98,61 @@ export abstract class LoadableData<T> extends ClientData<T> {
// *after* super() — so `this.fetch` is undefined here. makeLoadItem captures
// its loadItem eagerly, so we defer the lookup to call time via this wrapper.
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
const read = (key: string) => this.index.get().get(key)
this.load = makeLoadItem(fetch, this.get, options)
this.forceLoad = makeForceLoadItem(fetch, this.get)
this.derived = makeDeriveItem(this.index, this.load)
this.load = makeLoadItem(fetch, read, options)
this.forceLoad = makeForceLoadItem(fetch, read)
this.one = makeDeriveItem(this.index, this.load)
}
}
export type DerivedDataOptions<T> = {
filters: Filter[]
eventToItem: EventToItem<T>
getKey: (item: T) => string
loadOptions?: MakeLoadItemOptions
}
/**
* Base class for a reactive, keyed collection of data derived from nostr events.
* The repository is the single source of truth — the collection is a live view
* over `ctx.itemsByKey`, never a duplicated map. Subclasses implement `fetch`
* (how to load an item by key from the network) and pass the filters/decoder via
* `super`.
*/
export abstract class DerivedData<T> {
index: ReadableWithGetter<ItemsByKey<T>>
all: ReadableWithGetter<T[]>
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
load: (key: string, ...args: any[]) => Promise<Maybe<T>>
forceLoad: (key: string, ...args: any[]) => Promise<Maybe<T>>
abstract fetch(key: string, ...args: any[]): Promise<unknown>
constructor(
protected readonly ctx: IClient,
options: DerivedDataOptions<T>,
) {
this.index = withGetter(
ctx.use(Stores).itemsByKey<T>({
filters: options.filters,
eventToItem: options.eventToItem,
getKey: options.getKey,
}),
)
this.all = withGetter(deriveItems(this.index))
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
const read = (key: string) => this.index.get().get(key)
this.load = makeLoadItem(fetch, read, options.loadOptions)
this.forceLoad = makeForceLoadItem(fetch, read)
this.one = makeDeriveItem(this.index, this.load)
}
keys = () => this.index.get().keys()
values = () => this.index.get().values()
get = (key: string) => this.index.get().get(key)
}
-85
View File
@@ -1,85 +0,0 @@
import type {Readable} from "svelte/store"
import type {Maybe} from "@welshman/lib"
import type {Filter} from "@welshman/util"
import {deriveItems, getter, makeLoadItem, makeForceLoadItem, makeDeriveItem} from "@welshman/store"
import type {EventToItem, ItemsByKey, MakeLoadItemOptions} from "@welshman/store"
import type {IClient} from "./client.js"
import {Stores} from "./stores.js"
export type CollectionOptions<T> = {
filters: Filter[]
eventToItem: EventToItem<T>
getKey: (item: T) => string
loadOptions?: MakeLoadItemOptions
}
/**
* Base class for a reactive, keyed collection of data derived from nostr events.
* The repository is the single source of truth — the collection is a live view
* over `ctx.deriveItemsByKey`, never a duplicated map. Subclasses implement
* `fetch` (how to load an item by key from the network) and pass the
* filters/decoder via `super`.
*
* Like `ClientData`, subclasses depend only on the `IClient` seam.
*/
export abstract class Collection<T> {
byKey: Readable<ItemsByKey<T>>
all: Readable<T[]>
subscribe: Readable<ItemsByKey<T>>["subscribe"]
get: (key: string) => Maybe<T>
getAll: () => T[]
keys: () => IterableIterator<string>
values: () => IterableIterator<T>
load: (key: string, ...args: any[]) => Promise<Maybe<T>>
forceLoad: (key: string, ...args: any[]) => Promise<Maybe<T>>
derived: (key?: string, ...args: any[]) => Readable<Maybe<T>>
private getByKey: () => ItemsByKey<T>
abstract fetch(key: string, ...args: any[]): Promise<unknown>
constructor(
protected readonly ctx: IClient,
options: CollectionOptions<T>,
) {
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
this.byKey = ctx.use(Stores).deriveItemsByKey<T>({
filters: options.filters,
eventToItem: options.eventToItem,
getKey: options.getKey,
})
this.all = deriveItems(this.byKey)
this.subscribe = this.byKey.subscribe
this.getByKey = getter(this.byKey)
this.getAll = getter(this.all)
this.get = (key: string) => this.getByKey().get(key)
this.keys = () => this.getByKey().keys()
this.values = () => this.getByKey().values()
this.load = makeLoadItem(fetch, this.get, options.loadOptions)
this.forceLoad = makeForceLoadItem(fetch, this.get)
this.derived = makeDeriveItem(this.byKey, this.load)
}
// Convenience views of the current user's own item (replaces the old
// user.ts userProfile/userFollowList/etc. derived stores)
getForUser = () => {
const pubkey = this.ctx.user?.pubkey
return pubkey ? this.get(pubkey) : undefined
}
deriveForUser = (...args: any[]) => this.derived(this.ctx.user?.pubkey, ...args)
loadForUser = (...args: any[]) => {
const pubkey = this.ctx.user?.pubkey
return pubkey ? this.load(pubkey, ...args) : Promise.resolve(undefined)
}
forceLoadForUser = (...args: any[]) => {
const pubkey = this.ctx.user?.pubkey
return pubkey ? this.forceLoad(pubkey, ...args) : Promise.resolve(undefined)
}
}
+4 -3
View File
@@ -1,3 +1,4 @@
import {get} from "svelte/store"
import {Scope, FeedController} from "@welshman/feeds"
import type {FeedControllerOptions, Feed} from "@welshman/feeds"
import type {AdapterContext} from "@welshman/net"
@@ -26,11 +27,11 @@ export class Feeds {
case Scope.Self:
return [$pubkey]
case Scope.Follows:
return this.ctx.use(Wot).deriveFollows($pubkey).get()
return get(this.ctx.use(Wot).follows($pubkey))
case Scope.Network:
return this.ctx.use(Wot).deriveNetwork($pubkey).get()
return get(this.ctx.use(Wot).network($pubkey))
case Scope.Followers:
return this.ctx.use(Wot).deriveFollowers($pubkey).get()
return get(this.ctx.use(Wot).followers($pubkey))
default:
return []
}
+2 -2
View File
@@ -7,7 +7,7 @@ import {
removeFromList,
} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Collection} from "./collection.js"
import {DerivedData} from "./clientData.js"
import {Network} from "./network.js"
import {Thunks} from "./thunk.js"
import {User} from "./user.js"
@@ -17,7 +17,7 @@ import type {IClient} from "./client.js"
* Kind-3 follow lists, keyed by pubkey. Loaded via the outbox model (the
* author's write relays), so it depends on the relay-list collection.
*/
export class FollowLists extends Collection<ReturnType<typeof readList>> {
export class FollowLists extends DerivedData<ReturnType<typeof readList>> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [FOLLOWS]}],
+2 -2
View File
@@ -61,11 +61,11 @@ export class Handles extends LoadableData<Handle> {
return $profile?.nip05 ? this.load($profile.nip05) : undefined
}
deriveForPubkey = (pubkey: string, relays: string[] = []) => {
forPubkey = (pubkey: string, relays: string[] = []) => {
this.loadForPubkey(pubkey, relays)
return deriveDeduplicated(
[this.index, this.ctx.use(Profiles).derived(pubkey, relays)],
[this.index, this.ctx.use(Profiles).one(pubkey, relays)],
([$handlesByNip05, $profile]) => {
if (!$profile?.nip05) return undefined
-1
View File
@@ -3,7 +3,6 @@ export * from "./policies.js"
export * from "./network.js"
export * from "./stores.js"
export * from "./clientData.js"
export * from "./collection.js"
export * from "./user.js"
export * from "./router.js"
export * from "./relays.js"
+2 -2
View File
@@ -8,7 +8,7 @@ import {
removeFromList,
} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Collection} from "./collection.js"
import {DerivedData} from "./clientData.js"
import {Network} from "./network.js"
import {Router} from "./router.js"
import {User} from "./user.js"
@@ -20,7 +20,7 @@ import type {IClient} from "./client.js"
* outbox model (the author's write relays), so it depends on the relay-list
* collection.
*/
export class MessagingRelayLists extends Collection<ReturnType<typeof readList>> {
export class MessagingRelayLists extends DerivedData<ReturnType<typeof readList>> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [MESSAGING_RELAYS]}],
+2 -2
View File
@@ -9,7 +9,7 @@ import {
updateList,
} from "@welshman/util"
import type {TrustedEvent, PublishedList} from "@welshman/util"
import {Collection} from "./collection.js"
import {DerivedData} from "./clientData.js"
import type {IClient} from "./client.js"
import {Network} from "./network.js"
import {Thunks} from "./thunk.js"
@@ -20,7 +20,7 @@ import {User} from "./user.js"
* Kind-10000 mute lists, keyed by pubkey. Mute lists carry private entries in
* encrypted content, so decoding goes through the plaintext cache.
*/
export class MuteLists extends Collection<PublishedList> {
export class MuteLists extends DerivedData<PublishedList> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [MUTES]}],
+2 -2
View File
@@ -7,7 +7,7 @@ import {
removeFromList,
} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Collection} from "./collection.js"
import {DerivedData} from "./clientData.js"
import {Network} from "./network.js"
import {Thunks} from "./thunk.js"
import {User} from "./user.js"
@@ -17,7 +17,7 @@ import type {IClient} from "./client.js"
* NIP-51 pin lists (kind 10001), keyed by pubkey. Loaded via the outbox model
* (the author's write relays), so it depends on the relay-list collection.
*/
export class PinLists extends Collection<ReturnType<typeof readList>> {
export class PinLists extends DerivedData<ReturnType<typeof readList>> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [PINS]}],
+6 -5
View File
@@ -9,7 +9,8 @@ import {
PROFILE,
} from "@welshman/util"
import type {Profile} from "@welshman/util"
import {Collection} from "./collection.js"
import type {Readable} from "svelte/store"
import {DerivedData} from "./clientData.js"
import {Network} from "./network.js"
import {Router} from "./router.js"
import {Thunks} from "./thunk.js"
@@ -19,7 +20,7 @@ import type {IClient} from "./client.js"
* Kind-0 profiles, keyed by pubkey. Loaded via the outbox model (the author's
* write relays), resolved through the relay-list collection at fetch time.
*/
export class Profiles extends Collection<ReturnType<typeof readProfile>> {
export class Profiles extends DerivedData<ReturnType<typeof readProfile>> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [PROFILE]}],
@@ -40,12 +41,12 @@ export class Profiles extends Collection<ReturnType<typeof readProfile>> {
return this.ctx.use(Thunks).publish({event, relays})
}
display = (pubkey: string | undefined) =>
display = (pubkey: string | undefined, ...args: any[]): Readable<string> =>
pubkey ? displayProfile(this.get(pubkey), displayPubkey(pubkey)) : ""
deriveDisplay = (pubkey: string | undefined, ...args: any[]) =>
deriveDisplay = (pubkey: string | undefined, ...args: any[]): Readable<string> =>
pubkey
? derived(this.derived(pubkey, ...args), $profile =>
? derived(this.one(pubkey, ...args), $profile =>
displayProfile($profile, displayPubkey(pubkey)),
)
: readable("")
+2 -2
View File
@@ -12,7 +12,7 @@ import {
makeEvent,
} from "@welshman/util"
import type {TrustedEvent, PublishedList} from "@welshman/util"
import {Collection} from "./collection.js"
import {DerivedData} from "./clientData.js"
import {Router, addMinimalFallbacks} from "./router.js"
import {Network} from "./network.js"
import {User} from "./user.js"
@@ -23,7 +23,7 @@ import type {IClient} from "./client.js"
* NIP-65 relay lists, keyed by pubkey. This is the routing substrate every other
* outbox-model load depends on (see `Network.loadUsingOutbox`).
*/
export class RelayLists extends Collection<PublishedList> {
export class RelayLists extends DerivedData<PublishedList> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [RELAYS]}],
+2 -2
View File
@@ -1,5 +1,5 @@
import {derived} from "svelte/store"
import {fetchJson} from "@welshman/lib"
import {fetchone} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {displayRelayUrl, displayRelayProfile} from "@welshman/util"
import type {RelayProfile} from "@welshman/util"
@@ -39,5 +39,5 @@ export class Relays extends LoadableData<RelayProfile> {
display = (url: string) => displayRelayProfile(this.get(url), displayRelayUrl(url))
deriveDisplay = (url: string) =>
derived(this.derive(url), $relay => displayRelayProfile($relay, displayRelayUrl(url)))
derived(this.one(url), $relay => displayRelayProfile($relay, displayRelayUrl(url)))
}
+3 -3
View File
@@ -6,7 +6,7 @@ import type {Readable} from "svelte/store"
import {dec, inc, sortBy} from "@welshman/lib"
import {PROFILE} from "@welshman/util"
import type {PublishedProfile, RelayProfile} from "@welshman/util"
import {throttled, deriveItems} from "@welshman/store"
import {throttled} from "@welshman/store"
import type {IClient} from "./client.js"
import {Network} from "./network.js"
import {Router} from "./router.js"
@@ -69,7 +69,7 @@ export class Searches {
constructor(readonly ctx: IClient) {
this.profileSearch = derived(
[throttled(800, this.ctx.use(Profiles).all), throttled(800, this.ctx.use(Handles))],
[throttled(800, this.ctx.use(Profiles).all), throttled(800, this.ctx.use(Handles).index)],
([$profiles, $handlesByNip05]) => {
// Remove invalid nip05's from profiles
const options = $profiles.map(p => {
@@ -107,7 +107,7 @@ export class Searches {
}),
)
this.relaySearch = derived(deriveItems(this.ctx.use(Relays)), $relays =>
this.relaySearch = derived(this.ctx.use(Relays).all, $relays =>
createSearch($relays, {
getValue: (relay: RelayProfile) => relay.url,
fuseOptions: {
+2 -2
View File
@@ -8,7 +8,7 @@ import {
removeFromList,
} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Collection} from "./collection.js"
import {DerivedData} from "./clientData.js"
import {Network} from "./network.js"
import {Router} from "./router.js"
import {User} from "./user.js"
@@ -20,7 +20,7 @@ import type {IClient} from "./client.js"
* outbox model (the author's write relays), so it depends on the relay-list
* collection.
*/
export class SearchRelayLists extends Collection<ReturnType<typeof readList>> {
export class SearchRelayLists extends DerivedData<ReturnType<typeof readList>> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [SEARCH_RELAYS]}],
+7 -7
View File
@@ -30,29 +30,29 @@ export class Stores {
getEventsById = (options: Omit<EventsByIdOptions, "repository">) =>
getEventsById({...options, repository: this.ctx.repository})
deriveEventsById = (options: Omit<EventsByIdOptions, "repository">) =>
eventsById = (options: Omit<EventsByIdOptions, "repository">) =>
deriveEventsById({...options, repository: this.ctx.repository})
deriveEvents = (options: Omit<EventsByIdOptions, "repository">) =>
events = (options: Omit<EventsByIdOptions, "repository">) =>
deriveEvents({...options, repository: this.ctx.repository})
makeDeriveEvent = (options: Omit<EventOptions, "repository">) =>
makeEvent = (options: Omit<EventOptions, "repository">) =>
makeDeriveEvent({...options, repository: this.ctx.repository})
getEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
getEventsByIdByUrl({...options, tracker: this.ctx.tracker, repository: this.ctx.repository})
deriveEventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
eventsByIdByUrl = (options: Omit<EventsByIdByUrlOptions, "tracker" | "repository">) =>
deriveEventsByIdByUrl({...options, tracker: this.ctx.tracker, repository: this.ctx.repository})
getEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
getEventsByIdForUrl({...options, tracker: this.ctx.tracker, repository: this.ctx.repository})
deriveEventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
eventsByIdForUrl = (options: Omit<EventsByIdForUrlOptions, "tracker" | "repository">) =>
deriveEventsByIdForUrl({...options, tracker: this.ctx.tracker, repository: this.ctx.repository})
deriveItemsByKey = <T>(options: Omit<ItemsByKeyOptions<T>, "repository">) =>
itemsByKey = <T>(options: Omit<ItemsByKeyOptions<T>, "repository">) =>
deriveItemsByKey<T>({...options, repository: this.ctx.repository})
deriveIsDeleted = (event: TrustedEvent) => deriveIsDeleted(this.ctx.repository, event)
isDeleted = (event: TrustedEvent) => deriveIsDeleted(this.ctx.repository, event)
}
+2 -1
View File
@@ -1,3 +1,4 @@
import {get} from "svelte/store"
import {uniq, remove} from "@welshman/lib"
import {
getAddress,
@@ -31,7 +32,7 @@ export class Tags {
"p",
pubkey,
this.ctx.use(Router).FromPubkey(pubkey).getUrl() || "",
this.ctx.use(Profiles).display(pubkey),
get(this.ctx.use(Profiles).display(pubkey)),
]
tagEvent = (event: TrustedEvent, url = "", mark = "") => {
+62 -70
View File
@@ -15,10 +15,10 @@ const listPubkeys = (list: List | undefined) => getPubkeyTagValues(getListTags(l
* built from the perspective of the client's user (or, with no user, the union
* of every known follow list) and updated reactively as lists change.
*
* Every query is exposed as a reactive store — the aggregate `*ByPubkey`/`graph`/
* `max` fields and the parameterized `derive*` methods. All of them are wrapped
* in `withGetter`, so a synchronous snapshot is just `<store>.get()` /
* `derive*(...).get()`.
* The aggregate `*ByPubkey`/`graph`/`max` fields are long-lived `withGetter`
* stores (snapshot with `.get()`). The parameterized methods (`follows`,
* `wotScore`, …) return plain on-demand stores — snapshot them with svelte's
* `get(...)`.
*/
export class Wot {
followersByPubkey: ReadableWithGetter<Map<string, Set<string>>>
@@ -29,7 +29,7 @@ export class Wot {
constructor(readonly ctx: IClient) {
this.followersByPubkey = withGetter(
readable(new Map<string, Set<string>>(), set =>
this.ctx.use(FollowLists).subscribe(
this.ctx.use(FollowLists).index.subscribe(
throttle(1000, lists => {
const $followersByPubkey = new Map<string, Set<string>>()
@@ -47,7 +47,7 @@ export class Wot {
this.mutersByPubkey = withGetter(
readable(new Map<string, Set<string>>(), set =>
this.ctx.use(MuteLists).subscribe(
this.ctx.use(MuteLists).index.subscribe(
throttle(1000, lists => {
const $mutersByPubkey = new Map<string, Set<string>>()
@@ -66,20 +66,18 @@ export class Wot {
this.graph = withGetter(
readable(new Map<string, number>(), set => {
const rebuild = throttle(1000, () => {
const followLists = this.ctx.use(FollowLists)
const muteLists = this.ctx.use(MuteLists)
const $followLists = this.ctx.use(FollowLists).index.get()
const $muteLists = this.ctx.use(MuteLists).index.get()
const $pubkey = this.ctx.user?.pubkey
const $graph = new Map<string, number>()
const $follows = $pubkey
? listPubkeys(followLists.get($pubkey))
: Array.from(followLists.keys())
const roots = $pubkey ? listPubkeys($followLists.get($pubkey)) : Array.from($followLists.keys())
for (const follow of $follows) {
for (const pubkey of listPubkeys(followLists.get(follow))) {
for (const follow of roots) {
for (const pubkey of listPubkeys($followLists.get(follow))) {
$graph.set(pubkey, inc($graph.get(pubkey)))
}
for (const pubkey of listPubkeys(muteLists.get(follow))) {
for (const pubkey of listPubkeys($muteLists.get(follow))) {
$graph.set(pubkey, dec($graph.get(pubkey)))
}
}
@@ -88,8 +86,8 @@ export class Wot {
})
const unsubscribers = [
this.ctx.use(FollowLists).subscribe(rebuild),
this.ctx.use(MuteLists).subscribe(rebuild),
this.ctx.use(FollowLists).index.subscribe(rebuild),
this.ctx.use(MuteLists).index.subscribe(rebuild),
]
return () => unsubscribers.forEach(unsubscribe => unsubscribe())
@@ -99,79 +97,73 @@ export class Wot {
this.max = withGetter(derived(this.graph, $g => max(Array.from($g.values()))))
}
deriveFollows = (pubkey: string) =>
withGetter(derived(this.ctx.use(FollowLists), $lists => listPubkeys($lists.get(pubkey))))
follows = (pubkey: string) =>
derived(this.ctx.use(FollowLists).index, $lists => listPubkeys($lists.get(pubkey)))
deriveMutes = (pubkey: string) =>
withGetter(derived(this.ctx.use(MuteLists), $lists => listPubkeys($lists.get(pubkey))))
mutes = (pubkey: string) =>
derived(this.ctx.use(MuteLists).index, $lists => listPubkeys($lists.get(pubkey)))
deriveNetwork = (pubkey: string) =>
withGetter(
derived(this.ctx.use(FollowLists), $lists => {
const pubkeys = new Set(listPubkeys($lists.get(pubkey)))
const network = new Set<string>()
network = (pubkey: string) =>
derived(this.ctx.use(FollowLists).index, $lists => {
const pubkeys = new Set(listPubkeys($lists.get(pubkey)))
const network = new Set<string>()
for (const follow of pubkeys) {
for (const tpk of listPubkeys($lists.get(follow))) {
if (!pubkeys.has(tpk)) {
network.add(tpk)
}
for (const follow of pubkeys) {
for (const tpk of listPubkeys($lists.get(follow))) {
if (!pubkeys.has(tpk)) {
network.add(tpk)
}
}
}
return Array.from(network)
}),
)
return Array.from(network)
})
deriveFollowers = (pubkey: string) =>
withGetter(derived(this.followersByPubkey, $followers => Array.from($followers.get(pubkey) || [])))
followers = (pubkey: string) =>
derived(this.followersByPubkey, $followers => Array.from($followers.get(pubkey) || []))
deriveMuters = (pubkey: string) =>
withGetter(derived(this.mutersByPubkey, $muters => Array.from($muters.get(pubkey) || [])))
muters = (pubkey: string) =>
derived(this.mutersByPubkey, $muters => Array.from($muters.get(pubkey) || []))
deriveFollowsWhoFollow = (pubkey: string, target: string) =>
withGetter(
derived(this.ctx.use(FollowLists), $lists =>
listPubkeys($lists.get(pubkey)).filter(other =>
listPubkeys($lists.get(other)).includes(target),
),
followsWhoFollow = (pubkey: string, target: string) =>
derived(this.ctx.use(FollowLists).index, $lists =>
listPubkeys($lists.get(pubkey)).filter(other =>
listPubkeys($lists.get(other)).includes(target),
),
)
deriveFollowsWhoMute = (pubkey: string, target: string) =>
withGetter(
derived([this.ctx.use(FollowLists), this.ctx.use(MuteLists)], ([$follows, $mutes]) =>
followsWhoMute = (pubkey: string, target: string) =>
derived(
[this.ctx.use(FollowLists).index, this.ctx.use(MuteLists).index],
([$follows, $mutes]) =>
listPubkeys($follows.get(pubkey)).filter(other =>
listPubkeys($mutes.get(other)).includes(target),
),
),
)
deriveWotScore = (pubkey: string, target: string) =>
withGetter(
derived(
[
this.ctx.use(FollowLists),
this.ctx.use(MuteLists),
this.followersByPubkey,
this.mutersByPubkey,
],
([$follows, $mutes, $followers, $muters]) => {
let follows: string[]
let mutes: string[]
wotScore = (pubkey: string, target: string) =>
derived(
[
this.ctx.use(FollowLists).index,
this.ctx.use(MuteLists).index,
this.followersByPubkey,
this.mutersByPubkey,
],
([$follows, $mutes, $followers, $muters]) => {
let follows: string[]
let mutes: string[]
if (pubkey) {
const theirFollows = listPubkeys($follows.get(pubkey))
if (pubkey) {
const theirFollows = listPubkeys($follows.get(pubkey))
follows = theirFollows.filter(other => listPubkeys($follows.get(other)).includes(target))
mutes = theirFollows.filter(other => listPubkeys($mutes.get(other)).includes(target))
} else {
follows = Array.from($followers.get(target) || [])
mutes = Array.from($muters.get(target) || [])
}
follows = theirFollows.filter(other => listPubkeys($follows.get(other)).includes(target))
mutes = theirFollows.filter(other => listPubkeys($mutes.get(other)).includes(target))
} else {
follows = Array.from($followers.get(target) || [])
mutes = Array.from($muters.get(target) || [])
}
return follows.length - mutes.length
},
),
return follows.length - mutes.length
},
)
}
+4 -4
View File
@@ -67,11 +67,11 @@ export class Zappers extends LoadableData<Zapper> {
return $profile?.lnurl ? this.load($profile.lnurl) : undefined
}
deriveForPubkey = (pubkey: string, relays: string[] = []) => {
forPubkey = (pubkey: string, relays: string[] = []) => {
this.loadForPubkey(pubkey, relays)
return deriveDeduplicated(
[this.index, this.ctx.use(Profiles).derived(pubkey, relays)],
[this.index, this.ctx.use(Profiles).one(pubkey, relays)],
([$zappersByLnurl, $profile]) =>
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined,
)
@@ -105,7 +105,7 @@ export class Zappers extends LoadableData<Zapper> {
await Promise.all(zapReceipts.map(zapReceipt => this.validateZapReceipt(zapReceipt, parent))),
)
deriveValidZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Readable<Zap[]> => {
validZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Readable<Zap[]> => {
const splits = getZapSplits(parent)
const profiles = this.ctx.use(Profiles)
@@ -116,7 +116,7 @@ export class Zappers extends LoadableData<Zapper> {
const stores: Readable<any>[] = [
this.index,
...splits.map(split => profiles.derived(split.pubkey)),
...splits.map(split => profiles.one(split.pubkey)),
]
return deriveDeduplicatedByValue(stores, (values: any[]) => {