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> addRelay(url: string, mode: RelayMode): Promise<Thunk>
``` ```
## Inbox Relay Management (NIP 17) ## Messaging Relay Management (NIP 17)
```typescript ```typescript
removeInboxRelay(url: string): Promise<Thunk> removeMessagingRelay(url: string): Promise<Thunk>
addInboxRelay(url: string): Promise<Thunk> addMessagingRelay(url: string): Promise<Thunk>
``` ```
## Profile Management (NIP 01) ## 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 // 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, // 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()) const profile = deriveProfile(pubkey.get())
// Publish is done using thunks, which optimistically publish to the local database, deferring // 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
relays relaysByUrl deriveRelay loadRelay relays relaysByUrl deriveRelay loadRelay
relaySelections relaySelectionsByPubkey deriveRelaySelections loadRelaySelections relayLists relayListsByPubkey deriveRelayLists loadRelayLists
inboxRelaySelections inboxRelaySelectionsByPubkey deriveInboxRelaySelections loadInboxRelaySelections messagingRelayLists messagingRelayListsByPubkey deriveMessagingRelayLists loadMessagingRelayLists
// Identity // Identity
handles handlesByNip05 deriveHandle loadHandle 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)` - `removeRelay(url: string, mode: RelayMode)`
- `addRelay(url: string, mode: RelayMode)` - `addRelay(url: string, mode: RelayMode)`
- `removeInboxRelay(url: string)` - `removeMessagingRelay(url: string)`
- `addInboxRelay(url: string)` - `addMessagingRelay(url: string)`
- `setProfile(profile: Profile)` - `setProfile(profile: Profile)`
- `unfollow(value: string)` - `unfollow(value: string)`
- `follow(tag: string[])` - `follow(tag: string[])`
+8 -8
View File
@@ -39,10 +39,10 @@ export const userMutes: Store<List | undefined>
export const userPins: Store<List | undefined> export const userPins: Store<List | undefined>
// User relay selections // User relay selections
export const userRelaySelections: Store<List | undefined> export const userRelayLists: Store<List | undefined>
// User inbox relay selections // User messaging relay selections
export const userInboxRelaySelections: Store<List | undefined> export const userMessagingRelayLists: Store<List | undefined>
// User blossom servers // User blossom servers
export const userBlossomServers: Store<List | undefined> 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> function loadUserPins(relays?: string[], force?: boolean): Promise<void>
// Load user relay selections // 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 // Load user messaging relay selections
function loadUserInboxRelaySelections(relays?: string[], force?: boolean): Promise<void> function loadUserMessagingRelayLists(relays?: string[], force?: boolean): Promise<void>
// Load user blossom servers // Load user blossom servers
function loadUserBlossomServers(relays?: string[], force?: boolean): Promise<void> function loadUserBlossomServers(relays?: string[], force?: boolean): Promise<void>
@@ -94,13 +94,13 @@ const follows = userFollows.get()
### Manual Loading ### Manual Loading
```typescript ```typescript
import { loadUserMutes, loadUserRelaySelections } from '@welshman/app' import { loadUserMutes, loadUserRelayLists } from '@welshman/app'
// Load user mutes from specific relays // Load user mutes from specific relays
await loadUserMutes(['wss://relay1.com', 'wss://relay2.com']) await loadUserMutes(['wss://relay1.com', 'wss://relay2.com'])
// Force refresh user relay selections // Force refresh user relay selections
await loadUserRelaySelections([], true) await loadUserRelayLists([], true)
// Load from default relays // Load from default relays
await loadUserProfile() await loadUserProfile()
+5 -5
View File
@@ -31,7 +31,7 @@ const router = Router.get()
// Get relays for reading events from specific pubkeys // Get relays for reading events from specific pubkeys
const readRelays = router.FromPubkeys(['pubkey1', 'pubkey2']).getUrls() 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() const publishRelays = router.PublishEvent(event).getUrls()
// Try hard to find a quoted note with maximal fallbacks // 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:** **Configuration Options:**
- `getUserPubkey()` - Returns the current user's pubkey - `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 - `getDefaultRelays()` - Returns fallback relays
- `getIndexerRelays()` - Returns relays that index profiles and relay lists - `getIndexerRelays()` - Returns relays that index profiles and relay lists
- `getSearchRelays()` - Returns relays that support NIP-50 search - `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:** **Scenario Methods:**
- `FromRelays(relays)` - Use specific relays - `FromRelays(relays)` - Use specific relays
- `ForUser()` / `FromUser()` / `UserInbox()` - User's read/write/inbox relays - `ForUser()` / `FromUser()` / `UserMessaging()` - User's read/write/messaging relays
- `ForPubkey(pubkey)` / `FromPubkey(pubkey)` / `PubkeyInbox(pubkey)` - Pubkey's relays - `ForPubkey(pubkey)` / `FromPubkey(pubkey)` / `PubkeyMessaging(pubkey)` - Pubkey's relays
- `ForPubkeys(pubkeys)` / `FromPubkeys(pubkeys)` - Multiple pubkeys' relays - `ForPubkeys(pubkeys)` / `FromPubkeys(pubkeys)` - Multiple pubkeys' relays
- `Event(event)` - Relays for an event's author - `Event(event)` - Relays for an event's author
- `PublishEvent(event)` - Relays for publishing (author + mentions) - `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: `getFilterSelections(filters)` automatically chooses appropriate relays based on filter content:
- Search filters → search relays - Search filters → search relays
- Wrap events → user's inbox - Wrap events → user's messaging
- Profile/relay kinds → indexer relays - Profile/relay kinds → indexer relays
- Author filters → authors' relays - Author filters → authors' relays
- Everything else → user's relays (low weight) - 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 { export enum RelayMode {
Read = "read", Read = "read",
Write = "write", Write = "write",
Inbox = "inbox" Messaging = "messaging"
} }
// Relay information from NIP-11 // 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 {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store" import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {makeOutboxLoader} from "./relaySelections.js" import {makeOutboxLoader} from "./relayLists.js"
export const blossomServersByPubkey = deriveItemsByKey({ export const blossomServerListsByPubkey = deriveItemsByKey({
repository, repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)), eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [BLOSSOM_SERVERS]}], 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, createProfile,
editProfile, editProfile,
RelayMode, RelayMode,
INBOX_RELAYS, MESSAGING_RELAYS,
FOLLOWS, FOLLOWS,
RELAYS, RELAYS,
MUTES, MUTES,
@@ -32,16 +32,16 @@ import {
import type {RoomMeta, Profile} from "@welshman/util" import type {RoomMeta, Profile} from "@welshman/util"
import {Router, addMaximalFallbacks} from "@welshman/router" import {Router, addMaximalFallbacks} from "@welshman/router"
import { import {
userRelaySelections, userRelayList,
loadUserRelaySelections, forceLoadUserRelayList,
userInboxRelaySelections, userMessagingRelayList,
loadUserInboxRelaySelections, forceLoadUserMessagingRelayList,
userFollows, userFollowList,
loadUserFollows, forceLoadUserFollowList,
userMutes, userMuteList,
loadUserMutes, forceLoadUserMuteList,
userPins, userPinList,
loadUserPins, forceLoadUserPinList,
} from "./user.js" } from "./user.js"
import {nip44EncryptToSelf, signer} from "./session.js" import {nip44EncryptToSelf, signer} from "./session.js"
import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js" import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
@@ -49,9 +49,9 @@ import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
// NIP 65 // NIP 65
export const removeRelay = async (url: string, mode: RelayMode) => { 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 dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
const alt = mode === RelayMode.Read ? RelayMode.Write : RelayMode.Read const alt = mode === RelayMode.Read ? RelayMode.Write : RelayMode.Read
const tags = list.publicTags.filter(nthNe(1, url)) 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) => { 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 dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
const tag = removeUndefined(["r", url, dup && dup[2] !== mode ? undefined : mode]) const tag = removeUndefined(["r", url, dup && dup[2] !== mode ? undefined : mode])
const tags = [...list.publicTags.filter(nthNe(1, url)), tag] const tags = [...list.publicTags.filter(nthNe(1, url)), tag]
@@ -85,20 +85,20 @@ export const addRelay = async (url: string, mode: RelayMode) => {
// NIP 17 // NIP 17
export const removeInboxRelay = async (url: string) => { export const removeMessagingRelay = async (url: string) => {
await loadUserInboxRelaySelections([], true) 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 event = await removeFromList(list, url).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
return publishThunk({event, relays}) return publishThunk({event, relays})
} }
export const addInboxRelay = async (url: string) => { export const addMessagingRelay = async (url: string) => {
await loadUserInboxRelaySelections([], true) 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 event = await addToListPublicly(list, ["relay", url]).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -118,9 +118,9 @@ export const setProfile = (profile: Profile) => {
// NIP 02 // NIP 02
export const unfollow = async (value: string) => { 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 event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -128,9 +128,9 @@ export const unfollow = async (value: string) => {
} }
export const follow = async (tag: 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 event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -138,9 +138,9 @@ export const follow = async (tag: string[]) => {
} }
export const unmute = async (value: 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 event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -148,9 +148,9 @@ export const unmute = async (value: string) => {
} }
export const mutePublicly = async (tag: 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 event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -158,9 +158,9 @@ export const mutePublicly = async (tag: string[]) => {
} }
export const mutePrivately = 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 event = await addToListPrivately(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -174,9 +174,9 @@ export const setMutes = async ({
publicTags?: string[][] publicTags?: string[][]
privateTags?: 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 event = await updateList(list, {publicTags, privateTags}).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -184,9 +184,9 @@ export const setMutes = async ({
} }
export const unpin = async (value: string) => { 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 event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -194,9 +194,9 @@ export const unpin = async (value: string) => {
} }
export const pin = async (tag: 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 event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() 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) => export const sendWrapped = ({event, recipients, ...options}: SendWrappedOptions) =>
new MergedThunk( new MergedThunk(
uniq(recipients).map(recipient => { uniq(recipients).map(recipient => {
const relays = Router.get().PubkeyInbox(recipient).getUrls() const relays = Router.get().MessagesForPubkey(recipient).getUrls()
return publishThunk({event, relays, recipient, ...options}) 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 {Repository, Tracker} from "@welshman/net"
import {custom} from "@welshman/store"
export const tracker = new Tracker() export const tracker = new Tracker()
export const repository = Repository.get() 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 {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store" import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {makeOutboxLoader} from "./relaySelections.js" import {makeOutboxLoader} from "./relayLists.js"
export const followsByPubkey = deriveItemsByKey({ export const followListsByPubkey = deriveItemsByKey({
repository, repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [FOLLOWS]}], 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 {writable, derived} from "svelte/store"
import {tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib" import {tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib"
import {collection} from "@welshman/store" import {getter, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem} from "@welshman/store"
import {deriveProfile} from "./profiles.js" import {deriveProfile, loadProfile} from "./profiles.js"
import {appContext} from "./context.js" import {appContext} from "./context.js"
export type Handle = { 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[]) => { export const handles = deriveItems(handlesByNip05)
const base = appContext.dufflepudUrl!
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>() const handlesByNip05 = new Map<string, Handle>()
// Use dufflepud if we it's set up to protect user privacy, otherwise fetch directly // 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( 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 || []) { 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 { if (info) {
indexStore: handlesByNip05, return {...info, nip05}
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})
}
} }
})
handles.set(Array.from(stale.values()))
return nip05s
}),
}) })
export const deriveHandleForPubkey = (pubkey: string, relays: string[] = []) => export const forceLoadHandle = makeForceLoadItem(fetchHandle, getHandle)
derived([handlesByNip05, deriveProfile(pubkey, relays)], ([$handlesByNip05, $profile]) => {
if (!$profile?.nip05) {
return undefined
}
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) const handle = $handlesByNip05.get($profile.nip05)
if (handle?.pubkey === pubkey) { if (handle?.pubkey !== pubkey) return undefined
return handle
} return handle
}) })
}
export const displayNip05 = (nip05: string) => export const displayNip05 = (nip05: string) =>
nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05 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 "./pins.js"
export * from "./relays.js" export * from "./relays.js"
export * from "./relayStats.js" export * from "./relayStats.js"
export * from "./relaySelections.js" export * from "./relayLists.js"
export * from "./inboxRelaySelections.js" export * from "./messagingRelayLists.js"
export * from "./search.js" export * from "./search.js"
export * from "./session.js" export * from "./session.js"
export * from "./sync.js" export * from "./sync.js"
@@ -37,10 +37,10 @@ import {routerContext} from "@welshman/router"
import {Pool, SocketEvent, isRelayEvent, netContext} from "@welshman/net" import {Pool, SocketEvent, isRelayEvent, netContext} from "@welshman/net"
import {pubkey, unwrapAndStore} from "./session.js" import {pubkey, unwrapAndStore} from "./session.js"
import {repository, tracker} from "./core.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 {trackRelayStats, getRelayQuality} from "./relayStats.js"
import {relaySelectionsByPubkey} from "./relaySelections.js" import {deriveRelayList, getRelayList} from "./relayLists.js"
import {inboxRelaySelectionsByPubkey} from "./inboxRelaySelections.js" import {deriveMessagingRelayList, getMessagingRelayList} from "./messagingRelayLists.js"
// Sync relays with our database // Sync relays with our database
@@ -73,7 +73,7 @@ Pool.get().subscribe(socket => {
const _relayGetter = (fn?: (relay: RelayProfile) => any) => const _relayGetter = (fn?: (relay: RelayProfile) => any) =>
throttleWithValue(200, () => { throttleWithValue(200, () => {
let _relays = relays.get() let _relays = getRelays()
if (fn) { if (fn) {
_relays = _relays.filter(fn) _relays = _relays.filter(fn)
@@ -85,14 +85,14 @@ const _relayGetter = (fn?: (relay: RelayProfile) => any) =>
}) })
export const getPubkeyRelays = (pubkey: string, mode?: RelayMode) => export const getPubkeyRelays = (pubkey: string, mode?: RelayMode) =>
mode === RelayMode.Inbox mode === RelayMode.Messages
? getRelaysFromList(inboxRelaySelectionsByPubkey.get().get(pubkey)) ? getRelaysFromList(getMessagingRelayList(pubkey))
: getRelaysFromList(relaySelectionsByPubkey.get().get(pubkey), mode) : getRelaysFromList(getRelayList(pubkey), mode)
export const derivePubkeyRelays = (pubkey: string, mode?: RelayMode) => export const derivePubkeyRelays = (pubkey: string, mode?: RelayMode) =>
mode === RelayMode.Inbox mode === RelayMode.Messages
? derived(inboxRelaySelectionsByPubkey, $m => getRelaysFromList($m.get(pubkey))) ? derived(deriveMessagingRelayList(pubkey), list => getRelaysFromList(list))
: derived(relaySelectionsByPubkey, $m => getRelaysFromList($m.get(pubkey), mode)) : derived(deriveRelayList(pubkey), list => getRelaysFromList(list, mode))
routerContext.getUserPubkey = () => pubkey.get() routerContext.getUserPubkey = () => pubkey.get()
routerContext.getPubkeyRelays = getPubkeyRelays 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 {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {ensurePlaintext} from "./plaintext.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, repository,
eventToItem: async (event: TrustedEvent) => eventToItem: async (event: TrustedEvent) =>
readList( readList(
@@ -17,14 +17,16 @@ export const mutesByPubkey = deriveItemsByKey({
getKey: mute => mute.event.pubkey, 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 {TrustedEvent, PublishedList} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store" import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {makeOutboxLoader} from "./relaySelections.js" import {makeOutboxLoader} from "./relayLists.js"
export const pinsByPubkey = deriveItemsByKey({ export const pinListsByPubkey = deriveItemsByKey({
repository, repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)), eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [PINS]}], filters: [{kinds: [PINS]}],
getKey: pins => pins.event.pubkey, 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 {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store" import {deriveItemsByKey, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem, getter} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {makeOutboxLoaderWithIndexers} from "./relaySelections.js" import {makeOutboxLoaderWithIndexers} from "./relayLists.js"
export const profilesByPubkey = deriveItemsByKey({ export const profilesByPubkey = deriveItemsByKey({
repository, repository,
@@ -15,6 +15,8 @@ export const profiles = deriveItems(profilesByPubkey)
export const getProfilesByPubkey = getter(profilesByPubkey) export const getProfilesByPubkey = getter(profilesByPubkey)
export const getProfiles = getter(profiles)
export const getProfile = (pubkey: string) => getProfilesByPubkey().get(pubkey) export const getProfile = (pubkey: string) => getProfilesByPubkey().get(pubkey)
export const forceLoadProfile = makeForceLoadItem(makeOutboxLoaderWithIndexers(PROFILE), getProfile) export const forceLoadProfile = makeForceLoadItem(makeOutboxLoaderWithIndexers(PROFILE), getProfile)
@@ -41,21 +41,23 @@ export const makeOutboxLoaderWithIndexers =
]) ])
} }
export const relaySelectionsByPubkey = deriveItemsByKey({ export const relayListsByPubkey = deriveItemsByKey({
repository, repository,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)), eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
filters: [{kinds: [RELAYS]}], 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, fetchJson,
postJson, postJson,
Maybe, Maybe,
noop,
} from "@welshman/lib" } from "@welshman/lib"
import {withGetter} from "@welshman/store" import {withGetter} from "@welshman/store"
import {RelayProfile} from "@welshman/util" import {RelayProfile} from "@welshman/util"
import {normalizeRelayUrl, displayRelayUrl, displayRelayProfile, isRelayUrl} 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" 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 { try {
return fetchJson(url.replace(/^ws/, "http"), { const json = fetchJson(url.replace(/^ws/, "http"), {
headers: { headers: {
Accept: "application/nostr+json", Accept: "application/nostr+json",
}, },
}) })
if (json) {
return {...json, url}
}
} catch (e) { } catch (e) {
// pass // pass
} }
} }
export const fetchRelayProfilesDirectly = async ( export const fetchRelayUsingProxy = batcher(800, async (urls: string[]) => {
urls: string[], // Handle a race condition edge case where dufflepud url changes under us
): Promise<Map<string, RelayProfile>> => if (!appContext.dufflepudUrl) {
indexBy( return urls.map(noop)
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)
} }
return profilesByUrl const res: any = await postJson(`${appContext.dufflepudUrl}/relay/info`, {urls})
} const relaysByUrl = new Map<string, RelayProfile>()
export const fetchRelayProfiles = (urls: string[]) => for (const {url, info} of res?.data || []) {
appContext.dufflepudUrl relaysByUrl.set(url, info)
? fetchRelayProfilesUsingProxy(appContext.dufflepudUrl, urls) }
: fetchRelayProfilesDirectly(urls)
export const { return urls.map(url => {
indexStore: relaysByUrl, const info = relaysByUrl.get(url)
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()
for (const url of urls) { if (info) {
const profile = fresh.get(url) return {...info, url}
if (!url || !isRelayUrl(url)) {
console.warn(`Attempted to load invalid relay url: ${url}`)
continue
}
if (profile) {
stale.set(url, {...profile, 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) => export const displayRelayByPubkey = (url: string) =>
displayRelayProfile(relaysByUrl.get().get(url), displayRelayUrl(url)) displayRelayProfile(getRelay(url), displayRelayUrl(url))
export const deriveRelayDisplay = (url: string) => export const deriveRelayDisplay = (url: string) =>
derived(deriveRelay(url), $relay => displayRelayProfile($relay, displayRelayUrl(url))) 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 {isSignedEvent, SignedEvent} from "@welshman/util"
import {push as basePush, pull as basePull, publishOne, requestOne} from "@welshman/net" import {push as basePush, pull as basePull, publishOne, requestOne} from "@welshman/net"
import {repository} from "./core.js" import {repository} from "./core.js"
import {relaysByUrl} from "./relays.js" import {getRelay} from "./relays.js"
const query = (filters: Filter[]) => const query = (filters: Filter[]) =>
repository.query(filters, {shouldSort: filters.every(f => f.limit === undefined)}) repository.query(filters, {shouldSort: filters.every(f => f.limit === undefined)})
export const hasNegentropy = (url: string) => { export const hasNegentropy = (url: string) => {
const relay = relaysByUrl.get().get(url) const relay = getRelay(url)
if (relay?.negentropy) return true if (relay?.negentropy) return true
if (relay?.supported_nips?.includes?.(77)) 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 {readable} from 'svelte/store'
import {custom} from "@welshman/store" import {on, call} from "@welshman/lib"
import {deriveItems} from "@welshman/store"
import {getTopicTagValues} from "@welshman/util"
import {repository} from "./core.js" import {repository} from "./core.js"
export type Topic = { export type Topic = {
@@ -7,25 +9,41 @@ export type Topic = {
count: number count: number
} }
export const topics = custom<Topic[]>(setter => { export const topicsByName = call(() => {
const getTopics = () => { const topicsByName = new Map<string, Topic>()
const topics = new Map<string, number>()
for (const tagString of repository.eventsByTag.keys()) {
if (tagString.startsWith("t:")) {
const topic = tagString.slice(2).toLowerCase()
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 {derived, Readable} from "svelte/store"
import {withGetter, memoized} from "@welshman/store" import {withGetter, memoized} from "@welshman/store"
import {pubkey} from "./session.js" import {pubkey} from "./session.js"
import {profilesByPubkey, loadProfile} from "./profiles.js" import {profilesByPubkey, forceLoadProfile, loadProfile} from "./profiles.js"
import {followsByPubkey, loadFollows} from "./follows.js" import {followListsByPubkey, forceLoadFollowList, loadFollowList} from "./follows.js"
import {loadPins, pinsByPubkey} from "./pins.js" import {pinListsByPubkey, forceLoadPinList, loadPinList} from "./pins.js"
import {mutesByPubkey, loadMutes} from "./mutes.js" import {muteListsByPubkey, forceLoadMuteList, loadMuteList} from "./mutes.js"
import {blossomServersByPubkey, loadBlossomServers} from "./blossom.js" import {blossomServerListsByPubkey, forceLoadBlossomServerList, loadBlossomServerList} from "./blossom.js"
import {relaySelectionsByPubkey, loadRelaySelections} from "./relaySelections.js" import {relayListsByPubkey, forceLoadRelayList, loadRelayList} from "./relayLists.js"
import {inboxRelaySelectionsByPubkey, loadInboxRelaySelections} from "./inboxRelaySelections.js" import {messagingRelayListsByPubkey, forceLoadMessagingRelayList, loadMessagingRelayList} from "./messagingRelayLists.js"
import {wotGraph} from "./wot.js" import {wotGraph} from "./wot.js"
export type UserDataLoader = (pubkey: string, relays?: string[], force?: boolean) => unknown export type UserDataLoader = (pubkey: string, relays?: string[], force?: boolean) => unknown
@@ -45,49 +45,56 @@ export const userProfile = makeUserData({
loadItem: loadProfile, loadItem: loadProfile,
}) })
export const forceLoadUserProfile = makeUserLoader(forceLoadProfile)
export const loadUserProfile = makeUserLoader(loadProfile) export const loadUserProfile = makeUserLoader(loadProfile)
export const userFollows = makeUserData({ export const userFollowList = makeUserData({
mapStore: followsByPubkey, mapStore: followListsByPubkey,
loadItem: loadFollows, loadItem: loadFollowList,
}) })
export const loadUserFollows = makeUserLoader(loadFollows) export const forceLoadUserFollowList = makeUserLoader(forceLoadFollowList)
export const loadUserFollowList = makeUserLoader(loadFollowList)
export const userMutes = makeUserData({ export const userMuteList = makeUserData({
mapStore: mutesByPubkey, mapStore: muteListsByPubkey,
loadItem: loadMutes, loadItem: loadMuteList,
}) })
export const loadUserMutes = makeUserLoader(loadMutes) export const forceLoadUserMuteList = makeUserLoader(forceLoadMuteList)
export const loadUserMuteList = makeUserLoader(loadMuteList)
export const userPins = makeUserData({ export const userPinList = makeUserData({
mapStore: pinsByPubkey, mapStore: pinListsByPubkey,
loadItem: loadPins, loadItem: loadPinList,
}) })
export const loadUserPins = makeUserLoader(loadPins) export const forceLoadUserPinList = makeUserLoader(forceLoadPinList)
export const loadUserPinList = makeUserLoader(loadPinList)
export const userRelaySelections = makeUserData({ export const userRelayList = makeUserData({
mapStore: relaySelectionsByPubkey, mapStore: relayListsByPubkey,
loadItem: loadRelaySelections, loadItem: loadRelayList,
}) })
export const loadUserRelaySelections = makeUserLoader(loadRelaySelections) export const forceLoadUserRelayList = makeUserLoader(forceLoadRelayList)
export const loadUserRelayList = makeUserLoader(loadRelayList)
export const userInboxRelaySelections = makeUserData({ export const userMessagingRelayList = makeUserData({
mapStore: inboxRelaySelectionsByPubkey, mapStore: messagingRelayListsByPubkey,
loadItem: loadInboxRelaySelections, loadItem: loadMessagingRelayList,
}) })
export const loadUserInboxRelaySelections = makeUserLoader(loadInboxRelaySelections) export const forceLoadUserMessagingRelayList = makeUserLoader(forceLoadMessagingRelayList)
export const loadUserMessagingRelayList = makeUserLoader(loadMessagingRelayList)
export const userBlossomServers = makeUserData({ export const userBlossomServerList = makeUserData({
mapStore: blossomServersByPubkey, mapStore: blossomServerListsByPubkey,
loadItem: loadBlossomServers, 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 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 {getListTags, getPubkeyTagValues} from "@welshman/util"
import {throttled, withGetter} from "@welshman/store" import {throttled, withGetter} from "@welshman/store"
import {pubkey} from "./session.js" import {pubkey} from "./session.js"
import {follows, followsByPubkey} from "./follows.js" import {followLists, getFollowListsByPubkey, getFollowList} from "./follows.js"
import {mutes, mutesByPubkey} from "./mutes.js" import {muteLists, getMuteList} from "./mutes.js"
export const getFollows = (pubkey: string) => export const getFollows = (pubkey: string) =>
getPubkeyTagValues(getListTags(followsByPubkey.get().get(pubkey))) getPubkeyTagValues(getListTags(getFollowList(pubkey)))
export const getMutes = (pubkey: string) => export const getMutes = (pubkey: string) =>
getPubkeyTagValues(getListTags(mutesByPubkey.get().get(pubkey))) getPubkeyTagValues(getListTags(getMuteList(pubkey)))
export const getNetwork = (pubkey: string) => { export const getNetwork = (pubkey: string) => {
const pubkeys = new Set(getFollows(pubkey)) const pubkeys = new Set(getFollows(pubkey))
@@ -28,7 +28,7 @@ export const getNetwork = (pubkey: string) => {
} }
export const followersByPubkey = withGetter( export const followersByPubkey = withGetter(
derived(throttled(1000, follows), lists => { derived(throttled(1000, followLists), lists => {
const $followersByPubkey = new Map<string, Set<string>>() const $followersByPubkey = new Map<string, Set<string>>()
for (const list of lists) { for (const list of lists) {
@@ -42,7 +42,7 @@ export const followersByPubkey = withGetter(
) )
export const mutersByPubkey = withGetter( export const mutersByPubkey = withGetter(
derived(throttled(1000, mutes), lists => { derived(throttled(1000, muteLists), lists => {
const $mutersByPubkey = new Map<string, Set<string>>() const $mutersByPubkey = new Map<string, Set<string>>()
for (const list of lists) { 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 buildGraph = throttle(1000, () => {
const $pubkey = pubkey.get() const $pubkey = pubkey.get()
const $graph = new Map<string, number>() 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 follow of $follows) {
for (const pubkey of getFollows(follow)) { for (const pubkey of getFollows(follow)) {
@@ -89,8 +89,8 @@ const buildGraph = throttle(1000, () => {
}) })
pubkey.subscribe(buildGraph) pubkey.subscribe(buildGraph)
follows.subscribe(buildGraph) followLists.subscribe(buildGraph)
mutes.subscribe(buildGraph) muteLists.subscribe(buildGraph)
export const getWotScore = (pubkey: string, target: string) => { export const getWotScore = (pubkey: string, target: string) => {
const follows = pubkey ? getFollowsWhoFollow(pubkey, target) : getFollowers(target) const follows = pubkey ? getFollowsWhoFollow(pubkey, target) : getFollowers(target)
+29 -45
View File
@@ -10,15 +10,21 @@ import {
batcher, batcher,
postJson, postJson,
} from "@welshman/lib" } from "@welshman/lib"
import {collection} from "@welshman/store" import {getter, deriveItems, makeForceLoadItem, makeLoadItem, makeDeriveItem} from "@welshman/store"
import {deriveProfile, loadProfile} from "./profiles.js" import {deriveProfile, loadProfile} from "./profiles.js"
import {appContext} from "./context.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 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 // Use dufflepud if we it's set up to protect user privacy, otherwise fetch directly
if (base) { if (base) {
@@ -30,7 +36,7 @@ export const fetchZappers = async (lnurls: string[]) => {
) )
for (const {lnurl, info} of res?.data || []) { for (const {lnurl, info} of res?.data || []) {
tryCatch(() => zappersByLnurl.set(hexToBech32("lnurl", lnurl), info)) tryCatch(() => result.set(hexToBech32("lnurl", lnurl), info))
} }
} }
} else { } else {
@@ -45,61 +51,39 @@ export const fetchZappers = async (lnurls: string[]) => {
for (const {lnurl, info} of results) { for (const {lnurl, info} of results) {
if (info) { if (info) {
zappersByLnurl.set(lnurl, info) result.set(lnurl, info)
} }
} }
} }
return zappersByLnurl return lnurls.map(lnurl => {
} const info = result.get(lnurl)
export const { if (info) {
indexStore: zappersByLnurl, return {...info, lnurl}
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})
}
} }
})
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[] = []) => { export const loadZapperForPubkey = async (pubkey: string, relays: string[] = []) => {
const $profile = await loadProfile(pubkey, relays) const $profile = await loadProfile(pubkey, relays)
if (!$profile?.lnurl) { return $profile?.lnurl ? loadZapper($profile.lnurl) : undefined
return undefined
}
return loadZapper($profile.lnurl)
} }
export const deriveZapperForPubkey = (pubkey: string, relays: string[] = []) => export const deriveZapperForPubkey = (pubkey: string, relays: string[] = []) => {
derived([zappersByLnurl, deriveProfile(pubkey, relays)], ([$zappersByLnurl, $profile]) => { loadZapperForPubkey(pubkey, relays)
if (!$profile?.lnurl) {
return undefined
}
loadZapper($profile.lnurl) return derived([zappersByLnurl, deriveProfile(pubkey, relays)], ([$zappersByLnurl, $profile]) => {
return $profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined
return $zappersByLnurl.get($profile.lnurl)
}) })
}
export const getLnUrlsForEvent = async (event: TrustedEvent) => { export const getLnUrlsForEvent = async (event: TrustedEvent) => {
const lnurls = removeUndefined(getTagValues("zap", event.tags).map(getLnUrl)) const lnurls = removeUndefined(getTagValues("zap", event.tags).map(getLnUrl))
+8 -8
View File
@@ -21,7 +21,7 @@ import {
isShareableRelayUrl, isShareableRelayUrl,
PROFILE, PROFILE,
RELAYS, RELAYS,
INBOX_RELAYS, MESSAGING_RELAYS,
FOLLOWS, FOLLOWS,
WRAP, WRAP,
getPubkeyTagValues, getPubkeyTagValues,
@@ -37,7 +37,7 @@ import {
} from "@welshman/util" } from "@welshman/util"
import {Repository} from "@welshman/net" 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 = { export type RelaysAndFilters = {
relays: string[] relays: string[]
@@ -54,7 +54,7 @@ export type RouterOptions = {
/** /**
* Retrieves relays for the specified public key and mode. * Retrieves relays for the specified public key and mode.
* @param pubkey - The public key to retrieve relays for. * @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. * @returns An array of relay URLs as strings.
*/ */
getPubkeyRelays?: (pubkey: string, mode?: RelayMode) => string[] getPubkeyRelays?: (pubkey: string, mode?: RelayMode) => string[]
@@ -174,20 +174,20 @@ export class Router {
FromUser = () => this.FromRelays(this.getRelaysForUser(RelayMode.Write)) 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)) ForPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Read))
FromPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Write)) FromPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Write))
PubkeyInbox = (pubkey: string) => MessagesForPubkey = (pubkey: string) =>
this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Inbox)) this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Messages))
ForPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.ForPubkey(pubkey))) ForPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.ForPubkey(pubkey)))
FromPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.FromPubkey(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) => Event = (event: TrustedEvent) =>
this.FromRelays(this.getRelaysForPubkey(event.pubkey, RelayMode.Write)) this.FromRelays(this.getRelaysForPubkey(event.pubkey, RelayMode.Write))
@@ -371,7 +371,7 @@ export const getFilterSelectionsForWraps = (filter: Filter) => {
return [ return [
{ {
filter: {...filter, kinds: [WRAP]}, 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 {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 {matchFilters, getIdFilters, Filter, TrustedEvent} from "@welshman/util"
import {Repository, RepositoryUpdate, Tracker} from "@welshman/net" import {Repository, RepositoryUpdate, Tracker} from "@welshman/net"
import {deriveDeduplicated} from "./misc.js" import {deriveDeduplicated} from "./misc.js"
@@ -175,7 +175,7 @@ export const deriveEventsByIdForUrl = (
export type ItemsByKey<T> = Map<string, T> 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> 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 FEEDS = 10014
export const TOPICS = 10015 export const TOPICS = 10015
export const EMOJIS = 10030 export const EMOJIS = 10030
export const INBOX_RELAYS = 10050 export const MESSAGING_RELAYS = 10050
export const BLOSSOM_SERVERS = 10063 export const BLOSSOM_SERVERS = 10063
export const FILE_SERVERS = 10096 export const FILE_SERVERS = 10096
export const RELAY_MEMBERS = 13534 export const RELAY_MEMBERS = 13534
+1 -1
View File
@@ -5,7 +5,7 @@ import {last, normalizeUrl, stripProtocol} from "@welshman/lib"
export enum RelayMode { export enum RelayMode {
Read = "read", Read = "read",
Write = "write", Write = "write",
Inbox = "inbox", Messages = "messages",
} }
export type RelayProfile = { export type RelayProfile = {