Add projection utility and type
tests / tests (push) Failing after 5m56s

This commit is contained in:
Jon Staab
2026-06-18 10:15:28 -07:00
parent aae201414d
commit 72ab746254
9 changed files with 267 additions and 191 deletions
+40 -25
View File
@@ -2,29 +2,41 @@ import {writable} from "svelte/store"
import type {Readable, Unsubscriber} from "svelte/store"
import type {Maybe} from "@welshman/lib"
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 {deriveItems, getter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
import type {EventToItem, ItemsByKey, MakeLoadItemOptions} from "@welshman/store"
import type {IClient} from "./client.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 —
* things like relay stats or NIP-11 profiles that aren't backed by the
* 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> {
index = withGetter(writable(new Map<string, T>()))
all = withGetter(deriveItems(this.index))
protected store = writable(new Map<string, T>())
index: Projection<ItemsByKey<T>>
all: Projection<T[]>
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
subs: ((key: string, value: Maybe<T>) => void)[] = []
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()
@@ -34,7 +46,7 @@ export class ClientData<T> {
get = (key: string) => this.index.get().get(key)
set = (key: string, value: T) => {
this.index.update($items => {
this.store.update($items => {
$items.set(key, value)
return $items
@@ -44,7 +56,7 @@ export class ClientData<T> {
}
delete = (key: string) => {
this.index.update($items => {
this.store.update($items => {
$items.delete(key)
return $items
@@ -56,7 +68,7 @@ export class ClientData<T> {
clear = () => {
const keys = Array.from(this.index.get().keys())
this.index.set(new Map())
this.store.set(new Map())
for (const key of keys) {
this.emitItem(key, undefined)
@@ -102,7 +114,7 @@ export abstract class LoadableData<T> extends ClientData<T> {
this.load = makeLoadItem(fetch, read, options)
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`
* (how to load an item by key from the network) and pass the filters/decoder via
* `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> {
index: ReadableWithGetter<ItemsByKey<T>>
all: ReadableWithGetter<T[]>
index: Projection<ItemsByKey<T>>
all: Projection<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>>
@@ -133,21 +148,21 @@ export abstract class DerivedData<T> {
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 index = ctx.use(Stores).itemsByKey<T>({
filters: options.filters,
eventToItem: options.eventToItem,
getKey: options.getKey,
})
this.index = projection(index)
this.all = projection(deriveItems(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)
this.one = makeDeriveItem(index, this.load)
}
keys = () => this.index.get().keys()
+3 -4
View File
@@ -1,4 +1,3 @@
import {get} from "svelte/store"
import {Scope, FeedController} from "@welshman/feeds"
import type {FeedControllerOptions, Feed} from "@welshman/feeds"
import type {AdapterContext} from "@welshman/net"
@@ -27,11 +26,11 @@ export class Feeds {
case Scope.Self:
return [$pubkey]
case Scope.Follows:
return get(this.ctx.use(Wot).follows($pubkey))
return this.ctx.use(Wot).follows($pubkey).get()
case Scope.Network:
return get(this.ctx.use(Wot).network($pubkey))
return this.ctx.use(Wot).network($pubkey).get()
case Scope.Followers:
return get(this.ctx.use(Wot).followers($pubkey))
return this.ctx.use(Wot).followers($pubkey).get()
default:
return []
}
+14 -10
View File
@@ -1,8 +1,10 @@
import {tryCatch, batcher, postJson} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {queryProfile, displayNip05} from "@welshman/util"
import type {Handle} from "@welshman/util"
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 {Profiles} from "./profiles.js"
@@ -61,20 +63,22 @@ export class Handles extends LoadableData<Handle> {
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)
return deriveDeduplicated(
[this.index, this.ctx.use(Profiles).one(pubkey, relays)],
([$handlesByNip05, $profile]) => {
if (!$profile?.nip05) return undefined
const read = ([$handlesByNip05, $profile]: [ReadonlyMap<string, Handle>, Maybe<{nip05?: string}>]) => {
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)]),
)
}
+11 -10
View File
@@ -9,8 +9,9 @@ import {
PROFILE,
} from "@welshman/util"
import type {Profile} from "@welshman/util"
import type {Readable} from "svelte/store"
import {DerivedData} from "./clientData.js"
import type {Maybe} from "@welshman/lib"
import {DerivedData, projection} from "./clientData.js"
import type {Projection} from "./clientData.js"
import {Network} from "./network.js"
import {Router} from "./router.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})
}
display = (pubkey: string | undefined, ...args: any[]): Readable<string> =>
pubkey ? displayProfile(this.get(pubkey), displayPubkey(pubkey)) : ""
display = (pubkey: string | undefined, ...args: any[]): Projection<string> => {
const read = ($profile: Maybe<ReturnType<typeof readProfile>>) =>
pubkey ? displayProfile($profile, displayPubkey(pubkey)) : ""
deriveDisplay = (pubkey: string | undefined, ...args: any[]): Readable<string> =>
pubkey
? derived(this.one(pubkey, ...args), $profile =>
displayProfile($profile, displayPubkey(pubkey)),
)
: readable("")
return projection(
pubkey ? derived(this.one(pubkey, ...args), read) : readable(""),
() => read(pubkey ? this.get(pubkey) : undefined),
)
}
}
+7 -5
View File
@@ -1,9 +1,10 @@
import {derived} from "svelte/store"
import {fetchone} from "@welshman/lib"
import {fetchJson} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {displayRelayUrl, displayRelayProfile} 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
@@ -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) =>
derived(this.one(url), $relay => displayRelayProfile($relay, displayRelayUrl(url)))
return projection(derived(this.one(url), read), () => read(this.get(url)))
}
}
+2 -2
View File
@@ -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).index)],
[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(this.ctx.use(Relays).all, $relays =>
this.relaySearch = derived(this.ctx.use(Relays).all.$, $relays =>
createSearch($relays, {
getValue: (relay: RelayProfile) => relay.url,
fuseOptions: {
+1 -2
View File
@@ -1,4 +1,3 @@
import {get} from "svelte/store"
import {uniq, remove} from "@welshman/lib"
import {
getAddress,
@@ -32,7 +31,7 @@ export class Tags {
"p",
pubkey,
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 = "") => {
+169 -119
View File
@@ -2,9 +2,9 @@ import {readable, derived} from "svelte/store"
import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} 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 {projection} from "./clientData.js"
import type {Projection} from "./clientData.js"
import {FollowLists} from "./follows.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
* of every known follow list) and updated reactively as lists change.
*
* 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(...)`.
* The aggregate `*ByPubkey`/`graph`/`max` fields and the parameterized methods
* (`follows`, `wotScore`, …) are all `Projection`s — subscribe via `.$`, snapshot
* via `.get()`.
*/
export class Wot {
followersByPubkey: ReadableWithGetter<Map<string, Set<string>>>
mutersByPubkey: ReadableWithGetter<Map<string, Set<string>>>
graph: ReadableWithGetter<Map<string, number>>
max: ReadableWithGetter<number | undefined>
followersByPubkey: Projection<Map<string, Set<string>>>
mutersByPubkey: Projection<Map<string, Set<string>>>
graph: Projection<Map<string, number>>
max: Projection<number | undefined>
constructor(readonly ctx: IClient) {
this.followersByPubkey = withGetter(
readable(new Map<string, Set<string>>(), set =>
this.ctx.use(FollowLists).index.subscribe(
throttle(1000, lists => {
const $followersByPubkey = new Map<string, Set<string>>()
const followersByPubkeyStore = readable(new Map<string, Set<string>>(), set =>
this.ctx.use(FollowLists).index.$.subscribe(
throttle(1000, lists => {
const $followersByPubkey = new Map<string, Set<string>>()
for (const list of lists.values()) {
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
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)))
for (const list of lists.values()) {
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
addToMapKey($followersByPubkey, pubkey, list.event.pubkey)
}
}
set($graph)
})
const unsubscribers = [
this.ctx.use(FollowLists).index.subscribe(rebuild),
this.ctx.use(MuteLists).index.subscribe(rebuild),
]
return () => unsubscribers.forEach(unsubscribe => unsubscribe())
}),
set($followersByPubkey)
}),
),
)
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) =>
derived(this.ctx.use(FollowLists).index, $lists => listPubkeys($lists.get(pubkey)))
follows = (pubkey: string): Projection<string[]> => {
const read = ($lists: ReadonlyMap<string, List>) => listPubkeys($lists.get(pubkey))
mutes = (pubkey: string) =>
derived(this.ctx.use(MuteLists).index, $lists => listPubkeys($lists.get(pubkey)))
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
read(this.ctx.use(FollowLists).index.get()),
)
}
network = (pubkey: string) =>
derived(this.ctx.use(FollowLists).index, $lists => {
mutes = (pubkey: string): Projection<string[]> => {
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 network = new Set<string>()
@@ -117,53 +125,95 @@ export class Wot {
}
return Array.from(network)
})
}
followers = (pubkey: string) =>
derived(this.followersByPubkey, $followers => Array.from($followers.get(pubkey) || []))
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
read(this.ctx.use(FollowLists).index.get()),
)
}
muters = (pubkey: string) =>
derived(this.mutersByPubkey, $muters => Array.from($muters.get(pubkey) || []))
followers = (pubkey: string): Projection<string[]> => {
const read = ($followers: ReadonlyMap<string, Set<string>>) =>
Array.from($followers.get(pubkey) || [])
followsWhoFollow = (pubkey: string, target: string) =>
derived(this.ctx.use(FollowLists).index, $lists =>
return projection(derived(this.followersByPubkey.$, read), () =>
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(other)).includes(target),
),
)
)
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),
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
read(this.ctx.use(FollowLists).index.get()),
)
}
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
},
)
}
}
+20 -14
View File
@@ -8,10 +8,12 @@ import {
batcher,
postJson,
} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {getTagValue, getZapSplits, zapFromEvent} from "@welshman/util"
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
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 {Profiles} from "./profiles.js"
@@ -67,13 +69,15 @@ export class Zappers extends LoadableData<Zapper> {
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)
return deriveDeduplicated(
[this.index, this.ctx.use(Profiles).one(pubkey, relays)],
([$zappersByLnurl, $profile]) =>
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined,
const read = ([$zappersByLnurl, $profile]: [ReadonlyMap<string, Zapper>, Maybe<{lnurl?: string}>]) =>
$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))),
)
validZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Readable<Zap[]> => {
validZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Projection<Zap[]> => {
const splits = getZapSplits(parent)
const profiles = this.ctx.use(Profiles)
@@ -114,12 +118,7 @@ export class Zappers extends LoadableData<Zapper> {
this.loadForPubkey(split.pubkey, removeUndefined([split.relay]))
}
const stores: Readable<any>[] = [
this.index,
...splits.map(split => profiles.one(split.pubkey)),
]
return deriveDeduplicatedByValue(stores, (values: any[]) => {
const read = (values: any[]) => {
const $zappersByLnurl = values[0] as Map<string, Zapper>
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
}),
)
})
}
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))]),
)
}
}