Migrate collections to new stores, change some conventions

This commit is contained in:
Jon Staab
2025-11-19 16:47:17 -08:00
parent d197acc41e
commit 6d36f5a912
29 changed files with 354 additions and 414 deletions
+3 -3
View File
@@ -9,11 +9,11 @@ removeRelay(url: string, mode: RelayMode): Promise<Thunk>
addRelay(url: string, mode: RelayMode): Promise<Thunk>
```
## Inbox Relay Management (NIP 17)
## Messaging Relay Management (NIP 17)
```typescript
removeInboxRelay(url: string): Promise<Thunk>
addInboxRelay(url: string): Promise<Thunk>
removeMessagingRelay(url: string): Promise<Thunk>
addMessagingRelay(url: string): Promise<Thunk>
```
## Profile Management (NIP 01)
+1 -1
View File
@@ -36,7 +36,7 @@ defaultSocketPolicies.push(
// This will fetch the user's profile automatically, and return a store that updates
// automatically. Several different stores exist that are ready to go, including handles,
// zappers, relaySelections, relays, follows, mutes.
// zappers, relayLists, relays, follows, mutes.
const profile = deriveProfile(pubkey.get())
// Publish is done using thunks, which optimistically publish to the local database, deferring
+2 -2
View File
@@ -59,8 +59,8 @@ pins → pinsByPubkey → derivePins → loadPins
// Relays
relays relaysByUrl deriveRelay loadRelay
relaySelections relaySelectionsByPubkey deriveRelaySelections loadRelaySelections
inboxRelaySelections inboxRelaySelectionsByPubkey deriveInboxRelaySelections loadInboxRelaySelections
relayLists relayListsByPubkey deriveRelayLists loadRelayLists
messagingRelayLists messagingRelayListsByPubkey deriveMessagingRelayLists loadMessagingRelayLists
// Identity
handles handlesByNip05 deriveHandle loadHandle
+2 -2
View File
@@ -57,8 +57,8 @@ Several thunk factories are provided for common or more complicated scenarios li
- `removeRelay(url: string, mode: RelayMode)`
- `addRelay(url: string, mode: RelayMode)`
- `removeInboxRelay(url: string)`
- `addInboxRelay(url: string)`
- `removeMessagingRelay(url: string)`
- `addMessagingRelay(url: string)`
- `setProfile(profile: Profile)`
- `unfollow(value: string)`
- `follow(tag: string[])`
+8 -8
View File
@@ -39,10 +39,10 @@ export const userMutes: Store<List | undefined>
export const userPins: Store<List | undefined>
// User relay selections
export const userRelaySelections: Store<List | undefined>
export const userRelayLists: Store<List | undefined>
// User inbox relay selections
export const userInboxRelaySelections: Store<List | undefined>
// User messaging relay selections
export const userMessagingRelayLists: Store<List | undefined>
// User blossom servers
export const userBlossomServers: Store<List | undefined>
@@ -66,10 +66,10 @@ function loadUserMutes(relays?: string[], force?: boolean): Promise<void>
function loadUserPins(relays?: string[], force?: boolean): Promise<void>
// Load user relay selections
function loadUserRelaySelections(relays?: string[], force?: boolean): Promise<void>
function loadUserRelayLists(relays?: string[], force?: boolean): Promise<void>
// Load user inbox relay selections
function loadUserInboxRelaySelections(relays?: string[], force?: boolean): Promise<void>
// Load user messaging relay selections
function loadUserMessagingRelayLists(relays?: string[], force?: boolean): Promise<void>
// Load user blossom servers
function loadUserBlossomServers(relays?: string[], force?: boolean): Promise<void>
@@ -94,13 +94,13 @@ const follows = userFollows.get()
### Manual Loading
```typescript
import { loadUserMutes, loadUserRelaySelections } from '@welshman/app'
import { loadUserMutes, loadUserRelayLists } from '@welshman/app'
// Load user mutes from specific relays
await loadUserMutes(['wss://relay1.com', 'wss://relay2.com'])
// Force refresh user relay selections
await loadUserRelaySelections([], true)
await loadUserRelayLists([], true)
// Load from default relays
await loadUserProfile()
+5 -5
View File
@@ -31,7 +31,7 @@ const router = Router.get()
// Get relays for reading events from specific pubkeys
const readRelays = router.FromPubkeys(['pubkey1', 'pubkey2']).getUrls()
// Get relays for publishing an event (author's outbox + mentions' inboxes)
// Get relays for publishing an event (author's outbox + mentions' messaginges)
const publishRelays = router.PublishEvent(event).getUrls()
// Try hard to find a quoted note with maximal fallbacks
@@ -65,7 +65,7 @@ The main class for relay selection. Configure it once with your relay discovery
**Configuration Options:**
- `getUserPubkey()` - Returns the current user's pubkey
- `getPubkeyRelays(pubkey, mode)` - Returns relays for a pubkey ("read", "write", or "inbox")
- `getPubkeyRelays(pubkey, mode)` - Returns relays for a pubkey ("read", "write", or "messaging")
- `getDefaultRelays()` - Returns fallback relays
- `getIndexerRelays()` - Returns relays that index profiles and relay lists
- `getSearchRelays()` - Returns relays that support NIP-50 search
@@ -74,8 +74,8 @@ The main class for relay selection. Configure it once with your relay discovery
**Scenario Methods:**
- `FromRelays(relays)` - Use specific relays
- `ForUser()` / `FromUser()` / `UserInbox()` - User's read/write/inbox relays
- `ForPubkey(pubkey)` / `FromPubkey(pubkey)` / `PubkeyInbox(pubkey)` - Pubkey's relays
- `ForUser()` / `FromUser()` / `UserMessaging()` - User's read/write/messaging relays
- `ForPubkey(pubkey)` / `FromPubkey(pubkey)` / `PubkeyMessaging(pubkey)` - Pubkey's relays
- `ForPubkeys(pubkeys)` / `FromPubkeys(pubkeys)` - Multiple pubkeys' relays
- `Event(event)` - Relays for an event's author
- `PublishEvent(event)` - Relays for publishing (author + mentions)
@@ -105,7 +105,7 @@ Functions that determine how many fallback relays to add:
`getFilterSelections(filters)` automatically chooses appropriate relays based on filter content:
- Search filters → search relays
- Wrap events → user's inbox
- Wrap events → user's messaging
- Profile/relay kinds → indexer relays
- Author filters → authors' relays
- Everything else → user's relays (low weight)
+1 -1
View File
@@ -11,7 +11,7 @@ The `Relay` module provides utilities for working with Nostr relays, including U
export enum RelayMode {
Read = "read",
Write = "write",
Inbox = "inbox"
Messaging = "messaging"
}
// Relay information from NIP-11
+9 -9
View File
@@ -2,23 +2,23 @@ import {BLOSSOM_SERVERS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js"
import {makeOutboxLoader} from "./relaySelections.js"
import {makeOutboxLoader} from "./relayLists.js"
export const blossomServersByPubkey = deriveItemsByKey({
export const blossomServerListsByPubkey = deriveItemsByKey({
repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [BLOSSOM_SERVERS]}],
getKey: blossomServers => blossomServers.event.pubkey,
getKey: blossomServerList => blossomServerList.event.pubkey,
})
export const blossomServers = deriveItems(blossomServersByPubkey)
export const blossomServerLists = deriveItems(blossomServerListsByPubkey)
export const getBlossomServersByPubkey = getter(blossomServersByPubkey)
export const getBlossomServerListsByPubkey = getter(blossomServerListsByPubkey)
export const getBlossomServers = (pubkey: string) => getBlossomServersByPubkey().get(pubkey)
export const getBlossomServerList = (pubkey: string) => getBlossomServerListsByPubkey().get(pubkey)
export const forceLoadBlossomServers = makeForceLoadItem(makeOutboxLoader(BLOSSOM_SERVERS), getBlossomServers)
export const forceLoadBlossomServerList = makeForceLoadItem(makeOutboxLoader(BLOSSOM_SERVERS), getBlossomServerList)
export const loadBlossomServers = makeLoadItem(makeOutboxLoader(BLOSSOM_SERVERS), getBlossomServers)
export const loadBlossomServerList = makeLoadItem(makeOutboxLoader(BLOSSOM_SERVERS), getBlossomServerList)
export const deriveBlossomServers = makeDeriveItem(blossomServersByPubkey, loadBlossomServers)
export const deriveBlossomServerList = makeDeriveItem(blossomServerListsByPubkey, loadBlossomServerList)
+38 -38
View File
@@ -23,7 +23,7 @@ import {
createProfile,
editProfile,
RelayMode,
INBOX_RELAYS,
MESSAGING_RELAYS,
FOLLOWS,
RELAYS,
MUTES,
@@ -32,16 +32,16 @@ import {
import type {RoomMeta, Profile} from "@welshman/util"
import {Router, addMaximalFallbacks} from "@welshman/router"
import {
userRelaySelections,
loadUserRelaySelections,
userInboxRelaySelections,
loadUserInboxRelaySelections,
userFollows,
loadUserFollows,
userMutes,
loadUserMutes,
userPins,
loadUserPins,
userRelayList,
forceLoadUserRelayList,
userMessagingRelayList,
forceLoadUserMessagingRelayList,
userFollowList,
forceLoadUserFollowList,
userMuteList,
forceLoadUserMuteList,
userPinList,
forceLoadUserPinList,
} from "./user.js"
import {nip44EncryptToSelf, signer} from "./session.js"
import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
@@ -49,9 +49,9 @@ import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
// NIP 65
export const removeRelay = async (url: string, mode: RelayMode) => {
await loadUserRelaySelections([], true)
await forceLoadUserRelayList([])
const list = get(userRelaySelections) || makeList({kind: RELAYS})
const list = get(userRelayList) || makeList({kind: RELAYS})
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
const alt = mode === RelayMode.Read ? RelayMode.Write : RelayMode.Read
const tags = list.publicTags.filter(nthNe(1, url))
@@ -71,9 +71,9 @@ export const removeRelay = async (url: string, mode: RelayMode) => {
}
export const addRelay = async (url: string, mode: RelayMode) => {
await loadUserRelaySelections([], true)
await forceLoadUserRelayList([])
const list = get(userRelaySelections) || makeList({kind: RELAYS})
const list = get(userRelayList) || makeList({kind: RELAYS})
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
const tag = removeUndefined(["r", url, dup && dup[2] !== mode ? undefined : mode])
const tags = [...list.publicTags.filter(nthNe(1, url)), tag]
@@ -85,20 +85,20 @@ export const addRelay = async (url: string, mode: RelayMode) => {
// NIP 17
export const removeInboxRelay = async (url: string) => {
await loadUserInboxRelaySelections([], true)
export const removeMessagingRelay = async (url: string) => {
await forceLoadUserMessagingRelayList([])
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
const list = get(userMessagingRelayList) || makeList({kind: MESSAGING_RELAYS})
const event = await removeFromList(list, url).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
return publishThunk({event, relays})
}
export const addInboxRelay = async (url: string) => {
await loadUserInboxRelaySelections([], true)
export const addMessagingRelay = async (url: string) => {
await forceLoadUserMessagingRelayList([])
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
const list = get(userMessagingRelayList) || makeList({kind: MESSAGING_RELAYS})
const event = await addToListPublicly(list, ["relay", url]).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -118,9 +118,9 @@ export const setProfile = (profile: Profile) => {
// NIP 02
export const unfollow = async (value: string) => {
await loadUserFollows([], true)
await forceLoadUserFollowList([])
const list = get(userFollows) || makeList({kind: FOLLOWS})
const list = get(userFollowList) || makeList({kind: FOLLOWS})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -128,9 +128,9 @@ export const unfollow = async (value: string) => {
}
export const follow = async (tag: string[]) => {
await loadUserFollows([], true)
await forceLoadUserFollowList([])
const list = get(userFollows) || makeList({kind: FOLLOWS})
const list = get(userFollowList) || makeList({kind: FOLLOWS})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -138,9 +138,9 @@ export const follow = async (tag: string[]) => {
}
export const unmute = async (value: string) => {
await loadUserMutes([], true)
await forceLoadUserMuteList([])
const list = get(userMutes) || makeList({kind: MUTES})
const list = get(userMuteList) || makeList({kind: MUTES})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -148,9 +148,9 @@ export const unmute = async (value: string) => {
}
export const mutePublicly = async (tag: string[]) => {
await loadUserMutes([], true)
await forceLoadUserMuteList([])
const list = get(userMutes) || makeList({kind: MUTES})
const list = get(userMuteList) || makeList({kind: MUTES})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -158,9 +158,9 @@ export const mutePublicly = async (tag: string[]) => {
}
export const mutePrivately = async (tag: string[]) => {
await loadUserMutes([], true)
await forceLoadUserMuteList([])
const list = get(userMutes) || makeList({kind: MUTES})
const list = get(userMuteList) || makeList({kind: MUTES})
const event = await addToListPrivately(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -174,9 +174,9 @@ export const setMutes = async ({
publicTags?: string[][]
privateTags?: string[][]
}) => {
await loadUserMutes([], true)
await forceLoadUserMuteList([])
const list = get(userMutes) || makeList({kind: MUTES})
const list = get(userMuteList) || makeList({kind: MUTES})
const event = await updateList(list, {publicTags, privateTags}).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -184,9 +184,9 @@ export const setMutes = async ({
}
export const unpin = async (value: string) => {
await loadUserPins([], true)
await forceLoadUserPinList([])
const list = get(userPins) || makeList({kind: PINS})
const list = get(userPinList) || makeList({kind: PINS})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -194,9 +194,9 @@ export const unpin = async (value: string) => {
}
export const pin = async (tag: string[]) => {
await loadUserPins([], true)
await forceLoadUserPinList([])
const list = get(userPins) || makeList({kind: PINS})
const list = get(userPinList) || makeList({kind: PINS})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -213,7 +213,7 @@ export type SendWrappedOptions = Omit<ThunkOptions, "event" | "relays"> & {
export const sendWrapped = ({event, recipients, ...options}: SendWrappedOptions) =>
new MergedThunk(
uniq(recipients).map(recipient => {
const relays = Router.get().PubkeyInbox(recipient).getUrls()
const relays = Router.get().MessagesForPubkey(recipient).getUrls()
return publishThunk({event, relays, recipient, ...options})
}),
-50
View File
@@ -1,55 +1,5 @@
import {throttle} from "@welshman/lib"
import {Repository, Tracker} from "@welshman/net"
import {custom} from "@welshman/store"
export const tracker = new Tracker()
export const repository = Repository.get()
// Adapt objects to stores
export const makeRepositoryStore = ({throttle: t = 300}: {throttle?: number} = {}) =>
custom(
setter => {
let onUpdate = () => setter(repository)
if (t) {
onUpdate = throttle(t, onUpdate)
}
onUpdate()
repository.on("update", onUpdate)
return () => repository.off("update", onUpdate)
},
{
onUpdate: (other: Repository) => repository.load(other.dump()),
},
)
export const makeTrackerStore = ({throttle: t = 300}: {throttle?: number} = {}) =>
custom(
setter => {
let onUpdate = () => setter(tracker)
if (t) {
onUpdate = throttle(t, onUpdate)
}
onUpdate()
tracker.on("add", onUpdate)
tracker.on("remove", onUpdate)
tracker.on("load", onUpdate)
tracker.on("clear", onUpdate)
return () => {
tracker.off("add", onUpdate)
tracker.off("remove", onUpdate)
tracker.off("load", onUpdate)
tracker.off("clear", onUpdate)
}
},
{
onUpdate: (other: Tracker) => tracker.load(other.relaysById),
},
)
+12 -10
View File
@@ -2,23 +2,25 @@ import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js"
import {makeOutboxLoader} from "./relaySelections.js"
import {makeOutboxLoader} from "./relayLists.js"
export const followsByPubkey = deriveItemsByKey({
export const followListsByPubkey = deriveItemsByKey({
repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [FOLLOWS]}],
getKey: follows => follows.event.pubkey,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
getKey: followList => followList.event.pubkey,
})
export const follows = deriveItems(followsByPubkey)
export const followLists = deriveItems(followListsByPubkey)
export const getFollowsByPubkey = getter(followsByPubkey)
export const getFollowListsByPubkey = getter(followListsByPubkey)
export const getFollows = (pubkey: string) => getFollowsByPubkey().get(pubkey)
export const getFollowLists = getter(followLists)
export const forceLoadFollows = makeForceLoadItem(makeOutboxLoader(FOLLOWS), getFollows)
export const getFollowList = (pubkey: string) => getFollowListsByPubkey().get(pubkey)
export const loadFollows = makeLoadItem(makeOutboxLoader(FOLLOWS), getFollows)
export const forceLoadFollowList = makeForceLoadItem(makeOutboxLoader(FOLLOWS), getFollowList)
export const deriveFollows = makeDeriveItem(followsByPubkey, loadFollows)
export const loadFollowList = makeLoadItem(makeOutboxLoader(FOLLOWS), getFollowList)
export const deriveFollowList = makeDeriveItem(followListsByPubkey, loadFollowList)
+39 -42
View File
@@ -1,7 +1,7 @@
import {writable, derived} from "svelte/store"
import {tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib"
import {collection} from "@welshman/store"
import {deriveProfile} from "./profiles.js"
import {getter, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem} from "@welshman/store"
import {deriveProfile, loadProfile} from "./profiles.js"
import {appContext} from "./context.js"
export type Handle = {
@@ -40,16 +40,23 @@ export async function queryProfile(nip05: string) {
}
}
export const handles = writable<Handle[]>([])
export const handlesByNip05 = writable(new Map<string, Handle>())
export const fetchHandles = async (nip05s: string[]) => {
const base = appContext.dufflepudUrl!
export const handles = deriveItems(handlesByNip05)
export const getHandlesByNip05 = getter(handlesByNip05)
export const getHandles = getter(handles)
export const getHandle = (nip05: string) => getHandlesByNip05().get(nip05)
export const fetchHandle = batcher(800, async (nip05s: string[]) => {
const handlesByNip05 = new Map<string, Handle>()
// Use dufflepud if we it's set up to protect user privacy, otherwise fetch directly
if (base) {
if (appContext.dufflepudUrl) {
const res: any = await tryCatch(
async () => await postJson(`${base}/handle/info`, {handles: nip05s}),
async () => await postJson(`${appContext.dufflepudUrl}/handle/info`, {handles: nip05s}),
)
for (const {handle: nip05, info} of res?.data || []) {
@@ -72,50 +79,40 @@ export const fetchHandles = async (nip05s: string[]) => {
}
}
return handlesByNip05
}
return nip05s.map(nip05 => {
const info = handlesByNip05.get(nip05)
export const {
indexStore: handlesByNip05,
deriveItem: deriveHandle,
loadItem: loadHandle,
onItem: onHandle,
} = collection({
name: "handles",
store: handles,
getKey: (handle: Handle) => handle.nip05,
load: batcher(800, async (nip05s: string[]) => {
const fresh = await fetchHandles(uniq(nip05s))
const stale = handlesByNip05.get()
for (const nip05 of nip05s) {
const newHandle = fresh.get(nip05)
if (newHandle) {
stale.set(nip05, {...newHandle, nip05})
}
if (info) {
return {...info, nip05}
}
handles.set(Array.from(stale.values()))
return nip05s
}),
})
})
export const deriveHandleForPubkey = (pubkey: string, relays: string[] = []) =>
derived([handlesByNip05, deriveProfile(pubkey, relays)], ([$handlesByNip05, $profile]) => {
if (!$profile?.nip05) {
return undefined
}
export const forceLoadHandle = makeForceLoadItem(fetchHandle, getHandle)
loadHandle($profile.nip05)
export const loadHandle = makeLoadItem(fetchHandle, getHandle)
export const deriveHandle = makeDeriveItem(handlesByNip05, loadHandle)
export const loadHandleForPubkey = async (pubkey: string, relays: string[] = []) => {
const $profile = await loadProfile(pubkey, relays)
return $profile?.nip05 ? loadHandle($profile.nip05) : undefined
}
export const deriveHandleForPubkey = (pubkey: string, relays: string[] = []) => {
loadHandleForPubkey(pubkey, relays)
return derived([handlesByNip05, deriveProfile(pubkey, relays)], ([$handlesByNip05, $profile]) => {
if (!$profile?.nip05) return undefined
const handle = $handlesByNip05.get($profile.nip05)
if (handle?.pubkey === pubkey) {
return handle
}
if (handle?.pubkey !== pubkey) return undefined
return handle
})
}
export const displayNip05 = (nip05: string) =>
nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05
-24
View File
@@ -1,24 +0,0 @@
import {INBOX_RELAYS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js"
import {makeOutboxLoader} from "./relaySelections.js"
export const inboxRelaySelectionsByPubkey = deriveItemsByKey({
repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [INBOX_RELAYS]}],
getKey: inboxRelaySelections => inboxRelaySelections.event.pubkey,
})
export const inboxRelaySelections = deriveItems(inboxRelaySelectionsByPubkey)
export const getInboxRelaySelectionsByPubkey = getter(inboxRelaySelectionsByPubkey)
export const getInboxRelaySelections = (pubkey: string) => getInboxRelaySelectionsByPubkey().get(pubkey)
export const forceLoadInboxRelaySelections = makeForceLoadItem(makeOutboxLoader(INBOX_RELAYS), getInboxRelaySelections)
export const loadInboxRelaySelections = makeLoadItem(makeOutboxLoader(INBOX_RELAYS), getInboxRelaySelections)
export const deriveInboxRelaySelections = makeDeriveItem(inboxRelaySelectionsByPubkey, loadInboxRelaySelections)
+12 -12
View File
@@ -11,8 +11,8 @@ export * from "./profiles.js"
export * from "./pins.js"
export * from "./relays.js"
export * from "./relayStats.js"
export * from "./relaySelections.js"
export * from "./inboxRelaySelections.js"
export * from "./relayLists.js"
export * from "./messagingRelayLists.js"
export * from "./search.js"
export * from "./session.js"
export * from "./sync.js"
@@ -37,10 +37,10 @@ import {routerContext} from "@welshman/router"
import {Pool, SocketEvent, isRelayEvent, netContext} from "@welshman/net"
import {pubkey, unwrapAndStore} from "./session.js"
import {repository, tracker} from "./core.js"
import {relays, loadRelay} from "./relays.js"
import {getRelays, loadRelay} from "./relays.js"
import {trackRelayStats, getRelayQuality} from "./relayStats.js"
import {relaySelectionsByPubkey} from "./relaySelections.js"
import {inboxRelaySelectionsByPubkey} from "./inboxRelaySelections.js"
import {deriveRelayList, getRelayList} from "./relayLists.js"
import {deriveMessagingRelayList, getMessagingRelayList} from "./messagingRelayLists.js"
// Sync relays with our database
@@ -73,7 +73,7 @@ Pool.get().subscribe(socket => {
const _relayGetter = (fn?: (relay: RelayProfile) => any) =>
throttleWithValue(200, () => {
let _relays = relays.get()
let _relays = getRelays()
if (fn) {
_relays = _relays.filter(fn)
@@ -85,14 +85,14 @@ const _relayGetter = (fn?: (relay: RelayProfile) => any) =>
})
export const getPubkeyRelays = (pubkey: string, mode?: RelayMode) =>
mode === RelayMode.Inbox
? getRelaysFromList(inboxRelaySelectionsByPubkey.get().get(pubkey))
: getRelaysFromList(relaySelectionsByPubkey.get().get(pubkey), mode)
mode === RelayMode.Messages
? getRelaysFromList(getMessagingRelayList(pubkey))
: getRelaysFromList(getRelayList(pubkey), mode)
export const derivePubkeyRelays = (pubkey: string, mode?: RelayMode) =>
mode === RelayMode.Inbox
? derived(inboxRelaySelectionsByPubkey, $m => getRelaysFromList($m.get(pubkey)))
: derived(relaySelectionsByPubkey, $m => getRelaysFromList($m.get(pubkey), mode))
mode === RelayMode.Messages
? derived(deriveMessagingRelayList(pubkey), list => getRelaysFromList(list))
: derived(deriveRelayList(pubkey), list => getRelaysFromList(list, mode))
routerContext.getUserPubkey = () => pubkey.get()
routerContext.getPubkeyRelays = getPubkeyRelays
+24
View File
@@ -0,0 +1,24 @@
import {MESSAGING_RELAYS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js"
import {makeOutboxLoader} from "./relayLists.js"
export const messagingRelayListsByPubkey = deriveItemsByKey({
repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [MESSAGING_RELAYS]}],
getKey: messagingRelayLists => messagingRelayLists.event.pubkey,
})
export const messagingRelayLists = deriveItems(messagingRelayListsByPubkey)
export const getMessagingRelayListsByPubkey = getter(messagingRelayListsByPubkey)
export const getMessagingRelayList = (pubkey: string) => getMessagingRelayListsByPubkey().get(pubkey)
export const forceLoadMessagingRelayList = makeForceLoadItem(makeOutboxLoader(MESSAGING_RELAYS), getMessagingRelayList)
export const loadMessagingRelayList = makeLoadItem(makeOutboxLoader(MESSAGING_RELAYS), getMessagingRelayList)
export const deriveMessagingRelayList = makeDeriveItem(messagingRelayListsByPubkey, loadMessagingRelayList)
+10 -8
View File
@@ -3,9 +3,9 @@ import {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js"
import {ensurePlaintext} from "./plaintext.js"
import {makeOutboxLoader} from "./relaySelections.js"
import {makeOutboxLoader} from "./relayLists.js"
export const mutesByPubkey = deriveItemsByKey({
export const muteListsByPubkey = deriveItemsByKey<PublishedList>({
repository,
eventToItem: async (event: TrustedEvent) =>
readList(
@@ -17,14 +17,16 @@ export const mutesByPubkey = deriveItemsByKey({
getKey: mute => mute.event.pubkey,
})
export const mutes = deriveItems(mutesByPubkey)
export const muteLists = deriveItems(muteListsByPubkey)
export const getMutesByPubkey = getter(mutesByPubkey)
export const getMuteListsByPubkey = getter(muteListsByPubkey)
export const getMutes = (pubkey: string) => getMutesByPubkey().get(pubkey)
export const getMuteLists = getter(muteLists)
export const forceLoadMutes = makeForceLoadItem(makeOutboxLoader(MUTES), getMutes)
export const getMuteList = (pubkey: string) => getMuteListsByPubkey().get(pubkey)
export const loadMutes = makeLoadItem(makeOutboxLoader(MUTES), getMutes)
export const forceLoadMuteList = makeForceLoadItem(makeOutboxLoader(MUTES), getMuteList)
export const deriveMutes = makeDeriveItem(mutesByPubkey, loadMutes)
export const loadMuteList = makeLoadItem(makeOutboxLoader(MUTES), getMuteList)
export const deriveMuteList = makeDeriveItem(muteListsByPubkey, loadMuteList)
+10 -8
View File
@@ -2,23 +2,25 @@ import {PINS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js"
import {makeOutboxLoader} from "./relaySelections.js"
import {makeOutboxLoader} from "./relayLists.js"
export const pinsByPubkey = deriveItemsByKey({
export const pinListsByPubkey = deriveItemsByKey({
repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [PINS]}],
getKey: pins => pins.event.pubkey,
})
export const pins = deriveItems(pinsByPubkey)
export const pinLists = deriveItems(pinListsByPubkey)
export const getPinsByPubkey = getter(pinsByPubkey)
export const getPinListsByPubkey = getter(pinListsByPubkey)
export const getPins = (pubkey: string) => getPinsByPubkey().get(pubkey)
export const getPinLists = getter(pinLists)
export const forceLoadPins = makeForceLoadItem(makeOutboxLoader(PINS), getPins)
export const getPinList = (pubkey: string) => getPinListsByPubkey().get(pubkey)
export const loadPins = makeLoadItem(makeOutboxLoader(PINS), getPins)
export const forceLoadPinList = makeForceLoadItem(makeOutboxLoader(PINS), getPinList)
export const derivePins = makeDeriveItem(pinsByPubkey, loadPins)
export const loadPinList = makeLoadItem(makeOutboxLoader(PINS), getPinList)
export const derivePinList = makeDeriveItem(pinListsByPubkey, loadPinList)
+3 -1
View File
@@ -2,7 +2,7 @@ import {derived, readable} from "svelte/store"
import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js"
import {makeOutboxLoaderWithIndexers} from "./relaySelections.js"
import {makeOutboxLoaderWithIndexers} from "./relayLists.js"
export const profilesByPubkey = deriveItemsByKey({
repository,
@@ -15,6 +15,8 @@ export const profiles = deriveItems(profilesByPubkey)
export const getProfilesByPubkey = getter(profilesByPubkey)
export const getProfiles = getter(profiles)
export const getProfile = (pubkey: string) => getProfilesByPubkey().get(pubkey)
export const forceLoadProfile = makeForceLoadItem(makeOutboxLoaderWithIndexers(PROFILE), getProfile)
@@ -41,21 +41,23 @@ export const makeOutboxLoaderWithIndexers =
])
}
export const relaySelectionsByPubkey = deriveItemsByKey({
export const relayListsByPubkey = deriveItemsByKey({
repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [RELAYS]}],
getKey: relaySelections => relaySelections.event.pubkey,
getKey: relayList => relayList.event.pubkey,
})
export const relaySelections = deriveItems(relaySelectionsByPubkey)
export const relayLists = deriveItems(relayListsByPubkey)
export const getRelaySelectionsByPubkey = getter(relaySelectionsByPubkey)
export const getRelayListsByPubkey = getter(relayListsByPubkey)
export const getRelaySelections = (pubkey: string) => getRelaySelectionsByPubkey().get(pubkey)
export const getRelayLists = getter(relayLists)
export const forceLoadRelaySelections = makeForceLoadItem(makeOutboxLoaderWithIndexers(RELAYS), getRelaySelections)
export const getRelayList = (pubkey: string) => getRelayListsByPubkey().get(pubkey)
export const loadRelaySelections = makeLoadItem(makeOutboxLoaderWithIndexers(RELAYS), getRelaySelections)
export const forceLoadRelayList = makeForceLoadItem(makeOutboxLoaderWithIndexers(RELAYS), getRelayList)
export const deriveRelaySelections = makeDeriveItem(relaySelectionsByPubkey, loadRelaySelections)
export const loadRelayList = makeLoadItem(makeOutboxLoaderWithIndexers(RELAYS), getRelayList)
export const deriveRelayList = makeDeriveItem(relayListsByPubkey, loadRelayList)
+41 -67
View File
@@ -8,99 +8,73 @@ import {
fetchJson,
postJson,
Maybe,
noop,
} from "@welshman/lib"
import {withGetter} from "@welshman/store"
import {RelayProfile} from "@welshman/util"
import {normalizeRelayUrl, displayRelayUrl, displayRelayProfile, isRelayUrl} from "@welshman/util"
import {collection} from "@welshman/store"
import {getter, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem} from "@welshman/store"
import {appContext} from "./context.js"
export const relays = withGetter(writable<RelayProfile[]>([]))
export const relaysByUrl = writable(new Map<string, RelayProfile>())
export const fetchRelayProfileDirectly = async (url: string): Promise<Maybe<RelayProfile>> => {
export const relays = deriveItems(relaysByUrl)
export const getRelaysByUrl = getter(relaysByUrl)
export const getRelays = getter(relays)
export const getRelay = (url: string) => getRelaysByUrl().get(url)
export const fetchRelayDirectly = async (url: string): Promise<Maybe<RelayProfile>> => {
try {
return fetchJson(url.replace(/^ws/, "http"), {
const json = fetchJson(url.replace(/^ws/, "http"), {
headers: {
Accept: "application/nostr+json",
},
})
if (json) {
return {...json, url}
}
} catch (e) {
// pass
}
}
export const fetchRelayProfilesDirectly = async (
urls: string[],
): Promise<Map<string, RelayProfile>> =>
indexBy(
prop("url"),
removeUndefined(
await Promise.all(
urls.map(async url => {
const profile = await fetchRelayProfileDirectly(url)
if (profile) {
return {...profile, url}
}
}),
),
),
)
export const fetchRelayProfilesUsingProxy = async (
proxy: string,
urls: string[],
): Promise<Map<string, RelayProfile>> => {
const profilesByUrl = new Map<string, RelayProfile>()
const res: any = await postJson(`${proxy}/relay/info`, {urls})
for (const {url, info} of res?.data || []) {
profilesByUrl.set(url, info)
export const fetchRelayUsingProxy = batcher(800, async (urls: string[]) => {
// Handle a race condition edge case where dufflepud url changes under us
if (!appContext.dufflepudUrl) {
return urls.map(noop)
}
return profilesByUrl
}
const res: any = await postJson(`${appContext.dufflepudUrl}/relay/info`, {urls})
const relaysByUrl = new Map<string, RelayProfile>()
export const fetchRelayProfiles = (urls: string[]) =>
appContext.dufflepudUrl
? fetchRelayProfilesUsingProxy(appContext.dufflepudUrl, urls)
: fetchRelayProfilesDirectly(urls)
for (const {url, info} of res?.data || []) {
relaysByUrl.set(url, info)
}
export const {
indexStore: relaysByUrl,
deriveItem: deriveRelay,
loadItem: loadRelay,
onItem: onRelay,
} = collection({
name: "relays",
store: relays,
getKey: (relay: RelayProfile) => relay.url,
load: batcher(800, async (rawUrls: string[]) => {
const urls = rawUrls.map(normalizeRelayUrl)
const fresh = await fetchRelayProfiles(uniq(urls))
const stale = relaysByUrl.get()
return urls.map(url => {
const info = relaysByUrl.get(url)
for (const url of urls) {
const profile = fresh.get(url)
if (!url || !isRelayUrl(url)) {
console.warn(`Attempted to load invalid relay url: ${url}`)
continue
}
if (profile) {
stale.set(url, {...profile, url})
}
if (info) {
return {...info, url}
}
relays.set(Array.from(stale.values()))
return urls
}),
})
})
export const fetchRelay = (url: string) =>
appContext.dufflepudUrl ? fetchRelayUsingProxy(url) : fetchRelayDirectly(url)
export const forceLoadRelay = makeForceLoadItem(fetchRelay, getRelay)
export const loadRelay = makeLoadItem(fetchRelay, getRelay)
export const deriveRelay = makeDeriveItem(relaysByUrl, loadRelay)
export const displayRelayByPubkey = (url: string) =>
displayRelayProfile(relaysByUrl.get().get(url), displayRelayUrl(url))
displayRelayProfile(getRelay(url), displayRelayUrl(url))
export const deriveRelayDisplay = (url: string) =>
derived(deriveRelay(url), $relay => displayRelayProfile($relay, displayRelayUrl(url)))
+2 -2
View File
@@ -2,13 +2,13 @@ import type {Filter} from "@welshman/util"
import {isSignedEvent, SignedEvent} from "@welshman/util"
import {push as basePush, pull as basePull, publishOne, requestOne} from "@welshman/net"
import {repository} from "./core.js"
import {relaysByUrl} from "./relays.js"
import {getRelay} from "./relays.js"
const query = (filters: Filter[]) =>
repository.query(filters, {shouldSort: filters.every(f => f.limit === undefined)})
export const hasNegentropy = (url: string) => {
const relay = relaysByUrl.get().get(url)
const relay = getRelay(url)
if (relay?.negentropy) return true
if (relay?.supported_nips?.includes?.(77)) return true
+34 -16
View File
@@ -1,5 +1,7 @@
import {inc, throttle} from "@welshman/lib"
import {custom} from "@welshman/store"
import {readable} from 'svelte/store'
import {on, call} from "@welshman/lib"
import {deriveItems} from "@welshman/store"
import {getTopicTagValues} from "@welshman/util"
import {repository} from "./core.js"
export type Topic = {
@@ -7,25 +9,41 @@ export type Topic = {
count: number
}
export const topics = custom<Topic[]>(setter => {
const getTopics = () => {
const topics = new Map<string, number>()
for (const tagString of repository.eventsByTag.keys()) {
if (tagString.startsWith("t:")) {
const topic = tagString.slice(2).toLowerCase()
export const topicsByName = call(() => {
const topicsByName = new Map<string, Topic>()
topics.set(topic, inc(topics.get(topic)))
}
const addTopic = (name: string) => {
const topic = topicsByName.get(name)
if (topic) {
topic.count++
} else {
topicsByName.set(name, {name, count: 0})
}
return Array.from(topics.entries()).map(([name, count]) => ({name, count}))
}
setter(getTopics())
for (const tagString of repository.eventsByTag.keys()) {
if (tagString.startsWith("t:")) {
addTopic(tagString.slice(2).toLowerCase())
}
}
const onUpdate = throttle(3000, () => setter(getTopics()))
return readable<Map<string, Topic>>(topicsByName, set => {
return on(repository, 'update', ({added}) => {
let dirty = false
repository.on("update", onUpdate)
for (const event of added) {
for (const name of getTopicTagValues(event.tags)) {
addTopic(name)
dirty = true
}
}
return () => repository.off("update", onUpdate)
if (dirty) {
set(topicsByName)
}
})
})
})
export const topics = deriveItems(topicsByName)
+38 -31
View File
@@ -1,13 +1,13 @@
import {derived, Readable} from "svelte/store"
import {withGetter, memoized} from "@welshman/store"
import {pubkey} from "./session.js"
import {profilesByPubkey, loadProfile} from "./profiles.js"
import {followsByPubkey, loadFollows} from "./follows.js"
import {loadPins, pinsByPubkey} from "./pins.js"
import {mutesByPubkey, loadMutes} from "./mutes.js"
import {blossomServersByPubkey, loadBlossomServers} from "./blossom.js"
import {relaySelectionsByPubkey, loadRelaySelections} from "./relaySelections.js"
import {inboxRelaySelectionsByPubkey, loadInboxRelaySelections} from "./inboxRelaySelections.js"
import {profilesByPubkey, forceLoadProfile, loadProfile} from "./profiles.js"
import {followListsByPubkey, forceLoadFollowList, loadFollowList} from "./follows.js"
import {pinListsByPubkey, forceLoadPinList, loadPinList} from "./pins.js"
import {muteListsByPubkey, forceLoadMuteList, loadMuteList} from "./mutes.js"
import {blossomServerListsByPubkey, forceLoadBlossomServerList, loadBlossomServerList} from "./blossom.js"
import {relayListsByPubkey, forceLoadRelayList, loadRelayList} from "./relayLists.js"
import {messagingRelayListsByPubkey, forceLoadMessagingRelayList, loadMessagingRelayList} from "./messagingRelayLists.js"
import {wotGraph} from "./wot.js"
export type UserDataLoader = (pubkey: string, relays?: string[], force?: boolean) => unknown
@@ -45,49 +45,56 @@ export const userProfile = makeUserData({
loadItem: loadProfile,
})
export const forceLoadUserProfile = makeUserLoader(forceLoadProfile)
export const loadUserProfile = makeUserLoader(loadProfile)
export const userFollows = makeUserData({
mapStore: followsByPubkey,
loadItem: loadFollows,
export const userFollowList = makeUserData({
mapStore: followListsByPubkey,
loadItem: loadFollowList,
})
export const loadUserFollows = makeUserLoader(loadFollows)
export const forceLoadUserFollowList = makeUserLoader(forceLoadFollowList)
export const loadUserFollowList = makeUserLoader(loadFollowList)
export const userMutes = makeUserData({
mapStore: mutesByPubkey,
loadItem: loadMutes,
export const userMuteList = makeUserData({
mapStore: muteListsByPubkey,
loadItem: loadMuteList,
})
export const loadUserMutes = makeUserLoader(loadMutes)
export const forceLoadUserMuteList = makeUserLoader(forceLoadMuteList)
export const loadUserMuteList = makeUserLoader(loadMuteList)
export const userPins = makeUserData({
mapStore: pinsByPubkey,
loadItem: loadPins,
export const userPinList = makeUserData({
mapStore: pinListsByPubkey,
loadItem: loadPinList,
})
export const loadUserPins = makeUserLoader(loadPins)
export const forceLoadUserPinList = makeUserLoader(forceLoadPinList)
export const loadUserPinList = makeUserLoader(loadPinList)
export const userRelaySelections = makeUserData({
mapStore: relaySelectionsByPubkey,
loadItem: loadRelaySelections,
export const userRelayList = makeUserData({
mapStore: relayListsByPubkey,
loadItem: loadRelayList,
})
export const loadUserRelaySelections = makeUserLoader(loadRelaySelections)
export const forceLoadUserRelayList = makeUserLoader(forceLoadRelayList)
export const loadUserRelayList = makeUserLoader(loadRelayList)
export const userInboxRelaySelections = makeUserData({
mapStore: inboxRelaySelectionsByPubkey,
loadItem: loadInboxRelaySelections,
export const userMessagingRelayList = makeUserData({
mapStore: messagingRelayListsByPubkey,
loadItem: loadMessagingRelayList,
})
export const loadUserInboxRelaySelections = makeUserLoader(loadInboxRelaySelections)
export const forceLoadUserMessagingRelayList = makeUserLoader(forceLoadMessagingRelayList)
export const loadUserMessagingRelayList = makeUserLoader(loadMessagingRelayList)
export const userBlossomServers = makeUserData({
mapStore: blossomServersByPubkey,
loadItem: loadBlossomServers,
export const userBlossomServerList = makeUserData({
mapStore: blossomServerListsByPubkey,
loadItem: loadBlossomServerList,
})
export const loadUserBlossomServers = makeUserLoader(loadBlossomServers)
export const forceLoadUserBlossomServerList = makeUserLoader(forceLoadBlossomServerList)
export const loadUserBlossomServerList = makeUserLoader(loadBlossomServerList)
export const getUserWotScore = (tpk: string) => wotGraph.get().get(tpk) || 0
+9 -9
View File
@@ -3,14 +3,14 @@ import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util"
import {throttled, withGetter} from "@welshman/store"
import {pubkey} from "./session.js"
import {follows, followsByPubkey} from "./follows.js"
import {mutes, mutesByPubkey} from "./mutes.js"
import {followLists, getFollowListsByPubkey, getFollowList} from "./follows.js"
import {muteLists, getMuteList} from "./mutes.js"
export const getFollows = (pubkey: string) =>
getPubkeyTagValues(getListTags(followsByPubkey.get().get(pubkey)))
getPubkeyTagValues(getListTags(getFollowList(pubkey)))
export const getMutes = (pubkey: string) =>
getPubkeyTagValues(getListTags(mutesByPubkey.get().get(pubkey)))
getPubkeyTagValues(getListTags(getMuteList(pubkey)))
export const getNetwork = (pubkey: string) => {
const pubkeys = new Set(getFollows(pubkey))
@@ -28,7 +28,7 @@ export const getNetwork = (pubkey: string) => {
}
export const followersByPubkey = withGetter(
derived(throttled(1000, follows), lists => {
derived(throttled(1000, followLists), lists => {
const $followersByPubkey = new Map<string, Set<string>>()
for (const list of lists) {
@@ -42,7 +42,7 @@ export const followersByPubkey = withGetter(
)
export const mutersByPubkey = withGetter(
derived(throttled(1000, mutes), lists => {
derived(throttled(1000, muteLists), lists => {
const $mutersByPubkey = new Map<string, Set<string>>()
for (const list of lists) {
@@ -73,7 +73,7 @@ export const maxWot = withGetter(derived(wotGraph, $g => max(Array.from($g.value
const buildGraph = throttle(1000, () => {
const $pubkey = pubkey.get()
const $graph = new Map<string, number>()
const $follows = $pubkey ? getFollows($pubkey) : followsByPubkey.get().keys()
const $follows = $pubkey ? getFollows($pubkey) : getFollowListsByPubkey().keys()
for (const follow of $follows) {
for (const pubkey of getFollows(follow)) {
@@ -89,8 +89,8 @@ const buildGraph = throttle(1000, () => {
})
pubkey.subscribe(buildGraph)
follows.subscribe(buildGraph)
mutes.subscribe(buildGraph)
followLists.subscribe(buildGraph)
muteLists.subscribe(buildGraph)
export const getWotScore = (pubkey: string, target: string) => {
const follows = pubkey ? getFollowsWhoFollow(pubkey, target) : getFollowers(target)
+29 -45
View File
@@ -10,15 +10,21 @@ import {
batcher,
postJson,
} from "@welshman/lib"
import {collection} from "@welshman/store"
import {getter, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem} from "@welshman/store"
import {deriveProfile, loadProfile} from "./profiles.js"
import {appContext} from "./context.js"
export const zappers = writable<Zapper[]>([])
export const zappersByLnurl = writable(new Map<string, Zapper>())
export const fetchZappers = async (lnurls: string[]) => {
export const zappers = deriveItems(zappersByLnurl)
export const getZappersByLnurl = getter(zappersByLnurl)
export const getZapper = (lnurl: string) => getZappersByLnurl().get(lnurl)
export const fetchZapper = batcher(800, async (lnurls: string[]) => {
const base = appContext.dufflepudUrl
const zappersByLnurl = new Map<string, Zapper>()
const result = new Map<string, Zapper>()
// Use dufflepud if we it's set up to protect user privacy, otherwise fetch directly
if (base) {
@@ -30,7 +36,7 @@ export const fetchZappers = async (lnurls: string[]) => {
)
for (const {lnurl, info} of res?.data || []) {
tryCatch(() => zappersByLnurl.set(hexToBech32("lnurl", lnurl), info))
tryCatch(() => result.set(hexToBech32("lnurl", lnurl), info))
}
}
} else {
@@ -45,61 +51,39 @@ export const fetchZappers = async (lnurls: string[]) => {
for (const {lnurl, info} of results) {
if (info) {
zappersByLnurl.set(lnurl, info)
result.set(lnurl, info)
}
}
}
return zappersByLnurl
}
return lnurls.map(lnurl => {
const info = result.get(lnurl)
export const {
indexStore: zappersByLnurl,
deriveItem: deriveZapper,
loadItem: loadZapper,
onItem: onZapper,
} = collection({
name: "zappers",
store: zappers,
getKey: (zapper: Zapper) => zapper.lnurl,
load: batcher(800, async (lnurls: string[]) => {
const fresh = await fetchZappers(uniq(lnurls))
const stale = zappersByLnurl.get()
for (const lnurl of lnurls) {
const newZapper = fresh.get(lnurl)
if (newZapper) {
stale.set(lnurl, {...newZapper, lnurl})
}
if (info) {
return {...info, lnurl}
}
zappers.set(Array.from(stale.values()))
return lnurls
}),
})
})
export const forceLoadZapper = makeForceLoadItem(fetchZapper, getZapper)
export const loadZapper = makeLoadItem(fetchZapper, getZapper)
export const deriveZapper = makeDeriveItem(zappersByLnurl, loadZapper)
export const loadZapperForPubkey = async (pubkey: string, relays: string[] = []) => {
const $profile = await loadProfile(pubkey, relays)
if (!$profile?.lnurl) {
return undefined
}
return loadZapper($profile.lnurl)
return $profile?.lnurl ? loadZapper($profile.lnurl) : undefined
}
export const deriveZapperForPubkey = (pubkey: string, relays: string[] = []) =>
derived([zappersByLnurl, deriveProfile(pubkey, relays)], ([$zappersByLnurl, $profile]) => {
if (!$profile?.lnurl) {
return undefined
}
export const deriveZapperForPubkey = (pubkey: string, relays: string[] = []) => {
loadZapperForPubkey(pubkey, relays)
loadZapper($profile.lnurl)
return $zappersByLnurl.get($profile.lnurl)
return derived([zappersByLnurl, deriveProfile(pubkey, relays)], ([$zappersByLnurl, $profile]) => {
return $profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined
})
}
export const getLnUrlsForEvent = async (event: TrustedEvent) => {
const lnurls = removeUndefined(getTagValues("zap", event.tags).map(getLnUrl))
+8 -8
View File
@@ -21,7 +21,7 @@ import {
isShareableRelayUrl,
PROFILE,
RELAYS,
INBOX_RELAYS,
MESSAGING_RELAYS,
FOLLOWS,
WRAP,
getPubkeyTagValues,
@@ -37,7 +37,7 @@ import {
} from "@welshman/util"
import {Repository} from "@welshman/net"
export const INDEXED_KINDS = [PROFILE, RELAYS, INBOX_RELAYS, FOLLOWS]
export const INDEXED_KINDS = [PROFILE, RELAYS, MESSAGING_RELAYS, FOLLOWS]
export type RelaysAndFilters = {
relays: string[]
@@ -54,7 +54,7 @@ export type RouterOptions = {
/**
* Retrieves relays for the specified public key and mode.
* @param pubkey - The public key to retrieve relays for.
* @param mode - The relay mode (optional). May be "read", "write", or "inbox".
* @param mode - The relay mode (optional). May be "read", "write", or "messaging".
* @returns An array of relay URLs as strings.
*/
getPubkeyRelays?: (pubkey: string, mode?: RelayMode) => string[]
@@ -174,20 +174,20 @@ export class Router {
FromUser = () => this.FromRelays(this.getRelaysForUser(RelayMode.Write))
UserInbox = () => this.FromRelays(this.getRelaysForUser(RelayMode.Inbox))
UserMessages = () => this.FromRelays(this.getRelaysForUser(RelayMode.Messages))
ForPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Read))
FromPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Write))
PubkeyInbox = (pubkey: string) =>
this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Inbox))
MessagesForPubkey = (pubkey: string) =>
this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Messages))
ForPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.ForPubkey(pubkey)))
FromPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.FromPubkey(pubkey)))
PubkeyInboxes = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.PubkeyInbox(pubkey)))
MessagesForPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.MessagesForPubkey(pubkey)))
Event = (event: TrustedEvent) =>
this.FromRelays(this.getRelaysForPubkey(event.pubkey, RelayMode.Write))
@@ -371,7 +371,7 @@ export const getFilterSelectionsForWraps = (filter: Filter) => {
return [
{
filter: {...filter, kinds: [WRAP]},
scenario: Router.get().UserInbox(),
scenario: Router.get().UserMessages(),
},
]
}
+2 -2
View File
@@ -1,5 +1,5 @@
import {derived, readable, Readable} from "svelte/store"
import {on, now, indexBy, mapPop, Maybe, call, sortBy, first} from "@welshman/lib"
import {on, now, indexBy, mapPop, Maybe, MaybeAsync, call, sortBy, first} from "@welshman/lib"
import {matchFilters, getIdFilters, Filter, TrustedEvent} from "@welshman/util"
import {Repository, RepositoryUpdate, Tracker} from "@welshman/net"
import {deriveDeduplicated} from "./misc.js"
@@ -175,7 +175,7 @@ export const deriveEventsByIdForUrl = (
export type ItemsByKey<T> = Map<string, T>
export type EventToItem<T> = (event: TrustedEvent) => T
export type EventToItem<T> = (event: TrustedEvent) => MaybeAsync<Maybe<T>>
export type GetItem<T> = (key: string, ...args: any[]) => Maybe<T>
+1 -1
View File
@@ -135,7 +135,7 @@ export const ROOMS = 10009
export const FEEDS = 10014
export const TOPICS = 10015
export const EMOJIS = 10030
export const INBOX_RELAYS = 10050
export const MESSAGING_RELAYS = 10050
export const BLOSSOM_SERVERS = 10063
export const FILE_SERVERS = 10096
export const RELAY_MEMBERS = 13534
+1 -1
View File
@@ -5,7 +5,7 @@ import {last, normalizeUrl, stripProtocol} from "@welshman/lib"
export enum RelayMode {
Read = "read",
Write = "write",
Inbox = "inbox",
Messages = "messages",
}
export type RelayProfile = {