This commit is contained in:
@@ -2,29 +2,41 @@ import {writable} from "svelte/store"
|
|||||||
import type {Readable, Unsubscriber} from "svelte/store"
|
import type {Readable, Unsubscriber} from "svelte/store"
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import {deriveItems, withGetter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
|
import {deriveItems, getter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
|
||||||
import type {
|
import type {EventToItem, ItemsByKey, MakeLoadItemOptions} from "@welshman/store"
|
||||||
ReadableWithGetter,
|
|
||||||
EventToItem,
|
|
||||||
ItemsByKey,
|
|
||||||
MakeLoadItemOptions,
|
|
||||||
} from "@welshman/store"
|
|
||||||
import type {IClient} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import {Stores} from "./stores.js"
|
import {Stores} from "./stores.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility type which allows for using the same value both for hot gets and derived subscriptions
|
||||||
|
*/
|
||||||
|
export type Projection<T> = {
|
||||||
|
get: () => T
|
||||||
|
$: Readable<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projection = <T>($: Readable<T>, get = getter($)) => ({$, get})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for a reactive, keyed collection of "local" (non-event) data —
|
* 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
|
* things like relay stats or NIP-11 profiles that aren't backed by the
|
||||||
* repository. The collection owns its own map.
|
* repository. The collection owns its own map.
|
||||||
|
*
|
||||||
|
* `index` (map) and `all` (values) are `Projection`s — subscribe via `.$`,
|
||||||
|
* snapshot via `.get()`. Per-key access is `one(key)`, a plain on-demand store
|
||||||
|
* (snapshot with svelte's `get(...)`, or read `get(key)` directly).
|
||||||
*/
|
*/
|
||||||
export class ClientData<T> {
|
export class ClientData<T> {
|
||||||
index = withGetter(writable(new Map<string, T>()))
|
protected store = writable(new Map<string, T>())
|
||||||
all = withGetter(deriveItems(this.index))
|
index: Projection<ItemsByKey<T>>
|
||||||
|
all: Projection<T[]>
|
||||||
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
||||||
subs: ((key: string, value: Maybe<T>) => void)[] = []
|
subs: ((key: string, value: Maybe<T>) => void)[] = []
|
||||||
|
|
||||||
constructor(protected readonly ctx: IClient) {
|
constructor(protected readonly ctx: IClient) {
|
||||||
this.one = makeDeriveItem(this.index)
|
this.index = projection(this.store)
|
||||||
|
this.all = projection(deriveItems(this.store))
|
||||||
|
this.one = makeDeriveItem(this.store)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys = () => this.index.get().keys()
|
keys = () => this.index.get().keys()
|
||||||
@@ -34,7 +46,7 @@ export class ClientData<T> {
|
|||||||
get = (key: string) => this.index.get().get(key)
|
get = (key: string) => this.index.get().get(key)
|
||||||
|
|
||||||
set = (key: string, value: T) => {
|
set = (key: string, value: T) => {
|
||||||
this.index.update($items => {
|
this.store.update($items => {
|
||||||
$items.set(key, value)
|
$items.set(key, value)
|
||||||
|
|
||||||
return $items
|
return $items
|
||||||
@@ -44,7 +56,7 @@ export class ClientData<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete = (key: string) => {
|
delete = (key: string) => {
|
||||||
this.index.update($items => {
|
this.store.update($items => {
|
||||||
$items.delete(key)
|
$items.delete(key)
|
||||||
|
|
||||||
return $items
|
return $items
|
||||||
@@ -56,7 +68,7 @@ export class ClientData<T> {
|
|||||||
clear = () => {
|
clear = () => {
|
||||||
const keys = Array.from(this.index.get().keys())
|
const keys = Array.from(this.index.get().keys())
|
||||||
|
|
||||||
this.index.set(new Map())
|
this.store.set(new Map())
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
this.emitItem(key, undefined)
|
this.emitItem(key, undefined)
|
||||||
@@ -102,7 +114,7 @@ export abstract class LoadableData<T> extends ClientData<T> {
|
|||||||
|
|
||||||
this.load = makeLoadItem(fetch, read, options)
|
this.load = makeLoadItem(fetch, read, options)
|
||||||
this.forceLoad = makeForceLoadItem(fetch, read)
|
this.forceLoad = makeForceLoadItem(fetch, read)
|
||||||
this.one = makeDeriveItem(this.index, this.load)
|
this.one = makeDeriveItem(this.store, this.load)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,10 +131,13 @@ export type DerivedDataOptions<T> = {
|
|||||||
* over `ctx.itemsByKey`, never a duplicated map. Subclasses implement `fetch`
|
* 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
|
* (how to load an item by key from the network) and pass the filters/decoder via
|
||||||
* `super`.
|
* `super`.
|
||||||
|
*
|
||||||
|
* `index` (map) and `all` (values) are `Projection`s — subscribe via `.$`,
|
||||||
|
* snapshot via `.get()`. Per-key access is `one(key)`, a plain on-demand store.
|
||||||
*/
|
*/
|
||||||
export abstract class DerivedData<T> {
|
export abstract class DerivedData<T> {
|
||||||
index: ReadableWithGetter<ItemsByKey<T>>
|
index: Projection<ItemsByKey<T>>
|
||||||
all: ReadableWithGetter<T[]>
|
all: Projection<T[]>
|
||||||
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
||||||
load: (key: string, ...args: any[]) => Promise<Maybe<T>>
|
load: (key: string, ...args: any[]) => Promise<Maybe<T>>
|
||||||
forceLoad: (key: string, ...args: any[]) => Promise<Maybe<T>>
|
forceLoad: (key: string, ...args: any[]) => Promise<Maybe<T>>
|
||||||
@@ -133,21 +148,21 @@ export abstract class DerivedData<T> {
|
|||||||
protected readonly ctx: IClient,
|
protected readonly ctx: IClient,
|
||||||
options: DerivedDataOptions<T>,
|
options: DerivedDataOptions<T>,
|
||||||
) {
|
) {
|
||||||
this.index = withGetter(
|
const index = ctx.use(Stores).itemsByKey<T>({
|
||||||
ctx.use(Stores).itemsByKey<T>({
|
filters: options.filters,
|
||||||
filters: options.filters,
|
eventToItem: options.eventToItem,
|
||||||
eventToItem: options.eventToItem,
|
getKey: options.getKey,
|
||||||
getKey: options.getKey,
|
})
|
||||||
}),
|
|
||||||
)
|
this.index = projection(index)
|
||||||
this.all = withGetter(deriveItems(this.index))
|
this.all = projection(deriveItems(index))
|
||||||
|
|
||||||
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
|
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
|
||||||
const read = (key: string) => this.index.get().get(key)
|
const read = (key: string) => this.index.get().get(key)
|
||||||
|
|
||||||
this.load = makeLoadItem(fetch, read, options.loadOptions)
|
this.load = makeLoadItem(fetch, read, options.loadOptions)
|
||||||
this.forceLoad = makeForceLoadItem(fetch, read)
|
this.forceLoad = makeForceLoadItem(fetch, read)
|
||||||
this.one = makeDeriveItem(this.index, this.load)
|
this.one = makeDeriveItem(index, this.load)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys = () => this.index.get().keys()
|
keys = () => this.index.get().keys()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import {get} from "svelte/store"
|
|
||||||
import {Scope, FeedController} from "@welshman/feeds"
|
import {Scope, FeedController} from "@welshman/feeds"
|
||||||
import type {FeedControllerOptions, Feed} from "@welshman/feeds"
|
import type {FeedControllerOptions, Feed} from "@welshman/feeds"
|
||||||
import type {AdapterContext} from "@welshman/net"
|
import type {AdapterContext} from "@welshman/net"
|
||||||
@@ -27,11 +26,11 @@ export class Feeds {
|
|||||||
case Scope.Self:
|
case Scope.Self:
|
||||||
return [$pubkey]
|
return [$pubkey]
|
||||||
case Scope.Follows:
|
case Scope.Follows:
|
||||||
return get(this.ctx.use(Wot).follows($pubkey))
|
return this.ctx.use(Wot).follows($pubkey).get()
|
||||||
case Scope.Network:
|
case Scope.Network:
|
||||||
return get(this.ctx.use(Wot).network($pubkey))
|
return this.ctx.use(Wot).network($pubkey).get()
|
||||||
case Scope.Followers:
|
case Scope.Followers:
|
||||||
return get(this.ctx.use(Wot).followers($pubkey))
|
return this.ctx.use(Wot).followers($pubkey).get()
|
||||||
default:
|
default:
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {tryCatch, batcher, postJson} from "@welshman/lib"
|
import {tryCatch, batcher, postJson} from "@welshman/lib"
|
||||||
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {queryProfile, displayNip05} from "@welshman/util"
|
import {queryProfile, displayNip05} from "@welshman/util"
|
||||||
import type {Handle} from "@welshman/util"
|
import type {Handle} from "@welshman/util"
|
||||||
import {deriveDeduplicated} from "@welshman/store"
|
import {deriveDeduplicated} from "@welshman/store"
|
||||||
import {LoadableData} from "./clientData.js"
|
import {LoadableData, projection} from "./clientData.js"
|
||||||
|
import type {Projection} from "./clientData.js"
|
||||||
import type {IClient} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import {Profiles} from "./profiles.js"
|
import {Profiles} from "./profiles.js"
|
||||||
|
|
||||||
@@ -61,20 +63,22 @@ export class Handles extends LoadableData<Handle> {
|
|||||||
return $profile?.nip05 ? this.load($profile.nip05) : undefined
|
return $profile?.nip05 ? this.load($profile.nip05) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
forPubkey = (pubkey: string, relays: string[] = []) => {
|
forPubkey = (pubkey: string, relays: string[] = []): Projection<Maybe<Handle>> => {
|
||||||
this.loadForPubkey(pubkey, relays)
|
this.loadForPubkey(pubkey, relays)
|
||||||
|
|
||||||
return deriveDeduplicated(
|
const read = ([$handlesByNip05, $profile]: [ReadonlyMap<string, Handle>, Maybe<{nip05?: string}>]) => {
|
||||||
[this.index, this.ctx.use(Profiles).one(pubkey, relays)],
|
if (!$profile?.nip05) return undefined
|
||||||
([$handlesByNip05, $profile]) => {
|
|
||||||
if (!$profile?.nip05) return undefined
|
|
||||||
|
|
||||||
const handle = $handlesByNip05.get($profile.nip05)
|
const handle = $handlesByNip05.get($profile.nip05)
|
||||||
|
|
||||||
if (handle?.pubkey !== pubkey) return undefined
|
if (handle?.pubkey !== pubkey) return undefined
|
||||||
|
|
||||||
return handle
|
return handle
|
||||||
},
|
}
|
||||||
|
|
||||||
|
return projection(
|
||||||
|
deriveDeduplicated([this.index.$, this.ctx.use(Profiles).one(pubkey, relays)], read),
|
||||||
|
() => read([this.index.get(), this.ctx.use(Profiles).get(pubkey)]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
PROFILE,
|
PROFILE,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Profile} from "@welshman/util"
|
import type {Profile} from "@welshman/util"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {DerivedData} from "./clientData.js"
|
import {DerivedData, projection} from "./clientData.js"
|
||||||
|
import type {Projection} from "./clientData.js"
|
||||||
import {Network} from "./network.js"
|
import {Network} from "./network.js"
|
||||||
import {Router} from "./router.js"
|
import {Router} from "./router.js"
|
||||||
import {Thunks} from "./thunk.js"
|
import {Thunks} from "./thunk.js"
|
||||||
@@ -41,13 +42,13 @@ export class Profiles extends DerivedData<ReturnType<typeof readProfile>> {
|
|||||||
return this.ctx.use(Thunks).publish({event, relays})
|
return this.ctx.use(Thunks).publish({event, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
display = (pubkey: string | undefined, ...args: any[]): Readable<string> =>
|
display = (pubkey: string | undefined, ...args: any[]): Projection<string> => {
|
||||||
pubkey ? displayProfile(this.get(pubkey), displayPubkey(pubkey)) : ""
|
const read = ($profile: Maybe<ReturnType<typeof readProfile>>) =>
|
||||||
|
pubkey ? displayProfile($profile, displayPubkey(pubkey)) : ""
|
||||||
|
|
||||||
deriveDisplay = (pubkey: string | undefined, ...args: any[]): Readable<string> =>
|
return projection(
|
||||||
pubkey
|
pubkey ? derived(this.one(pubkey, ...args), read) : readable(""),
|
||||||
? derived(this.one(pubkey, ...args), $profile =>
|
() => read(pubkey ? this.get(pubkey) : undefined),
|
||||||
displayProfile($profile, displayPubkey(pubkey)),
|
)
|
||||||
)
|
}
|
||||||
: readable("")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {fetchone} from "@welshman/lib"
|
import {fetchJson} from "@welshman/lib"
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {displayRelayUrl, displayRelayProfile} from "@welshman/util"
|
import {displayRelayUrl, displayRelayProfile} from "@welshman/util"
|
||||||
import type {RelayProfile} from "@welshman/util"
|
import type {RelayProfile} from "@welshman/util"
|
||||||
import {LoadableData} from "./clientData.js"
|
import {LoadableData, projection} from "./clientData.js"
|
||||||
|
import type {Projection} from "./clientData.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-11 relay profiles, keyed by url. A "local" loadable collection: items
|
* NIP-11 relay profiles, keyed by url. A "local" loadable collection: items
|
||||||
@@ -36,8 +37,9 @@ export class Relays extends LoadableData<RelayProfile> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
display = (url: string) => displayRelayProfile(this.get(url), displayRelayUrl(url))
|
display = (url: string): Projection<string> => {
|
||||||
|
const read = ($relay: Maybe<RelayProfile>) => displayRelayProfile($relay, displayRelayUrl(url))
|
||||||
|
|
||||||
deriveDisplay = (url: string) =>
|
return projection(derived(this.one(url), read), () => read(this.get(url)))
|
||||||
derived(this.one(url), $relay => displayRelayProfile($relay, displayRelayUrl(url)))
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class Searches {
|
|||||||
|
|
||||||
constructor(readonly ctx: IClient) {
|
constructor(readonly ctx: IClient) {
|
||||||
this.profileSearch = derived(
|
this.profileSearch = derived(
|
||||||
[throttled(800, this.ctx.use(Profiles).all), throttled(800, this.ctx.use(Handles).index)],
|
[throttled(800, this.ctx.use(Profiles).all.$), throttled(800, this.ctx.use(Handles).index.$)],
|
||||||
([$profiles, $handlesByNip05]) => {
|
([$profiles, $handlesByNip05]) => {
|
||||||
// Remove invalid nip05's from profiles
|
// Remove invalid nip05's from profiles
|
||||||
const options = $profiles.map(p => {
|
const options = $profiles.map(p => {
|
||||||
@@ -107,7 +107,7 @@ export class Searches {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
this.relaySearch = derived(this.ctx.use(Relays).all, $relays =>
|
this.relaySearch = derived(this.ctx.use(Relays).all.$, $relays =>
|
||||||
createSearch($relays, {
|
createSearch($relays, {
|
||||||
getValue: (relay: RelayProfile) => relay.url,
|
getValue: (relay: RelayProfile) => relay.url,
|
||||||
fuseOptions: {
|
fuseOptions: {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import {get} from "svelte/store"
|
|
||||||
import {uniq, remove} from "@welshman/lib"
|
import {uniq, remove} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
getAddress,
|
getAddress,
|
||||||
@@ -32,7 +31,7 @@ export class Tags {
|
|||||||
"p",
|
"p",
|
||||||
pubkey,
|
pubkey,
|
||||||
this.ctx.use(Router).FromPubkey(pubkey).getUrl() || "",
|
this.ctx.use(Router).FromPubkey(pubkey).getUrl() || "",
|
||||||
get(this.ctx.use(Profiles).display(pubkey)),
|
this.ctx.use(Profiles).display(pubkey).get(),
|
||||||
]
|
]
|
||||||
|
|
||||||
tagEvent = (event: TrustedEvent, url = "", mark = "") => {
|
tagEvent = (event: TrustedEvent, url = "", mark = "") => {
|
||||||
|
|||||||
+169
-119
@@ -2,9 +2,9 @@ import {readable, derived} from "svelte/store"
|
|||||||
import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
|
import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
|
||||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||||
import type {List} from "@welshman/util"
|
import type {List} from "@welshman/util"
|
||||||
import {withGetter} from "@welshman/store"
|
|
||||||
import type {ReadableWithGetter} from "@welshman/store"
|
|
||||||
import type {IClient} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
|
import {projection} from "./clientData.js"
|
||||||
|
import type {Projection} from "./clientData.js"
|
||||||
import {FollowLists} from "./follows.js"
|
import {FollowLists} from "./follows.js"
|
||||||
import {MuteLists} from "./mutes.js"
|
import {MuteLists} from "./mutes.js"
|
||||||
|
|
||||||
@@ -15,96 +15,104 @@ const listPubkeys = (list: List | undefined) => getPubkeyTagValues(getListTags(l
|
|||||||
* built from the perspective of the client's user (or, with no user, the union
|
* 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.
|
* of every known follow list) and updated reactively as lists change.
|
||||||
*
|
*
|
||||||
* The aggregate `*ByPubkey`/`graph`/`max` fields are long-lived `withGetter`
|
* The aggregate `*ByPubkey`/`graph`/`max` fields and the parameterized methods
|
||||||
* stores (snapshot with `.get()`). The parameterized methods (`follows`,
|
* (`follows`, `wotScore`, …) are all `Projection`s — subscribe via `.$`, snapshot
|
||||||
* `wotScore`, …) return plain on-demand stores — snapshot them with svelte's
|
* via `.get()`.
|
||||||
* `get(...)`.
|
|
||||||
*/
|
*/
|
||||||
export class Wot {
|
export class Wot {
|
||||||
followersByPubkey: ReadableWithGetter<Map<string, Set<string>>>
|
followersByPubkey: Projection<Map<string, Set<string>>>
|
||||||
mutersByPubkey: ReadableWithGetter<Map<string, Set<string>>>
|
mutersByPubkey: Projection<Map<string, Set<string>>>
|
||||||
graph: ReadableWithGetter<Map<string, number>>
|
graph: Projection<Map<string, number>>
|
||||||
max: ReadableWithGetter<number | undefined>
|
max: Projection<number | undefined>
|
||||||
|
|
||||||
constructor(readonly ctx: IClient) {
|
constructor(readonly ctx: IClient) {
|
||||||
this.followersByPubkey = withGetter(
|
const followersByPubkeyStore = readable(new Map<string, Set<string>>(), set =>
|
||||||
readable(new Map<string, Set<string>>(), set =>
|
this.ctx.use(FollowLists).index.$.subscribe(
|
||||||
this.ctx.use(FollowLists).index.subscribe(
|
throttle(1000, lists => {
|
||||||
throttle(1000, lists => {
|
const $followersByPubkey = new Map<string, Set<string>>()
|
||||||
const $followersByPubkey = new Map<string, Set<string>>()
|
|
||||||
|
|
||||||
for (const list of lists.values()) {
|
for (const list of lists.values()) {
|
||||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||||
addToMapKey($followersByPubkey, pubkey, list.event.pubkey)
|
addToMapKey($followersByPubkey, pubkey, list.event.pubkey)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set($followersByPubkey)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
this.mutersByPubkey = withGetter(
|
|
||||||
readable(new Map<string, Set<string>>(), set =>
|
|
||||||
this.ctx.use(MuteLists).index.subscribe(
|
|
||||||
throttle(1000, lists => {
|
|
||||||
const $mutersByPubkey = new Map<string, Set<string>>()
|
|
||||||
|
|
||||||
for (const list of lists.values()) {
|
|
||||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
|
||||||
addToMapKey($mutersByPubkey, pubkey, list.event.pubkey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set($mutersByPubkey)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
this.graph = withGetter(
|
|
||||||
readable(new Map<string, number>(), set => {
|
|
||||||
const rebuild = throttle(1000, () => {
|
|
||||||
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 roots = $pubkey ? listPubkeys($followLists.get($pubkey)) : Array.from($followLists.keys())
|
|
||||||
|
|
||||||
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))) {
|
|
||||||
$graph.set(pubkey, dec($graph.get(pubkey)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set($graph)
|
set($followersByPubkey)
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
const unsubscribers = [
|
|
||||||
this.ctx.use(FollowLists).index.subscribe(rebuild),
|
|
||||||
this.ctx.use(MuteLists).index.subscribe(rebuild),
|
|
||||||
]
|
|
||||||
|
|
||||||
return () => unsubscribers.forEach(unsubscribe => unsubscribe())
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
this.max = withGetter(derived(this.graph, $g => max(Array.from($g.values()))))
|
const mutersByPubkeyStore = readable(new Map<string, Set<string>>(), set =>
|
||||||
|
this.ctx.use(MuteLists).index.$.subscribe(
|
||||||
|
throttle(1000, lists => {
|
||||||
|
const $mutersByPubkey = new Map<string, Set<string>>()
|
||||||
|
|
||||||
|
for (const list of lists.values()) {
|
||||||
|
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||||
|
addToMapKey($mutersByPubkey, pubkey, list.event.pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set($mutersByPubkey)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const graphStore = readable(new Map<string, number>(), set => {
|
||||||
|
const rebuild = throttle(1000, () => {
|
||||||
|
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 roots = $pubkey ? listPubkeys($followLists.get($pubkey)) : Array.from($followLists.keys())
|
||||||
|
|
||||||
|
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))) {
|
||||||
|
$graph.set(pubkey, dec($graph.get(pubkey)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set($graph)
|
||||||
|
})
|
||||||
|
|
||||||
|
const unsubscribers = [
|
||||||
|
this.ctx.use(FollowLists).index.$.subscribe(rebuild),
|
||||||
|
this.ctx.use(MuteLists).index.$.subscribe(rebuild),
|
||||||
|
]
|
||||||
|
|
||||||
|
return () => unsubscribers.forEach(unsubscribe => unsubscribe())
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxStore = derived(graphStore, $g => max(Array.from($g.values())))
|
||||||
|
|
||||||
|
this.followersByPubkey = projection(followersByPubkeyStore)
|
||||||
|
this.mutersByPubkey = projection(mutersByPubkeyStore)
|
||||||
|
this.graph = projection(graphStore)
|
||||||
|
this.max = projection(maxStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
follows = (pubkey: string) =>
|
follows = (pubkey: string): Projection<string[]> => {
|
||||||
derived(this.ctx.use(FollowLists).index, $lists => listPubkeys($lists.get(pubkey)))
|
const read = ($lists: ReadonlyMap<string, List>) => listPubkeys($lists.get(pubkey))
|
||||||
|
|
||||||
mutes = (pubkey: string) =>
|
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||||
derived(this.ctx.use(MuteLists).index, $lists => listPubkeys($lists.get(pubkey)))
|
read(this.ctx.use(FollowLists).index.get()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
network = (pubkey: string) =>
|
mutes = (pubkey: string): Projection<string[]> => {
|
||||||
derived(this.ctx.use(FollowLists).index, $lists => {
|
const read = ($lists: ReadonlyMap<string, List>) => listPubkeys($lists.get(pubkey))
|
||||||
|
|
||||||
|
return projection(derived(this.ctx.use(MuteLists).index.$, read), () =>
|
||||||
|
read(this.ctx.use(MuteLists).index.get()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
network = (pubkey: string): Projection<string[]> => {
|
||||||
|
const read = ($lists: ReadonlyMap<string, List>) => {
|
||||||
const pubkeys = new Set(listPubkeys($lists.get(pubkey)))
|
const pubkeys = new Set(listPubkeys($lists.get(pubkey)))
|
||||||
const network = new Set<string>()
|
const network = new Set<string>()
|
||||||
|
|
||||||
@@ -117,53 +125,95 @@ export class Wot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(network)
|
return Array.from(network)
|
||||||
})
|
}
|
||||||
|
|
||||||
followers = (pubkey: string) =>
|
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||||
derived(this.followersByPubkey, $followers => Array.from($followers.get(pubkey) || []))
|
read(this.ctx.use(FollowLists).index.get()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
muters = (pubkey: string) =>
|
followers = (pubkey: string): Projection<string[]> => {
|
||||||
derived(this.mutersByPubkey, $muters => Array.from($muters.get(pubkey) || []))
|
const read = ($followers: ReadonlyMap<string, Set<string>>) =>
|
||||||
|
Array.from($followers.get(pubkey) || [])
|
||||||
|
|
||||||
followsWhoFollow = (pubkey: string, target: string) =>
|
return projection(derived(this.followersByPubkey.$, read), () =>
|
||||||
derived(this.ctx.use(FollowLists).index, $lists =>
|
read(this.followersByPubkey.get()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
muters = (pubkey: string): Projection<string[]> => {
|
||||||
|
const read = ($muters: ReadonlyMap<string, Set<string>>) =>
|
||||||
|
Array.from($muters.get(pubkey) || [])
|
||||||
|
|
||||||
|
return projection(derived(this.mutersByPubkey.$, read), () => read(this.mutersByPubkey.get()))
|
||||||
|
}
|
||||||
|
|
||||||
|
followsWhoFollow = (pubkey: string, target: string): Projection<string[]> => {
|
||||||
|
const read = ($lists: ReadonlyMap<string, List>) =>
|
||||||
listPubkeys($lists.get(pubkey)).filter(other =>
|
listPubkeys($lists.get(pubkey)).filter(other =>
|
||||||
listPubkeys($lists.get(other)).includes(target),
|
listPubkeys($lists.get(other)).includes(target),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
followsWhoMute = (pubkey: string, target: string) =>
|
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||||
derived(
|
read(this.ctx.use(FollowLists).index.get()),
|
||||||
[this.ctx.use(FollowLists).index, this.ctx.use(MuteLists).index],
|
)
|
||||||
([$follows, $mutes]) =>
|
}
|
||||||
listPubkeys($follows.get(pubkey)).filter(other =>
|
|
||||||
listPubkeys($mutes.get(other)).includes(target),
|
followsWhoMute = (pubkey: string, target: string): Projection<string[]> => {
|
||||||
|
const read = ($follows: ReadonlyMap<string, List>, $mutes: ReadonlyMap<string, List>) =>
|
||||||
|
listPubkeys($follows.get(pubkey)).filter(other =>
|
||||||
|
listPubkeys($mutes.get(other)).includes(target),
|
||||||
|
)
|
||||||
|
|
||||||
|
return projection(
|
||||||
|
derived(
|
||||||
|
[this.ctx.use(FollowLists).index.$, this.ctx.use(MuteLists).index.$],
|
||||||
|
([$follows, $mutes]) => read($follows, $mutes),
|
||||||
|
),
|
||||||
|
() => read(this.ctx.use(FollowLists).index.get(), this.ctx.use(MuteLists).index.get()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
wotScore = (pubkey: string, target: string): Projection<number> => {
|
||||||
|
const read = (
|
||||||
|
$follows: ReadonlyMap<string, List>,
|
||||||
|
$mutes: ReadonlyMap<string, List>,
|
||||||
|
$followers: ReadonlyMap<string, Set<string>>,
|
||||||
|
$muters: ReadonlyMap<string, Set<string>>,
|
||||||
|
) => {
|
||||||
|
let follows: string[]
|
||||||
|
let mutes: string[]
|
||||||
|
|
||||||
|
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) || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
return follows.length - mutes.length
|
||||||
|
}
|
||||||
|
|
||||||
|
return projection(
|
||||||
|
derived(
|
||||||
|
[
|
||||||
|
this.ctx.use(FollowLists).index.$,
|
||||||
|
this.ctx.use(MuteLists).index.$,
|
||||||
|
this.followersByPubkey.$,
|
||||||
|
this.mutersByPubkey.$,
|
||||||
|
],
|
||||||
|
([$follows, $mutes, $followers, $muters]) => read($follows, $mutes, $followers, $muters),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
read(
|
||||||
|
this.ctx.use(FollowLists).index.get(),
|
||||||
|
this.ctx.use(MuteLists).index.get(),
|
||||||
|
this.followersByPubkey.get(),
|
||||||
|
this.mutersByPubkey.get(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
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))
|
|
||||||
|
|
||||||
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
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import {
|
|||||||
batcher,
|
batcher,
|
||||||
postJson,
|
postJson,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {getTagValue, getZapSplits, zapFromEvent} from "@welshman/util"
|
import {getTagValue, getZapSplits, zapFromEvent} from "@welshman/util"
|
||||||
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
|
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
|
||||||
import {deriveDeduplicated, deriveDeduplicatedByValue} from "@welshman/store"
|
import {deriveDeduplicated, deriveDeduplicatedByValue} from "@welshman/store"
|
||||||
import {LoadableData} from "./clientData.js"
|
import {LoadableData, projection} from "./clientData.js"
|
||||||
|
import type {Projection} from "./clientData.js"
|
||||||
import type {IClient} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import {Profiles} from "./profiles.js"
|
import {Profiles} from "./profiles.js"
|
||||||
|
|
||||||
@@ -67,13 +69,15 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
return $profile?.lnurl ? this.load($profile.lnurl) : undefined
|
return $profile?.lnurl ? this.load($profile.lnurl) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
forPubkey = (pubkey: string, relays: string[] = []) => {
|
forPubkey = (pubkey: string, relays: string[] = []): Projection<Maybe<Zapper>> => {
|
||||||
this.loadForPubkey(pubkey, relays)
|
this.loadForPubkey(pubkey, relays)
|
||||||
|
|
||||||
return deriveDeduplicated(
|
const read = ([$zappersByLnurl, $profile]: [ReadonlyMap<string, Zapper>, Maybe<{lnurl?: string}>]) =>
|
||||||
[this.index, this.ctx.use(Profiles).one(pubkey, relays)],
|
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined
|
||||||
([$zappersByLnurl, $profile]) =>
|
|
||||||
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined,
|
return projection(
|
||||||
|
deriveDeduplicated([this.index.$, this.ctx.use(Profiles).one(pubkey, relays)], read),
|
||||||
|
() => read([this.index.get(), this.ctx.use(Profiles).get(pubkey)]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +109,7 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
await Promise.all(zapReceipts.map(zapReceipt => this.validateZapReceipt(zapReceipt, parent))),
|
await Promise.all(zapReceipts.map(zapReceipt => this.validateZapReceipt(zapReceipt, parent))),
|
||||||
)
|
)
|
||||||
|
|
||||||
validZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Readable<Zap[]> => {
|
validZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Projection<Zap[]> => {
|
||||||
const splits = getZapSplits(parent)
|
const splits = getZapSplits(parent)
|
||||||
const profiles = this.ctx.use(Profiles)
|
const profiles = this.ctx.use(Profiles)
|
||||||
|
|
||||||
@@ -114,12 +118,7 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
this.loadForPubkey(split.pubkey, removeUndefined([split.relay]))
|
this.loadForPubkey(split.pubkey, removeUndefined([split.relay]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const stores: Readable<any>[] = [
|
const read = (values: any[]) => {
|
||||||
this.index,
|
|
||||||
...splits.map(split => profiles.one(split.pubkey)),
|
|
||||||
]
|
|
||||||
|
|
||||||
return deriveDeduplicatedByValue(stores, (values: any[]) => {
|
|
||||||
const $zappersByLnurl = values[0] as Map<string, Zapper>
|
const $zappersByLnurl = values[0] as Map<string, Zapper>
|
||||||
const $profiles = values.slice(1) as Array<{lnurl?: string} | undefined>
|
const $profiles = values.slice(1) as Array<{lnurl?: string} | undefined>
|
||||||
|
|
||||||
@@ -140,6 +139,13 @@ export class Zappers extends LoadableData<Zapper> {
|
|||||||
return zapper ? zapFromEvent(zapReceipt, zapper) : undefined
|
return zapper ? zapFromEvent(zapReceipt, zapper) : undefined
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const stores: Readable<any>[] = [this.index.$, ...splits.map(split => profiles.one(split.pubkey))]
|
||||||
|
|
||||||
|
return projection(
|
||||||
|
deriveDeduplicatedByValue(stores, read),
|
||||||
|
() => read([this.index.get(), ...splits.map(split => profiles.get(split.pubkey))]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user