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,
} from "@welshman/util"
import type {TrustedEvent, EventTemplate, List} from "@welshman/util"
import type {SubscribeRequestWithHandlers, Subscription} from "@welshman/net"
import {PublishStatus, AuthStatus, SocketStatus, SubscriptionEvent} from "@welshman/net"
import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
import {
pubkey,
@@ -51,7 +51,6 @@ import {
nip44EncryptToSelf,
loadRelay,
addSession,
subscribe,
clearStorage,
dropSession,
} 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(" ")),
]
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) => {
const result = await thunk.result
const [{status, message}] = Object.values(result) as any
+2 -2
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {displayRelayUrl, GROUP_META} from "@welshman/util"
import {load} from "@welshman/app"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -24,6 +23,7 @@
deriveOtherRooms,
} from "@app/state"
import {deriveNotification, THREAD_FILTERS} from "@app/notifications"
import {pullConservatively} from "@app/requests"
import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes"
@@ -65,7 +65,7 @@
onMount(async () => {
replaceState = Boolean(element.closest(".drawer"))
load({relays: [url], filters: [{kinds: [GROUP_META]}]})
pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]})
})
</script>
+13 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {onMount} from "svelte"
import {groupBy, uniqBy} from "@welshman/lib"
import {REACTION} from "@welshman/util"
import {groupBy, uniqBy, batch} from "@welshman/lib"
import {REACTION, DELETE} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
import {displayList} from "@lib/util"
@@ -23,7 +24,16 @@
)
onMount(() => {
load({relays, filters})
load({
relays,
filters,
onEvent: batch(300, (events: TrustedEvent[]) => {
load({
relays,
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
})
}),
})
})
</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,
setContext,
remove,
assoc,
sortBy,
sort,
uniq,
partition,
nth,
pushToMapKey,
nthEq,
@@ -17,6 +15,7 @@ import {
parseJson,
fromPairs,
memoize,
addToMapKey,
} from "@welshman/lib"
import {
getIdFilters,
@@ -57,15 +56,13 @@ import {
relay,
getSession,
getSigner,
hasNegentropy,
pull,
createSearch,
userFollows,
ensurePlaintext,
thunks,
walkThunks,
} from "@welshman/app"
import type {AppSyncOpts, Thunk} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
@@ -209,25 +206,6 @@ export const ensureUnwrapped = async (event: TrustedEvent) => {
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 deriveEvent = (idOrAddress: string, hints: string[] = []) => {
@@ -382,10 +360,7 @@ export const {
store: memberships,
getKey: list => list.event.pubkey,
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
load({
...request,
filters: [{kinds: [GROUPS], authors: [pubkey]}],
}),
load({...request, filters: [{kinds: [GROUPS], authors: [pubkey]}]}),
})
// 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) =>
derived(userMembership, $userMembership => [
GENERAL,
...sortBy(roomComparator(url), getMembershipRoomsByUrl(url, $userMembership)),
])
derived(userRoomsByUrl, $userRoomsByUrl =>
sortBy(roomComparator(url), Array.from($userRoomsByUrl.get(url) || [])),
)
export const deriveOtherRooms = (url: string) =>
derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) =>