Clean up store semantics
This commit is contained in:
@@ -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]}],
|
||||
|
||||
@@ -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]}],
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
@@ -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]}],
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]}],
|
||||
|
||||
@@ -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]}],
|
||||
|
||||
@@ -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]}],
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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]}],
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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]}],
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
Reference in New Issue
Block a user