Optimize deriveUserItem, avoid double load of quotes

This commit is contained in:
Jon Staab 2026-07-29 18:24:57 -07:00
parent 2a5849afcb
commit 934f1cdf10
12 changed files with 75 additions and 78 deletions

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {RelayLists} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@ -20,7 +21,7 @@
const {next}: Props = $props() const {next}: Props = $props()
const userRelayList = deriveUserItem($app => $relayLists) const userRelayList = deriveUserItem(RelayLists)
const back = () => history.back() const back = () => history.back()

View file

@ -2,15 +2,21 @@
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {derived, writable} from "svelte/store" import {derived, writable} from "svelte/store"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {Address, MESSAGE, eventOutbox, relays as relaySelections, seen} from "@welshman/util" import {
Address,
MESSAGE,
eventOutbox,
getIdFilters,
relays as relaySelections,
seen,
} from "@welshman/util"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte" import NoteContent from "@app/components/NoteContent.svelte"
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte" import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import {router} from "@app/core" import {network, router} from "@app/core"
import {deriveEvent} from "@app/repository" import {deriveEvent} from "@app/repository"
import {entityLink} from "@app/env" import {entityLink} from "@app/env"
import {goToEvent} from "@app/routes" import {goToEvent} from "@app/routes"
@ -26,12 +32,16 @@
const {id, identifier, kind, pubkey, relays = []} = value const {id, identifier, kind, pubkey, relays = []} = value
const idOrAddress = id || new Address(kind, pubkey, identifier).toString() const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
const ref = {id, pubkey, kind, identifier, relays} const ref = {id, pubkey, kind, identifier, relays}
const hints = removeUndefined([...relays, url])
const mergedRelays = writable(hints)
const quote = deriveEvent(idOrAddress, hints)
// Start with the hints we were handed, then widen to everything the router can work out: // Start with the hints we were handed, then widen to everything the router can work out:
// where the quoted event has been seen, its author's outbox, and — since whoever quoted it // where the quoted event has been seen, its author's outbox, and — since whoever quoted it
// must have seen it — the relays the quoting event came from. // must have seen it — the relays the quoting event came from. Resolving that may take a
const mergedRelays = writable(removeUndefined([...relays, url])) // round trip for the author's relay list, so only load again if the hints came up empty.
$router.resolver $router.resolver
.relays([ .relays([
...relaySelections(removeUndefined([url])), ...relaySelections(removeUndefined([url])),
@ -39,11 +49,13 @@
eventOutbox(ref), eventOutbox(ref),
seen(event, 0.5), seen(event, 0.5),
]) ])
.then(urls => mergedRelays.set(urls)) .then(urls => {
mergedRelays.set(urls)
const quote = derived(mergedRelays, ($mergedRelays, set: (event: Maybe<TrustedEvent>) => void) => if (!$quote) {
deriveEvent(idOrAddress, $mergedRelays).subscribe(set), $network.load({filters: getIdFilters([idOrAddress]), relays: urls})
) }
})
const entity = derived(mergedRelays, $mergedRelays => const entity = derived(mergedRelays, $mergedRelays =>
id id

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import cx from "classnames" import cx from "classnames"
import {Profiles} from "@welshman/app"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
@ -17,7 +18,7 @@
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {notifications} from "@app/notifications" import {notifications} from "@app/notifications"
import {goToChat, makeSpacePath} from "@app/routes" import {goToChat, makeSpacePath} from "@app/routes"
import {deriveUserItem, profiles} from "@app/core" import {deriveUserItem} from "@app/core"
type Props = { type Props = {
children?: Snippet children?: Snippet
@ -25,7 +26,7 @@
const {children}: Props = $props() const {children}: Props = $props()
const userProfile = deriveUserItem($app => $profiles) const userProfile = deriveUserItem(Profiles)
const chatHandler = () => goToChat() const chatHandler = () => goToChat()

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {readable} from "svelte/store"
import {clamp} from "@welshman/lib" import {clamp} from "@welshman/lib"
import {WotScope} from "@welshman/app"
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl" import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte" import ProfileCircles from "@app/components/ProfileCircles.svelte"
@ -14,38 +14,18 @@
const {pubkey: target, isSelf = false}: Props = $props() const {pubkey: target, isSelf = false}: Props = $props()
const profileDisplay = $profiles.display(target).$ const profileDisplay = $profiles.display(target).$
const followers = $wot.followers(target).$ const followers = $wot.followers(target, WotScope.Follows).$
const score = $wot.score(target, WotScope.Follows).$
const score = $derived($wot.wotScore($user.pubkey, target).$) const display = $derived(isSelf ? $followers.length : clamp([0, 100], $score))
const followsWhoFollow = $derived(
!isSelf && $user.pubkey
? $wot.followsWhoFollow($user.pubkey, target).$
: readable<string[]>([]),
)
const followerCount = $derived($followers.length)
const networkFollowCount = $derived(isSelf ? followerCount : $followsWhoFollow.length)
const displayScore = $derived(isSelf ? followerCount : Math.round(clamp([0, 100], $score)))
const progress = $derived(isSelf ? undefined : displayScore)
const trustMessage = $derived.by(() => { const trustMessage = $derived.by(() => {
if (isSelf) { if ($followers.length > 0) {
if (followerCount === 0) return "No followers in your network yet." return `Followed by ${$followers.length}+ people in your network.`
return `Followed by ${followerCount}+ people in your network.`
} }
if (networkFollowCount > 0) { return isSelf
return `Followed by ${networkFollowCount}+ people in your network.` ? "No followers in your network yet."
} : "This user is not well known in your network."
if (displayScore >= 50) return "This user is highly trusted in your network."
if (displayScore >= 10) return "This user has some trust in your network."
return "This user is not well known in your network."
}) })
$effect(() => { $effect(() => {
@ -66,24 +46,24 @@
<span class="text-sm opacity-75">{isSelf ? "Followers" : "Trust score"}</span> <span class="text-sm opacity-75">{isSelf ? "Followers" : "Trust score"}</span>
<span class="text-lg font-semibold"> <span class="text-lg font-semibold">
{#if isSelf} {#if isSelf}
{displayScore} {display}
{:else} {:else}
{displayScore} / 100 {display} / 100
{/if} {/if}
</span> </span>
</div> </div>
{#if !isSelf} {#if !isSelf}
<progress class="progress w-full" value={progress} max="100"></progress> <progress class="progress w-full" value={display} max="100"></progress>
{/if} {/if}
<p class="text-sm opacity-75">{trustMessage}</p> <p class="text-sm opacity-75">{trustMessage}</p>
</div> </div>
{#if $followsWhoFollow.length > 0} {#if !isSelf && $followers.length > 0}
<div class="flex flex-col gap-2 border-t border-line pt-4"> <div class="flex flex-col gap-2 border-t border-line pt-4">
<p class="text-sm font-medium">People who follow {$profileDisplay}</p> <p class="text-sm font-medium">People who follow {$profileDisplay}</p>
<ProfileCircles pubkeys={$followsWhoFollow} limit={5} /> <ProfileCircles pubkeys={$followers} limit={5} />
<p class="text-sm opacity-75"> <p class="text-sm opacity-75">
{$followsWhoFollow.length} {$followers.length}
{$followsWhoFollow.length === 1 ? "person" : "people"} you follow also follow {$followers.length === 1 ? "person" : "people"} you follow also follow
{$profileDisplay}. {$profileDisplay}.
</p> </p>
</div> </div>

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {getWalletAddress} from "@welshman/util" import {getWalletAddress} from "@welshman/util"
import {Profiles} from "@welshman/app"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@ -13,7 +14,7 @@
import {clearModals} from "@app/modal" import {clearModals} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
const userProfile = deriveUserItem($app => $profiles) const userProfile = deriveUserItem(Profiles)
const lud16 = getWalletAddress(wallet.get()!) const lud16 = getWalletAddress(wallet.get()!)

View file

@ -4,6 +4,7 @@
import {sleep} from "@welshman/lib" import {sleep} from "@welshman/lib"
import {WalletType} from "@welshman/util" import {WalletType} from "@welshman/util"
import type {NWCInfo} from "@welshman/util" import type {NWCInfo} from "@welshman/util"
import {Profiles} from "@welshman/app"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl" import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
import Lock from "@assets/icons/lock-keyhole.svg?dataurl" import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
@ -23,12 +24,12 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import WalletAsReceivingAddress from "@app/components/WalletAsReceivingAddress.svelte" import WalletAsReceivingAddress from "@app/components/WalletAsReceivingAddress.svelte"
import {deriveUserItem, profiles} from "@app/core" import {deriveUserItem} from "@app/core"
import {getWebLn, wallet} from "@app/lightning" import {getWebLn, wallet} from "@app/lightning"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
const userProfile = deriveUserItem($app => $profiles) const userProfile = deriveUserItem(Profiles)
const back = () => history.back() const back = () => history.back()

View file

@ -15,6 +15,7 @@
<script lang="ts"> <script lang="ts">
import {clamp} from "@welshman/lib" import {clamp} from "@welshman/lib"
import {WotScope} from "@welshman/app"
import {followLists, user, wot} from "@app/core" import {followLists, user, wot} from "@app/core"
interface Props { interface Props {
@ -27,7 +28,7 @@
const radius = 6 const radius = 6
const center = radius + 1 const center = radius + 1
const score = $derived($wot.wotScore($user.pubkey, target).$) const score = $derived($wot.score(target, WotScope.Follows).$)
const follows = $derived($followLists.one($user.pubkey)) const follows = $derived($followLists.one($user.pubkey))
const active = $derived(($follows?.pubkeys() ?? []).includes(target)) const active = $derived(($follows?.pubkeys() ?? []).includes(target))
const normalizedScore = $derived(clamp([0, max], $score) / max) const normalizedScore = $derived(clamp([0, max], $score) / max)

View file

@ -3,7 +3,7 @@ import type {Readable} from "svelte/store"
import {always} from "@welshman/lib" import {always} from "@welshman/lib"
import type {Maybe} from "@welshman/lib" import type {Maybe} from "@welshman/lib"
import {withGetter} from "@welshman/store" import {withGetter} from "@welshman/store"
import type {ItemsByKey, ReadableWithGetter} from "@welshman/store" import type {ReadableWithGetter} from "@welshman/store"
import { import {
App, App,
BlockedRelayLists, BlockedRelayLists,
@ -37,7 +37,7 @@ import {
appPolicyRelayStats, appPolicyRelayStats,
appPolicyWraps, appPolicyWraps,
} from "@welshman/app" } from "@welshman/app"
import type {AppPolicy, IApp, Projection, Session} from "@welshman/app" import type {AppPolicy, DerivedPlugin, Plugin, Session} from "@welshman/app"
import type {BaseEventReader, EventWriter, KindFactory} from "@welshman/domain" import type {BaseEventReader, EventWriter, KindFactory} from "@welshman/domain"
import {maybeMakeRelayMockAdapter} from "@lib/test/relayMocks" import {maybeMakeRelayMockAdapter} from "@lib/test/relayMocks"
import {DEFAULT_RELAYS, DEFAULT_SEARCH_RELAYS, DUFFLEPUD_URL, INDEXER_RELAYS} from "@app/env" import {DEFAULT_RELAYS, DEFAULT_SEARCH_RELAYS, DUFFLEPUD_URL, INDEXER_RELAYS} from "@app/env"
@ -106,17 +106,20 @@ export const user = withGetter(derived(app, $app => User.require($app)))
export const fromApp = <T>(read: ($app: App) => Readable<T>): Readable<T> => export const fromApp = <T>(read: ($app: App) => Readable<T>): Readable<T> =>
derived(app, ($app, set: (value: T) => void) => read($app).subscribe(set)) derived(app, ($app, set: (value: T) => void) => read($app).subscribe(set))
// The signed-in user's entry in a keyed collection, e.g. // The signed-in user's entry in a keyed collection, e.g. deriveUserItem(Profiles).
// deriveUserItem($app => $app.use(Profiles)). export const deriveUserItem = <T>(Ctor: Plugin<DerivedPlugin<T>>) =>
export const deriveUserItem = <T>(getPlugin: ($app: App) => {index: Projection<ItemsByKey<T>>}) => derived(app, ($app, set: (item: Maybe<T>) => void) => {
derived( let previous: Maybe<T>
[app, fromApp($app => getPlugin($app).index.$)],
([$app, $index]: [App, ItemsByKey<T>]) => {
const $pubkey = $app.user?.pubkey
return $pubkey ? $index.get($pubkey) : undefined return $app.use(Ctor).index.$.subscribe($index => {
}, const item = $app.user ? $index.get($app.user.pubkey) : undefined
)
if (item !== previous) {
previous = item
set(item)
}
})
})
export const login = async ($session: Session) => { export const login = async ($session: Session) => {
const $user = await User.fromSession($session) const $user = await User.fromSession($session)
@ -135,8 +138,7 @@ export const login = async ($session: Session) => {
// Plugins bound to the current app, so `$profiles` in a component and `profiles.get()` in a // Plugins bound to the current app, so `$profiles` in a component and `profiles.get()` in a
// module both stay pointed at the right one after login swaps it. Flotilla's own plugins expose // module both stay pointed at the right one after login swaps it. Flotilla's own plugins expose
// themselves the same way. // themselves the same way.
export const usePlugin = <T>(Ctor: new (app: IApp) => T) => export const usePlugin = <T>(Ctor: Plugin<T>) => withGetter(derived(app, $app => $app.use(Ctor)))
withGetter(derived(app, $app => $app.use(Ctor)))
export const blockedRelayLists = usePlugin(BlockedRelayLists) export const blockedRelayLists = usePlugin(BlockedRelayLists)
export const blossomServerLists = usePlugin(BlossomServerLists) export const blossomServerLists = usePlugin(BlossomServerLists)

View file

@ -1,9 +1,8 @@
import {get, writable} from "svelte/store" import {get, writable} from "svelte/store"
import {on, call, dissoc, assoc, noop, uniq} from "@welshman/lib" import {on, call, dissoc, assoc, uniq} from "@welshman/lib"
import {isDVMKind, isEphemeralKind, verifyEvent} from "@welshman/util" import {isDVMKind, isEphemeralKind, verifyEvent} from "@welshman/util"
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net" import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
import { import {
AuthStatus,
SocketEvent, SocketEvent,
isRelayEvent, isRelayEvent,
isRelayOk, isRelayOk,
@ -15,7 +14,6 @@ import {
isClientNegOpen, isClientNegOpen,
isClientNegClose, isClientNegClose,
} from "@welshman/net" } from "@welshman/net"
import {merged} from "@welshman/store"
import { import {
BlockedRelayLists, BlockedRelayLists,
MessagingRelayLists, MessagingRelayLists,

View file

@ -138,7 +138,7 @@ export const publishRoomQuote = async ({
// User // User
export const userRoomList = deriveUserItem($app => $app.use(RoomLists)) export const userRoomList = deriveUserItem(RoomLists)
export const userSpaceUrls = derived(userRoomList, $userRoomList => $userRoomList?.urls() ?? []) export const userSpaceUrls = derived(userRoomList, $userRoomList => $userRoomList?.urls() ?? [])

View file

@ -96,13 +96,13 @@ const pullAndListen = ({url, signal, filters}: SyncOpts) => {
}) })
} }
const userRoomList = deriveUserItem($app => $app.use(RoomLists)) const userRoomList = deriveUserItem(RoomLists)
const userRelayList = deriveUserItem($app => $app.use(RelayLists)) const userRelayList = deriveUserItem(RelayLists)
const userFollowList = deriveUserItem($app => $app.use(FollowLists)) const userFollowList = deriveUserItem(FollowLists)
const userMessagingRelayList = deriveUserItem($app => $app.use(MessagingRelayLists)) const userMessagingRelayList = deriveUserItem(MessagingRelayLists)
const getSpaceUrls = ($roomList: Maybe<RoomListReader>) => const getSpaceUrls = ($roomList: Maybe<RoomListReader>) =>
PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : ($roomList?.urls() ?? []) PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : ($roomList?.urls() ?? [])

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {publish} from "@welshman/app" import {BlossomServerLists, MuteLists, publish} from "@welshman/app"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl" import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
@ -17,8 +17,8 @@
import {PLATFORM_NAME} from "@app/env" import {PLATFORM_NAME} from "@app/env"
import {userSettingsValues, publishSettings} from "@app/settings" import {userSettingsValues, publishSettings} from "@app/settings"
const userMuteList = deriveUserItem($app => $muteLists) const userMuteList = deriveUserItem(MuteLists)
const userBlossomServerList = deriveUserItem($app => $blossomServerLists) const userBlossomServerList = deriveUserItem(BlossomServerLists)
const reset = () => { const reset = () => {
settings = {...$userSettingsValues} settings = {...$userSettingsValues}