Allow users to set max concurrent relays

This commit is contained in:
Jonathan Staab 2023-02-18 12:52:15 -06:00
parent bab545f789
commit 5cbd59d99a
19 changed files with 98 additions and 107 deletions

View File

@ -16,6 +16,7 @@
- [x] Improve anonymous and new user experience by prompting for relays and follows
- [x] Fixed kind0 merging to avoid dropping properties coracle doesn't support
- [x] Implement NIP-65 properly
- [x] Allow users to set max number of concurrent relays
## 0.2.11

View File

@ -1,15 +1,10 @@
# Current
- [ ] Fix anon/new user experience
- [ ] When logging in rather than generating a new keypair, ask for a relay to bootstrap from
- [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead.
- [ ] Test publishing events with zero relays
- [ ] Try lumping tables into a single key each to reduce load/save contention and time
- [ ] Fix turning off link previews, or make sure it applies to images/videos too
- [ ] Allow user to set sample size to manage bandwidth/performance tradeoff
# Snacks
- [ ] If a user has no write relays (or is not logged in), open a modal
- [ ] open web+nostr links like snort
- [ ] DM/chat read status in encrypted note
- [ ] Relay recommendations based on follows/followers

View File

@ -24,7 +24,7 @@
import {loadAppData} from "src/app"
import alerts from "src/app/alerts"
import messages from "src/app/messages"
import {modal, toast, settings, routes, menuIsOpen, logUsage} from "src/app/ui"
import {modal, toast, routes, menuIsOpen, logUsage} from "src/app/ui"
import RelayCard from "src/partials/RelayCard.svelte"
import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte'
@ -119,7 +119,7 @@
const interval = setInterval(
async () => {
const {dufflepudUrl} = $settings
const {dufflepudUrl} = user.getSettings()
if (!dufflepudUrl) {
return

View File

@ -4,7 +4,7 @@ import {chunk} from 'hurdak/lib/hurdak'
import {batch, timedelta, now} from 'src/util/misc'
import {
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
getUserReadRelays, getRelaysForEventChildren, getUserRelays
getRelaysForEventChildren, sampleRelays,
} from 'src/agent/relays'
import database from 'src/agent/database'
import pool from 'src/agent/pool'
@ -32,11 +32,7 @@ const publish = async (relays, event) => {
}
const load = async (relays, filter, opts?): Promise<Record<string, unknown>[]> => {
if (relays.length === 0) {
relays = getUserReadRelays()
}
const events = await pool.request(relays, filter, opts)
const events = await pool.request(sampleRelays(relays), filter, opts)
await sync.processEvents(events)
@ -44,11 +40,7 @@ const load = async (relays, filter, opts?): Promise<Record<string, unknown>[]> =
}
const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
if (relays.length === 0) {
relays = getUserReadRelays()
}
return pool.subscribe(relays, filter, {
return pool.subscribe(sampleRelays(relays), filter, {
onEvent: batch(300, events => {
if (shouldProcess) {
sync.processEvents(events)
@ -62,12 +54,8 @@ const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
}
const listenUntilEose = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
if (relays.length === 0) {
relays = getUserReadRelays()
}
return new Promise(resolve => {
pool.subscribeUntilEose(relays, filter, {
pool.subscribeUntilEose(sampleRelays(relays), filter, {
onClose: () => resolve(),
onEvent: batch(300, events => {
if (shouldProcess) {
@ -92,25 +80,22 @@ const loadPeople = async (pubkeys, {relays = null, kinds = personKinds, force =
await Promise.all(
chunk(256, pubkeys).map(async chunk => {
// Use the best relays we have, but fall back to user relays
const chunkRelays = relays || (
getAllPubkeyWriteRelays(chunk)
.concat(getUserReadRelays())
.slice(0, 10)
await load(
sampleRelays(relays || getAllPubkeyWriteRelays(chunk), 0.5),
{kinds, authors: chunk},
opts
)
await load(chunkRelays, {kinds, authors: chunk}, opts)
})
)
}
const loadParents = notes => {
const notesWithParent = notes.filter(findReplyId)
const relays = aggregateScores(notesWithParent.map(getRelaysForEventParent))
.concat(getUserRelays())
.slice(0, 10)
return load(relays, {kinds: [1], ids: notesWithParent.map(findReplyId)})
return load(
sampleRelays(aggregateScores(notesWithParent.map(getRelaysForEventParent)), 0.3),
{kinds: [1], ids: notesWithParent.map(findReplyId)}
)
}
const streamContext = ({notes, updateNotes, depth = 0}) => {
@ -118,9 +103,7 @@ const streamContext = ({notes, updateNotes, depth = 0}) => {
chunk(256, notes).forEach(chunk => {
const authors = getStalePubkeys(pluck('pubkey', chunk))
const filter = [{kinds: [1, 7], '#e': pluck('id', chunk)}] as Array<object>
const relays = aggregateScores(chunk.map(getRelaysForEventChildren))
.concat(getUserRelays())
.slice(0, 10)
const relays = sampleRelays(aggregateScores(chunk.map(getRelaysForEventChildren)))
if (authors.length > 0) {
filter.push({kinds: personKinds, authors})

View File

@ -125,7 +125,27 @@ export const uniqByUrl = uniqBy(prop('url'))
export const sortByScore = sortBy(r => -r.score)
export const sampleRelays = relays => shuffle(relays).slice(0, 30)
export const sampleRelays = (relays, scale = 1) => {
let limit = user.getSetting('relayLimit')
// Allow the caller to scale down how many relays we're bothering depending on
// the use case, but only if we have enough relays to handle it
if (limit > 10) {
limit *= scale
}
// Shuffle and limit target relays
relays = shuffle(relays).slice(0, limit)
// If we're still under the limit, add user relays for good measure
if (relays.length < limit) {
relays = relays.concat(
shuffle(getUserReadRelays()).slice(0, limit - relays.length)
)
}
return relays
}
export const aggregateScores = relayGroups => {
const scores = {} as Record<string, {

View File

@ -180,7 +180,6 @@ const getWeight = type => {
if (type === 'kind:3') return 0.8
if (type === 'kind:2') return 0.5
if (type === 'seen') return 0.2
if (type === 'tag') return 0.1
}
const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => {
@ -268,19 +267,6 @@ const processRoutes = async events => {
},
default: noop,
})
// Add tag hints
events.forEach(e => {
Tags.wrap(e.tags).type("p").all().forEach(([_, pubkey, url]) => {
updates.push(
calculateRoute(pubkey, url, 'tag', 'write', e.created_at)
)
updates.push(
calculateRoute(pubkey, url, 'tag', 'read', e.created_at)
)
})
})
}
updates = updates.filter(identity)

View File

@ -14,6 +14,7 @@ import cmd from 'src/agent/cmd'
// sync this stuff to regular private variables so we don't have to constantly call
// `get` on our stores.
let settingsCopy = null
let profileCopy = null
let petnamesCopy = []
let relaysCopy = []
@ -21,6 +22,13 @@ let relaysCopy = []
const anonPetnames = synced('agent/user/anonPetnames', [])
const anonRelays = synced('agent/user/anonRelays', [])
const settings = synced("agent/user/settings", {
relayLimit: 20,
showMedia: true,
reportAnalytics: true,
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
})
const profile = derived(
[keys.pubkey, database.people as Readable<any>],
([pubkey, $people]) => {
@ -46,6 +54,10 @@ const relays = derived(
// Keep our copies up to date
settings.subscribe($settings => {
settingsCopy = $settings
})
profile.subscribe($profile => {
profileCopy = $profile
})
@ -59,6 +71,13 @@ relays.subscribe($relays => {
})
const user = {
// Settings
settings,
getSettings: () => settingsCopy,
getSetting: k => settingsCopy[k],
dufflepud: path => `${settingsCopy.dufflepudUrl}${path}`,
// Profile
profile,
@ -102,7 +121,7 @@ const user = {
}
},
async addRelay(url) {
this.updateRelays($relays => $relays.concat({url, write: false, read: true}))
this.updateRelays($relays => $relays.concat({url, write: true, read: true}))
},
async removeRelay(url) {
this.updateRelays(reject(whereEq({url})))

View File

@ -1,6 +1,5 @@
import {pluck} from 'ramda'
import {first} from 'hurdak/lib/hurdak'
import {log} from 'src/util/logger'
import {writable} from 'svelte/store'
import pool from 'src/agent/pool'
import {getUserRelays} from 'src/agent/relays'
@ -9,23 +8,16 @@ export const slowConnections = writable([])
setInterval(() => {
// Only notify about relays the user is actually subscribed to
const relayUrls = pluck('url', getUserRelays())
const relayUrls = new Set(pluck('url', getUserRelays()))
// Prune connections we haven't used in a while
pool.getConnections()
.filter(conn => conn.lastRequest < Date.now() - 60_000)
.forEach(conn => conn.disconnect())
// Log stats for debugging purposes
log(
'Connection stats',
pool.getConnections()
.map(c => `${c.nostr.url} ${c.getQuality().join(' ')}`)
)
// Alert the user to any heinously slow connections
slowConnections.set(
pool.getConnections()
.filter(c => relayUrls.includes(c.nostr.url) && first(c.getQuality()) < 0.3)
.filter(c => relayUrls.has(c.nostr.url) && first(c.getQuality()) < 0.3)
)
}, 30_000)

View File

@ -1,4 +1,5 @@
import type {DisplayEvent} from 'src/util/types'
import {navigate} from 'svelte-routing'
import {omit, sortBy, identity} from 'ramda'
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
import {renderContent} from 'src/util/html'
@ -23,7 +24,7 @@ export const loadAppData = async pubkey => {
}
}
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
export const login = ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
if (privkey) {
keys.setPrivateKey(privkey)
} else {
@ -33,6 +34,12 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
modal.set({type: 'login/connect', noEscape: true})
}
export const signup = privkey => {
keys.setPrivateKey(privkey)
navigate('/notes/network')
}
export const renderNote = (note, {showEntire = false}) => {
const shouldEllipsize = note.content.length > 500 && !showEntire
const peopleByPubkey = createMap(

View File

@ -6,8 +6,9 @@ import {navigate} from "svelte-routing"
import {nip19} from 'nostr-tools'
import {writable, get} from "svelte/store"
import {globalHistory} from "svelte-routing/src/history"
import {synced, sleep} from "src/util/misc"
import {sleep} from "src/util/misc"
import {warn} from 'src/util/logger'
import user from 'src/agent/user'
// Routing
@ -74,15 +75,6 @@ export const modal = {
},
}
// Settings, alerts, etc
export const settings = synced("coracle/settings", {
reportAnalytics: true,
showLinkPreviews: true,
relayLimit: 30,
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
})
// Wait for bugsnag to be started in main
setTimeout(() => {
Bugsnag.addOnError(event => {
@ -90,7 +82,7 @@ setTimeout(() => {
return false
}
if (!get(settings).reportAnalytics) {
if (!user.getSetting('reportAnalytics')) {
return false
}
@ -103,7 +95,7 @@ setTimeout(() => {
const session = Math.random().toString().slice(2)
export const logUsage = async name => {
const {dufflepudUrl, reportAnalytics} = get(settings)
const {dufflepudUrl, reportAnalytics} = user.getSettings()
if (reportAnalytics) {
try {

View File

@ -12,7 +12,7 @@
import ImageCircle from 'src/partials/ImageCircle.svelte'
import Preview from 'src/partials/Preview.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {toast, settings, modal} from "src/app/ui"
import {toast, modal} from "src/app/ui"
import {renderNote} from "src/app"
import {formatTimestamp, stringToColor} from 'src/util/misc'
import Compose from "src/partials/Compose.svelte"
@ -37,7 +37,7 @@
let replyContainer = null
const {profile} = user
const links = $settings.showLinkPreviews ? extractUrls(note.content) || [] : []
const links = extractUrls(note.content)
const showEntire = anchorId === note.id
const interactive = !anchorId || !showEntire
const person = database.watch('people', () => database.getPersonWithFallback(note.pubkey))
@ -247,11 +247,11 @@
{:else}
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
<p>{@html renderNote(note, {showEntire})}</p>
{#each links.slice(-2) as link}
{#if user.getSetting('showMedia') && links.length > 0}
<button class="inline-block" on:click={e => e.stopPropagation()}>
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
<Preview url={last(links)} />
</button>
{/each}
{/if}
</div>
<div class="flex justify-between text-light" on:click={e => e.stopPropagation()}>
<div class="flex gap-6">

View File

@ -10,7 +10,6 @@
import Note from "src/partials/Note.svelte"
import user from 'src/agent/user'
import network from 'src/agent/network'
import {getUserReadRelays, uniqByUrl} from 'src/agent/relays'
import {modal} from "src/app/ui"
import {mergeParents} from "src/app"
@ -22,18 +21,13 @@
let notesBuffer = []
const since = now()
const maxNotes = 300
const maxNotes = 100
const cursor = new Cursor()
const {profile} = user
const muffle = Tags
.wrap(($profile?.muffle || []).filter(t => Math.random() > parseFloat(last(t))))
.values().all()
// Sample relays in case we have a whole ton of them. Add in user relays in
// case we don't have any
const sampleRelays = () =>
uniqByUrl(relays.concat(getUserReadRelays())).slice(0, 30)
const processNewNotes = async newNotes => {
// Remove people we're not interested in hearing about, sort by created date
newNotes = newNotes.filter(e => !muffle.includes(e.pubkey))
@ -82,7 +76,7 @@
}
onMount(() => {
const sub = network.listen(sampleRelays(), {...filter, since}, onChunk)
const sub = network.listen(relays, {...filter, since}, onChunk)
const scroller = createScroller(() => {
if ($modal) {
@ -91,7 +85,7 @@
const {limit, until} = cursor
return network.listenUntilEose(sampleRelays(), {...filter, until, limit}, onChunk)
return network.listenUntilEose(relays, {...filter, until, limit}, onChunk)
})
return () => {

View File

@ -2,9 +2,9 @@
import {onMount} from 'svelte'
import {slide} from 'svelte/transition'
import Anchor from 'src/partials/Anchor.svelte'
import user from 'src/agent/user'
export let url
export let endpoint
let preview
@ -15,7 +15,7 @@
preview = {video: url}
} else {
try {
const res = await fetch(endpoint, {
const res = await fetch(user.dufflepud('/link/preview'), {
method: 'POST',
body: JSON.stringify({url}),
headers: {

View File

@ -64,11 +64,13 @@
</p>
</div>
{#if joined}
{#if $relays.length > 1}
<button
class="flex gap-3 items-center text-light"
on:click={() => user.removeRelay(relay.url)}>
<i class="fa fa-right-from-bracket" /> Leave
</button>
{/if}
{:else}
<button
class="flex gap-3 items-center text-light"

View File

@ -12,7 +12,7 @@
const {nostr} = window as any
if (nostr) {
await login({pubkey: await nostr.getPublicKey()})
login({pubkey: await nostr.getPublicKey()})
} else {
modal.set({type: 'login/privkey'})
}

View File

@ -8,9 +8,9 @@
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import user from 'src/agent/user'
import {toast, settings} from "src/app/ui"
import {toast} from "src/app/ui"
let values = {...$settings}
let values = {...user.getSettings()}
onMount(async () => {
if (!user.getProfile()) {
@ -21,7 +21,7 @@
const submit = async event => {
event.preventDefault()
settings.set(values)
user.settings.set(values)
toast.show("info", "Your settings have been saved!")
}
@ -38,11 +38,11 @@
<div class="flex flex-col gap-8 w-full">
<div class="flex flex-col gap-1">
<div class="flex gap-2 items-center">
<strong>Show link and image previews</strong>
<Toggle bind:value={values.showLinkPreviews} />
<strong>Show images and link previews</strong>
<Toggle bind:value={values.showMedia} />
</div>
<p class="text-sm text-light">
If enabled, coracle will automatically retrieve a link preview for the first link
If enabled, coracle will automatically retrieve a link preview for the last link
in any note.
</p>
</div>

View File

@ -10,13 +10,13 @@
let nsec = ''
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const logIn = async () => {
const logIn = () => {
const privkey = (nsec.startsWith('nsec') ? nip19.decode(nsec).data : nsec) as string
if (!privkey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid private key.")
} else {
await login({privkey})
login({privkey})
}
}
</script>

View File

@ -9,13 +9,13 @@
let npub = ''
const logIn = async () => {
const logIn = () => {
const pubkey = (npub.startsWith('npub') ? nip19.decode(npub).data : npub) as string
if (!pubkey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid public key.")
} else {
await login({pubkey})
login({pubkey})
}
}
</script>

View File

@ -6,13 +6,13 @@
import Content from 'src/partials/Content.svelte'
import Heading from 'src/partials/Heading.svelte'
import {toast} from "src/app/ui"
import {login} from "src/app"
import {signup} from "src/app"
const nsec = nip19.nsecEncode(generatePrivateKey())
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const logIn = async () => {
await login({privkey: nip19.decode(nsec).data as string})
const logIn = () => {
signup(nip19.decode(nsec).data as string)
}
const copyKey = () => {