Add request utils for complex requests

This commit is contained in:
Jon Staab
2024-12-10 16:38:22 -08:00
parent 19d67783fc
commit df42ec9915
10 changed files with 165 additions and 139 deletions
+2 -30
View File
@@ -27,8 +27,8 @@ import {
getRelayTagValues, getRelayTagValues,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, EventTemplate, List} from "@welshman/util" import type {TrustedEvent, EventTemplate, List} from "@welshman/util"
import type {SubscribeRequestWithHandlers, Subscription} from "@welshman/net" import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {PublishStatus, AuthStatus, SocketStatus, SubscriptionEvent} from "@welshman/net" import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer" import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
import { import {
pubkey, pubkey,
@@ -51,7 +51,6 @@ import {
nip44EncryptToSelf, nip44EncryptToSelf,
loadRelay, loadRelay,
addSession, addSession,
subscribe,
clearStorage, clearStorage,
dropSession, dropSession,
} from "@welshman/app" } from "@welshman/app"
@@ -97,33 +96,6 @@ export const makeIMeta = (url: string, data: Record<string, string>) => [
...Object.entries(data).map(([k, v]) => [k, v].join(" ")), ...Object.entries(data).map(([k, v]) => [k, v].join(" ")),
] ]
export const subscribePersistent = (request: SubscribeRequestWithHandlers) => {
let sub: Subscription
let done = false
const start = async () => {
// If the subscription gets closed quickly, don't start flapping
await Promise.all([
sleep(30_000),
new Promise(resolve => {
sub = subscribe(request)
sub.emitter.on(SubscriptionEvent.Complete, resolve)
}),
])
if (!done) {
start()
}
}
start()
return () => {
done = true
sub?.close()
}
}
export const getThunkError = async (thunk: Thunk) => { export const getThunkError = async (thunk: Thunk) => {
const result = await thunk.result const result = await thunk.result
const [{status, message}] = Object.values(result) as any const [{status, message}] = Object.values(result) as any
+2 -2
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {displayRelayUrl, GROUP_META} from "@welshman/util" import {displayRelayUrl, GROUP_META} from "@welshman/util"
import {load} from "@welshman/app"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -24,6 +23,7 @@
deriveOtherRooms, deriveOtherRooms,
} from "@app/state" } from "@app/state"
import {deriveNotification, THREAD_FILTERS} from "@app/notifications" import {deriveNotification, THREAD_FILTERS} from "@app/notifications"
import {pullConservatively} from "@app/requests"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
@@ -65,7 +65,7 @@
onMount(async () => { onMount(async () => {
replaceState = Boolean(element.closest(".drawer")) replaceState = Boolean(element.closest(".drawer"))
load({relays: [url], filters: [{kinds: [GROUP_META]}]}) pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]})
}) })
</script> </script>
+13 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {groupBy, uniqBy} from "@welshman/lib" import {groupBy, uniqBy, batch} from "@welshman/lib"
import {REACTION} from "@welshman/util" import {REACTION, DELETE} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store" import {deriveEvents} from "@welshman/store"
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app" import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
import {displayList} from "@lib/util" import {displayList} from "@lib/util"
@@ -23,7 +24,16 @@
) )
onMount(() => { onMount(() => {
load({relays, filters}) load({
relays,
filters,
onEvent: batch(300, (events: TrustedEvent[]) => {
load({
relays,
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
})
}),
})
}) })
</script> </script>
+106
View File
@@ -0,0 +1,106 @@
import type {Unsubscriber} from "svelte/store"
import {sleep, partition, assoc, now, sortBy} from "@welshman/lib"
import {MESSAGE, DELETE, THREAD, COMMENT} from "@welshman/util"
import type {SubscribeRequestWithHandlers, Subscription} from "@welshman/net"
import {SubscriptionEvent} from "@welshman/net"
import type {AppSyncOpts} from "@welshman/app"
import {subscribe, load, pull, repository, hasNegentropy} from "@welshman/app"
import {userRoomsByUrl, LEGACY_MESSAGE, GENERAL} from "@app/state"
// Utils
export const pullConservatively = ({relays, filters}: AppSyncOpts) => {
const [smart, dumb] = partition(hasNegentropy, relays)
const promises = [pull({relays: smart, filters})]
// Since pulling from relays without negentropy is expensive, limit how many
// duplicates we repeatedly download
if (dumb.length > 0) {
const events = sortBy(e => -e.created_at, repository.query(filters))
if (events.length > 100) {
filters = filters.map(assoc("since", events[10]!.created_at))
}
promises.push(pull({relays: dumb, filters}))
}
return Promise.all(promises)
}
export const subscribePersistent = (request: SubscribeRequestWithHandlers) => {
let sub: Subscription
let done = false
const start = async () => {
// If the subscription gets closed quickly, don't start flapping
await Promise.all([
sleep(30_000),
new Promise(resolve => {
sub = subscribe(request)
sub.emitter.on(SubscriptionEvent.Complete, resolve)
}),
])
if (!done) {
start()
}
}
start()
return () => {
done = true
sub?.close()
}
}
// Application requests
export const listenForNotifications = () => {
const since = now()
const unsubscribers: Unsubscriber[] = []
for (const [url, rooms] of userRoomsByUrl.get()) {
load({
relays: [url],
filters: [
{kinds: [THREAD], limit: 1},
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
],
})
unsubscribers.push(
subscribePersistent({
relays: [url],
filters: [
{kinds: [THREAD], since},
{kinds: [COMMENT], "#K": [String(THREAD)], since},
{kinds: [MESSAGE], "#h": Array.from(rooms), since},
],
}),
)
}
return () => {
for (const unsubscribe of unsubscribers) {
unsubscribe()
}
}
}
export const listenForChannelMessages = (url: string, room: string) => {
const since = now()
const relays = [url]
const legacyRoom = room === GENERAL ? "general" : room
// Load legacy immediate so our request doesn't get rejected by nip29 relays
load({relays, filters: [{kinds: [LEGACY_MESSAGE], "#~": [legacyRoom]}], delay: 0})
// Load historical state with negentropy if available
pullConservatively({relays, filters: [{kinds: [MESSAGE, DELETE], "#h": [room]}]})
// Listen for new messages
return subscribePersistent({relays, filters: [{kinds: [MESSAGE, DELETE], "#h": [room], since}]})
}
+22 -32
View File
@@ -5,11 +5,9 @@ import {
ctx, ctx,
setContext, setContext,
remove, remove,
assoc,
sortBy, sortBy,
sort, sort,
uniq, uniq,
partition,
nth, nth,
pushToMapKey, pushToMapKey,
nthEq, nthEq,
@@ -17,6 +15,7 @@ import {
parseJson, parseJson,
fromPairs, fromPairs,
memoize, memoize,
addToMapKey,
} from "@welshman/lib" } from "@welshman/lib"
import { import {
getIdFilters, getIdFilters,
@@ -57,15 +56,13 @@ import {
relay, relay,
getSession, getSession,
getSigner, getSigner,
hasNegentropy,
pull,
createSearch, createSearch,
userFollows, userFollows,
ensurePlaintext, ensurePlaintext,
thunks, thunks,
walkThunks, walkThunks,
} from "@welshman/app" } from "@welshman/app"
import type {AppSyncOpts, Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import type {SubscribeRequestWithHandlers} from "@welshman/net" import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store" import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
@@ -209,25 +206,6 @@ export const ensureUnwrapped = async (event: TrustedEvent) => {
return rumor return rumor
} }
export const pullConservatively = ({relays, filters}: AppSyncOpts) => {
const [smart, dumb] = partition(hasNegentropy, relays)
const promises = [pull({relays: smart, filters})]
// Since pulling from relays without negentropy is expensive, limit how many
// duplicates we repeatedly download
if (dumb.length > 0) {
const events = sortBy(e => -e.created_at, repository.query(filters))
if (events.length > 100) {
filters = filters.map(assoc("since", events[100]!.created_at))
}
promises.push(pull({relays: dumb, filters}))
}
return Promise.all(promises)
}
export const trackerStore = makeTrackerStore() export const trackerStore = makeTrackerStore()
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => { export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
@@ -382,10 +360,7 @@ export const {
store: memberships, store: memberships,
getKey: list => list.event.pubkey, getKey: list => list.event.pubkey,
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) => load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
load({ load({...request, filters: [{kinds: [GROUPS], authors: [pubkey]}]}),
...request,
filters: [{kinds: [GROUPS], authors: [pubkey]}],
}),
}) })
// Chats // Chats
@@ -614,11 +589,26 @@ export const userMembership = withGetter(
}), }),
) )
export const userRoomsByUrl = withGetter(
derived(userMembership, $userMembership => {
const $userRoomsByUrl = new Map<string, Set<string>>()
for (const [_, room, url] of getGroupTags(getListTags($userMembership))) {
addToMapKey($userRoomsByUrl, url, room)
}
for (const url of $userRoomsByUrl.keys()) {
addToMapKey($userRoomsByUrl, url, GENERAL)
}
return $userRoomsByUrl
}),
)
export const deriveUserRooms = (url: string) => export const deriveUserRooms = (url: string) =>
derived(userMembership, $userMembership => [ derived(userRoomsByUrl, $userRoomsByUrl =>
GENERAL, sortBy(roomComparator(url), Array.from($userRoomsByUrl.get(url) || [])),
...sortBy(roomComparator(url), getMembershipRoomsByUrl(url, $userMembership)), )
])
export const deriveOtherRooms = (url: string) => export const deriveOtherRooms = (url: string) =>
derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) => derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) =>
+7 -39
View File
@@ -5,7 +5,7 @@
import {get, derived} from "svelte/store" import {get, derived} from "svelte/store"
import {dev} from "$app/environment" import {dev} from "$app/environment"
import {bytesToHex, hexToBytes} from "@noble/hashes/utils" import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
import {identity, uniq, sleep, take, sortBy, ago, now, HOUR, WEEK, Worker} from "@welshman/lib" import {identity, sleep, take, sortBy, ago, now, HOUR, WEEK, Worker} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import { import {
PROFILE, PROFILE,
@@ -15,9 +15,6 @@
RELAYS, RELAYS,
INBOX_RELAYS, INBOX_RELAYS,
WRAP, WRAP,
MESSAGE,
COMMENT,
THREAD,
getPubkeyTagValues, getPubkeyTagValues,
getListTags, getListTags,
} from "@welshman/util" } from "@welshman/util"
@@ -39,7 +36,6 @@
dropSession, dropSession,
getRelayUrls, getRelayUrls,
userInboxRelaySelections, userInboxRelaySelections,
load,
} from "@welshman/app" } from "@welshman/app"
import * as lib from "@welshman/lib" import * as lib from "@welshman/lib"
import * as util from "@welshman/util" import * as util from "@welshman/util"
@@ -51,17 +47,11 @@
import {setupTracking} from "@app/tracking" import {setupTracking} from "@app/tracking"
import {setupAnalytics} from "@app/analytics" import {setupAnalytics} from "@app/analytics"
import {theme} from "@app/theme" import {theme} from "@app/theme"
import { import {INDEXER_RELAYS, userMembership, ensureUnwrapped, canDecrypt} from "@app/state"
INDEXER_RELAYS, import {loadUserData} from "@app/commands"
getMembershipUrls, import {subscribePersistent, listenForNotifications} from "@app/requests"
getMembershipRooms,
userMembership,
ensureUnwrapped,
canDecrypt,
GENERAL,
} from "@app/state"
import {loadUserData, subscribePersistent} from "@app/commands"
import * as commands from "@app/commands" import * as commands from "@app/commands"
import * as requests from "@app/requests"
import {checked} from "@app/notifications" import {checked} from "@app/notifications"
import * as notifications from "@app/notifications" import * as notifications from "@app/notifications"
import * as state from "@app/state" import * as state from "@app/state"
@@ -86,6 +76,7 @@
...app, ...app,
...state, ...state,
...commands, ...commands,
...requests,
...notifications, ...notifications,
}) })
@@ -199,30 +190,7 @@
userMembership.subscribe($membership => { userMembership.subscribe($membership => {
unsubSpaces?.() unsubSpaces?.()
unsubSpaces = listenForNotifications()
const since = ago(30)
const rooms = uniq(getMembershipRooms($membership).map(m => m.room)).concat(GENERAL)
const relays = uniq(getMembershipUrls($membership))
// Get one event for each of our notification categories
load({
relays,
filters: [
{kinds: [THREAD], limit: 1},
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
],
})
// Listen for new notifications/memberships
unsubSpaces = subscribePersistent({
relays,
filters: [
{kinds: [THREAD], since},
{kinds: [COMMENT], "#K": [String(THREAD)], since},
{kinds: [MESSAGE], "#h": rooms, since},
],
})
}) })
// Listen for chats, populate chat-based notifications // Listen for chats, populate chat-based notifications
+2 -1
View File
@@ -12,7 +12,8 @@
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte" import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
import ChatStart from "@app/components/ChatStart.svelte" import ChatStart from "@app/components/ChatStart.svelte"
import ChatItem from "@app/components/ChatItem.svelte" import ChatItem from "@app/components/ChatItem.svelte"
import {chatSearch, pullConservatively} from "@app/state" import {chatSearch} from "@app/state"
import {pullConservatively} from "@app/requests"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
const startChat = () => pushModal(ChatStart) const startChat = () => pushModal(ChatStart)
+8 -30
View File
@@ -5,11 +5,11 @@
import {derived} from "svelte/store" import {derived} from "svelte/store"
import type {Editor} from "svelte-tiptap" import type {Editor} from "svelte-tiptap"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sleep, now, ctx} from "@welshman/lib" import {sleep, ctx} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {throttled} from "@welshman/store" import {throttled} from "@welshman/store"
import {createEvent, DELETE, MESSAGE} from "@welshman/util" import {createEvent, MESSAGE} from "@welshman/util"
import {formatTimestampAsDate, load, publishThunk, deriveRelay} from "@welshman/app" import {formatTimestampAsDate, publishThunk, deriveRelay} from "@welshman/app"
import {slide} from "@lib/transition" import {slide} from "@lib/transition"
import {createScroller, type Scroller} from "@lib/html" import {createScroller, type Scroller} from "@lib/html"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -22,7 +22,6 @@
import ChannelMessage from "@app/components/ChannelMessage.svelte" import ChannelMessage from "@app/components/ChannelMessage.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte" import ChannelCompose from "@app/components/ChannelCompose.svelte"
import { import {
pullConservatively,
userSettingValues, userSettingValues,
userMembership, userMembership,
decodeRelay, decodeRelay,
@@ -34,13 +33,8 @@
displayChannel, displayChannel,
} from "@app/state" } from "@app/state"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
import { import {nip29, addRoomMembership, removeRoomMembership, getThunkError} from "@app/commands"
nip29, import {listenForChannelMessages} from "@app/requests"
addRoomMembership,
removeRoomMembership,
getThunkError,
subscribePersistent,
} from "@app/commands"
import {PROTECTED} from "@app/state" import {PROTECTED} from "@app/state"
import {popKey} from "@app/implicit" import {popKey} from "@app/implicit"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@@ -96,7 +90,7 @@
delay: $userSettingValues.send_delay, delay: $userSettingValues.send_delay,
}) })
let limit = 15 let limit = 30
let loading = true let loading = true
let unsub: () => void let unsub: () => void
let element: HTMLElement let element: HTMLElement
@@ -135,32 +129,16 @@
// Sveltekiiit // Sveltekiiit
await sleep(100) await sleep(100)
if (!nip29.isSupported($relay)) {
load({
delay: 0,
relays: [url],
filters: [{kinds: [LEGACY_MESSAGE], "#~": [legacyRoom]}],
})
}
pullConservatively({
relays: [url],
filters: [{kinds: [MESSAGE, DELETE], "#h": [room]}],
})
scroller = createScroller({ scroller = createScroller({
element, element,
delay: 300, delay: 300,
threshold: 3000, threshold: 3000,
onScroll: () => { onScroll: () => {
limit += 15 limit += 30
}, },
}) })
unsub = subscribePersistent({ unsub = listenForChannelMessages(url, room)
relays: [url],
filters: [{kinds: [MESSAGE], "#h": [room], since: now()}],
})
}) })
onDestroy(() => { onDestroy(() => {
@@ -14,7 +14,8 @@
import EventItem from "@app/components/EventItem.svelte" import EventItem from "@app/components/EventItem.svelte"
import EventCreate from "@app/components/EventCreate.svelte" import EventCreate from "@app/components/EventCreate.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {deriveEventsForUrl, pullConservatively, decodeRelay} from "@app/state" import {deriveEventsForUrl, decodeRelay} from "@app/state"
import {pullConservatively} from "@app/requests"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
@@ -15,7 +15,7 @@
import ThreadActions from "@app/components/ThreadActions.svelte" import ThreadActions from "@app/components/ThreadActions.svelte"
import ThreadReply from "@app/components/ThreadReply.svelte" import ThreadReply from "@app/components/ThreadReply.svelte"
import {deriveEvent, decodeRelay} from "@app/state" import {deriveEvent, decodeRelay} from "@app/state"
import {subscribePersistent} from "@app/commands" import {subscribePersistent} from "@app/requests"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
const {relay, id} = $page.params const {relay, id} = $page.params