mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 16:31:04 +00:00
Remove network, add load
This commit is contained in:
parent
ffc53f9210
commit
dc87380904
@ -24,8 +24,9 @@
|
||||
hasNewNotfications,
|
||||
getUserRelayUrls,
|
||||
searchableRelays,
|
||||
Subscription,
|
||||
} from "src/engine2"
|
||||
import engine, {Keys, Directory, Network} from "src/app/engine"
|
||||
import engine, {Keys, Directory} from "src/app/engine"
|
||||
|
||||
const {keyState, canUseGiftWrap} = Keys
|
||||
const logoUrl = import.meta.env.VITE_LOGO_URL || "/images/logo.png"
|
||||
@ -71,15 +72,15 @@
|
||||
// If we have a query, search using nostr.band. If not, ask for random profiles.
|
||||
// This allows us to populate results even if search isn't supported by forced urls
|
||||
if (term.length > 2) {
|
||||
Network.subscribe({
|
||||
new Subscription({
|
||||
relays: $searchableRelays,
|
||||
filter: [{kinds: [0], search, limit: 10}],
|
||||
filters: [{kinds: [0], search, limit: 10}],
|
||||
timeout: 3000,
|
||||
})
|
||||
} else if (Directory.profiles.get().length < 50) {
|
||||
Network.subscribe({
|
||||
new Subscription({
|
||||
relays: getUserRelayUrls("read"),
|
||||
filter: [{kinds: [0], limit: 50}],
|
||||
filters: [{kinds: [0], limit: 50}],
|
||||
timeout: 3000,
|
||||
})
|
||||
}
|
||||
|
@ -47,8 +47,4 @@ export const Crypt = engine.Crypt
|
||||
export const Directory = engine.Directory
|
||||
export const Events = engine.Events
|
||||
export const Keys = engine.Keys
|
||||
export const Network = engine.Network
|
||||
export const Nip04 = engine.Nip04
|
||||
export const Nip05 = engine.Nip05
|
||||
export const Nip28 = engine.Nip28
|
||||
export const Nip44 = engine.Nip44
|
||||
|
@ -6,8 +6,15 @@
|
||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||
import ContentEditable from "src/partials/ContentEditable.svelte"
|
||||
import Suggestions from "src/partials/Suggestions.svelte"
|
||||
import {isFollowing, searchableRelays, getPubkeyHints} from "src/engine2"
|
||||
import {Network, Directory} from "src/app/engine"
|
||||
import {
|
||||
people,
|
||||
displayPerson,
|
||||
Subscription,
|
||||
isFollowing,
|
||||
searchableRelays,
|
||||
getPubkeyHints,
|
||||
getPeopleSearch,
|
||||
} from "src/engine2"
|
||||
|
||||
export let onSubmit
|
||||
|
||||
@ -28,16 +35,17 @@
|
||||
},
|
||||
}
|
||||
|
||||
const {searchProfiles} = Directory
|
||||
const searchPeople = people.derived(getPeopleSearch)
|
||||
|
||||
const loadProfiles = debounce(500, search => {
|
||||
const loadPeople = debounce(500, search => {
|
||||
if (search.length > 2 && search.startsWith("@")) {
|
||||
Network.subscribe({
|
||||
const sub = new Subscription({
|
||||
timeout: 3000,
|
||||
relays: $searchableRelays,
|
||||
filter: [{kinds: [0], search, limit: 10}],
|
||||
onEvent: debounce(100, () => applySearch(getInfo().word)),
|
||||
filters: [{kinds: [0], search, limit: 10}],
|
||||
})
|
||||
|
||||
sub.on("event", () => applySearch(getInfo().word))
|
||||
}
|
||||
})
|
||||
|
||||
@ -46,7 +54,7 @@
|
||||
if (word.length > 1 && word.startsWith("@")) {
|
||||
const [followed, notFollowed] = partition(
|
||||
p => isFollowing(p.pubkey).get(),
|
||||
$searchProfiles(word.slice(1))
|
||||
$searchPeople(word.slice(1))
|
||||
)
|
||||
|
||||
results = followed.concat(notFollowed)
|
||||
@ -64,7 +72,7 @@
|
||||
return {selection, node, offset, word}
|
||||
}
|
||||
|
||||
const autocomplete = ({profile = null, force = false} = {}) => {
|
||||
const autocomplete = ({person = null, force = false} = {}) => {
|
||||
let completed = false
|
||||
|
||||
const {selection, node, offset, word} = getInfo()
|
||||
@ -98,8 +106,8 @@
|
||||
}
|
||||
|
||||
// Mentions
|
||||
if ((force || word.length > 1) && word.startsWith("@") && profile) {
|
||||
annotate("@", Directory.displayProfile(profile).trim(), pubkeyEncoder.encode(profile.pubkey))
|
||||
if ((force || word.length > 1) && word.startsWith("@") && person) {
|
||||
annotate("@", displayPerson(person).trim(), pubkeyEncoder.encode(person.pubkey))
|
||||
}
|
||||
|
||||
// Topics
|
||||
@ -130,7 +138,7 @@
|
||||
|
||||
// Enter adds a newline, so do it on key down
|
||||
if (["Enter"].includes(e.code)) {
|
||||
autocomplete({profile: suggestions.get()})
|
||||
autocomplete({person: suggestions.get()})
|
||||
}
|
||||
|
||||
// Only autocomplete topics on space
|
||||
@ -145,11 +153,11 @@
|
||||
const {word} = getInfo()
|
||||
|
||||
// Populate search data
|
||||
loadProfiles(word)
|
||||
loadPeople(word)
|
||||
applySearch(word)
|
||||
|
||||
if (["Tab"].includes(e.code)) {
|
||||
autocomplete({profile: suggestions.get()})
|
||||
autocomplete({person: suggestions.get()})
|
||||
}
|
||||
|
||||
if (["Escape", "Space"].includes(e.code)) {
|
||||
@ -167,7 +175,7 @@
|
||||
dispatch("keyup", e)
|
||||
}
|
||||
|
||||
export const mention = profile => {
|
||||
export const mention = person => {
|
||||
const input = contenteditable.getInput()
|
||||
const selection = window.getSelection()
|
||||
const textNode = document.createTextNode("@")
|
||||
@ -179,7 +187,7 @@
|
||||
selection.getRangeAt(0).insertNode(spaceNode)
|
||||
selection.collapse(input, 1)
|
||||
|
||||
autocomplete({profile, force: true})
|
||||
autocomplete({person, force: true})
|
||||
}
|
||||
|
||||
const createNewLines = (n = 1) => {
|
||||
@ -258,7 +266,7 @@
|
||||
<slot name="addon" />
|
||||
</div>
|
||||
|
||||
<Suggestions bind:this={suggestions} select={profile => autocomplete({profile})}>
|
||||
<Suggestions bind:this={suggestions} select={person => autocomplete({person})}>
|
||||
<div slot="item" let:item>
|
||||
<PersonBadge inert pubkey={item.pubkey} />
|
||||
</div>
|
||||
|
@ -7,27 +7,27 @@
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import ImageCircle from "src/partials/ImageCircle.svelte"
|
||||
import {Nip28} from "src/app/engine"
|
||||
import {channels} from "src/engine2"
|
||||
|
||||
export let note
|
||||
|
||||
const {name, picture, about} = tryJson(() => JSON.parse(note.content))
|
||||
const channel = Nip28.channels
|
||||
const channel = channels
|
||||
.key(note.id)
|
||||
.derived(defaultTo({id: note.id, name, picture, about}))
|
||||
.derived(defaultTo({id: note.id, meta: {name, picture, about}}))
|
||||
const noteId = nip19.noteEncode(note.kind === 40 ? note.id : Tags.from(note).getMeta("e"))
|
||||
</script>
|
||||
|
||||
<Card interactive invertColors on:click={() => navigate(`/chat/${noteId}`)}>
|
||||
<Content>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if $channel.picture}
|
||||
<ImageCircle size={10} src={$channel.picture} />
|
||||
{#if $channel.meta?.picture}
|
||||
<ImageCircle size={10} src={$channel.meta?.picture} />
|
||||
{/if}
|
||||
<h3 class="staatliches text-2xl">{$channel.name}</h3>
|
||||
<h3 class="staatliches text-2xl">{$channel.meta?.name}</h3>
|
||||
</div>
|
||||
{#if $channel.about}
|
||||
<p>{$channel.about}</p>
|
||||
{#if $channel.meta?.about}
|
||||
<p>{$channel.meta?.about}</p>
|
||||
{/if}
|
||||
</Content>
|
||||
</Card>
|
||||
|
@ -9,8 +9,8 @@
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
||||
import {getSetting, isEventMuted, getEventHints, mergeHints} from "src/engine2"
|
||||
import {Directory, Network} from "src/app/engine"
|
||||
import {getSetting, Subscription, isEventMuted, getEventHints, mergeHints} from "src/engine2"
|
||||
import {Directory} from "src/app/engine"
|
||||
|
||||
export let note
|
||||
export let value
|
||||
@ -29,23 +29,24 @@
|
||||
getEventHints(getSetting("relay_limit"), note),
|
||||
])
|
||||
|
||||
const filter = (
|
||||
const filters = [
|
||||
id
|
||||
? {ids: [id]}
|
||||
: filterVals(xs => xs.length > 0, {
|
||||
"#d": [identifier],
|
||||
kinds: [kind],
|
||||
authors: [pubkey],
|
||||
})
|
||||
) as Filter
|
||||
}),
|
||||
] as Filter[]
|
||||
|
||||
const onEvent = event => {
|
||||
const sub = new Subscription({timeout: 30000, relays, filters})
|
||||
|
||||
sub.on("event", event => {
|
||||
loading = false
|
||||
muted = isEventMuted(event).get()
|
||||
quote = event
|
||||
}
|
||||
|
||||
const sub = Network.subscribe({timeout: 30000, relays, filter, onEvent})
|
||||
sub.close()
|
||||
})
|
||||
|
||||
const openQuote = e => {
|
||||
const noteId = id || quote?.id
|
||||
|
@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {nip19} from "nostr-tools"
|
||||
import {Nip05} from "src/app/engine"
|
||||
import {people, displayHandle} from "src/engine2"
|
||||
|
||||
export let pubkey
|
||||
|
||||
const npub = nip19.npubEncode(pubkey)
|
||||
const handle = Nip05.handles.key(pubkey)
|
||||
const person = people.key(pubkey)
|
||||
</script>
|
||||
|
||||
<div class="overflow-hidden overflow-ellipsis opacity-75">
|
||||
{$handle ? Nip05.displayHandle($handle) : npub}
|
||||
{$person?.handle ? displayHandle($person.handle) : npub}
|
||||
</div>
|
||||
|
@ -5,9 +5,8 @@
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import PersonSummary from "src/app/shared/PersonSummary.svelte"
|
||||
import {loadPubkeys} from "src/engine2"
|
||||
import {getSetting, getPubkeyHints, follows} from "src/engine2"
|
||||
import {Network} from "src/app/engine"
|
||||
import type {Event} from "src/engine2"
|
||||
import {Subscription, getSetting, loadPubkeys, getPubkeyHints, follows} from "src/engine2"
|
||||
|
||||
export let type
|
||||
export let pubkey
|
||||
@ -18,17 +17,21 @@
|
||||
if (type === "follows") {
|
||||
pubkeys = $follows
|
||||
} else {
|
||||
const sub = Network.subscribe({
|
||||
const sub = new Subscription({
|
||||
relays: getPubkeyHints(getSetting("relay_limit"), pubkey, "read"),
|
||||
filter: {kinds: [3], "#p": [pubkey]},
|
||||
onEvent: batch(500, events => {
|
||||
filters: [{kinds: [3], "#p": [pubkey]}],
|
||||
})
|
||||
|
||||
sub.on(
|
||||
"event",
|
||||
batch(500, (events: Event[]) => {
|
||||
const newPubkeys = pluck("pubkey", events)
|
||||
|
||||
loadPubkeys(newPubkeys)
|
||||
|
||||
pubkeys = uniq(pubkeys.concat(newPubkeys))
|
||||
}),
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
return sub.close
|
||||
}
|
||||
|
@ -5,8 +5,9 @@
|
||||
import {tweened} from "svelte/motion"
|
||||
import {numberFmt} from "src/util/misc"
|
||||
import {modal} from "src/partials/state"
|
||||
import {people, getPubkeyHints} from "src/engine2"
|
||||
import {Keys, Network} from "src/app/engine"
|
||||
import type {Event} from "src/engine2"
|
||||
import {people, count, Subscription, getPubkeyHints} from "src/engine2"
|
||||
import {Keys} from "src/app/engine"
|
||||
|
||||
export let pubkey
|
||||
|
||||
@ -25,26 +26,30 @@
|
||||
canLoadFollowers = false
|
||||
|
||||
// Get our followers count
|
||||
const count = await Network.count({kinds: [3], "#p": [pubkey]})
|
||||
const total = await count([{kinds: [3], "#p": [pubkey]}])
|
||||
|
||||
if (count) {
|
||||
followersCount.set(count)
|
||||
if (total) {
|
||||
followersCount.set(total)
|
||||
} else {
|
||||
const followers = new Set()
|
||||
|
||||
sub = Network.subscribe({
|
||||
sub = new Subscription({
|
||||
timeout: 30_000,
|
||||
shouldProcess: false,
|
||||
ephemeral: true,
|
||||
relays: getPubkeyHints(3, Keys.pubkey.get(), "read"),
|
||||
filter: [{kinds: [3], "#p": [pubkey]}],
|
||||
onEvent: batch(300, events => {
|
||||
filters: [{kinds: [3], "#p": [pubkey]}],
|
||||
})
|
||||
|
||||
sub.on(
|
||||
"event",
|
||||
batch(300, (events: Event[]) => {
|
||||
for (const e of events) {
|
||||
followers.add(e.pubkey)
|
||||
}
|
||||
|
||||
followersCount.set(followers.size)
|
||||
}),
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {poll} from "hurdak"
|
||||
import {onMount} from "svelte"
|
||||
import {Network} from "src/app/engine"
|
||||
import {pool} from "src/engine2"
|
||||
|
||||
export let relay
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
onMount(() => {
|
||||
return poll(3000, () => {
|
||||
const cxn = Network.pool.get(relay.url, {autoConnect: false})
|
||||
const cxn = pool.get(relay.url, {autoConnect: false})
|
||||
|
||||
if (cxn) {
|
||||
description = cxn.meta.getDescription()
|
||||
|
@ -4,15 +4,25 @@ import Bugsnag from "@bugsnag/js"
|
||||
import {nip19} from "nostr-tools"
|
||||
import {navigate} from "svelte-routing"
|
||||
import {writable} from "svelte/store"
|
||||
import {whereEq, omit, filter, pluck, sortBy, slice} from "ramda"
|
||||
import {omit, path, filter, pluck, sortBy, slice} from "ramda"
|
||||
import {hash, union, sleep, doPipe, shuffle} from "hurdak"
|
||||
import {warn} from "src/util/logger"
|
||||
import {now} from "src/util/misc"
|
||||
import {userKinds, noteKinds} from "src/util/nostr"
|
||||
import {modal, toast} from "src/partials/state"
|
||||
import type {Event} from "src/engine2"
|
||||
import {loadPubkeys, follows, network, getUserRelayUrls, getSetting, dufflepud} from "src/engine2"
|
||||
import {Events, Nip28, Env, Network, Keys} from "src/app/engine"
|
||||
import {
|
||||
pool,
|
||||
loadPubkeys,
|
||||
channels,
|
||||
follows,
|
||||
network,
|
||||
Subscription,
|
||||
getUserRelayUrls,
|
||||
getSetting,
|
||||
dufflepud,
|
||||
} from "src/engine2"
|
||||
import {Events, Env, Keys} from "src/app/engine"
|
||||
|
||||
// Routing
|
||||
|
||||
@ -85,7 +95,7 @@ setInterval(() => {
|
||||
|
||||
// Prune connections we haven't used in a while, clear errors periodically,
|
||||
// and keep track of slow connections
|
||||
for (const [url, connection] of Network.pool.data.entries()) {
|
||||
for (const [url, connection] of pool.data.entries()) {
|
||||
if (connection.meta.last_activity < now() - 60) {
|
||||
connection.disconnect()
|
||||
} else if (connection.lastError < Date.now() - 10_000) {
|
||||
@ -107,7 +117,7 @@ let timeout
|
||||
export const listenForNotifications = async () => {
|
||||
const pubkey = Keys.pubkey.get()
|
||||
|
||||
const channelIds = pluck("id", Nip28.channels.get().filter(whereEq({joined: true})))
|
||||
const channelIds = pluck("id", channels.get().filter(path(["nip28", "joined"])))
|
||||
|
||||
const eventIds: string[] = doPipe(Events.cache.get(), [
|
||||
filter((e: Event) => noteKinds.includes(e.kind)),
|
||||
@ -119,9 +129,9 @@ export const listenForNotifications = async () => {
|
||||
// Only grab one event from each category/relay so we have enough to show
|
||||
// the notification badges, but load the details lazily
|
||||
listener?.close()
|
||||
listener = Network.subscribe({
|
||||
listener = new Subscription({
|
||||
relays: getUserRelayUrls("read"),
|
||||
filter: [
|
||||
filters: [
|
||||
// Messages
|
||||
{kinds: [4], authors: [pubkey], limit: 1},
|
||||
{kinds: [4], "#p": [pubkey], limit: 1},
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {defaultTo, filter, whereEq} from "ramda"
|
||||
import {assoc} from "ramda"
|
||||
import {formatTimestamp} from "src/util/misc"
|
||||
import {toHex} from "src/util/nostr"
|
||||
import {modal} from "src/partials/state"
|
||||
@ -10,6 +10,7 @@
|
||||
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
|
||||
import NoteContent from "src/app/shared/NoteContent.svelte"
|
||||
import {
|
||||
channels,
|
||||
imgproxy,
|
||||
publishNip28Message,
|
||||
joinNip28Channel,
|
||||
@ -17,23 +18,20 @@
|
||||
loadNip28Messages,
|
||||
publishNip28ChannelChecked,
|
||||
} from "src/engine2"
|
||||
import {Nip28, Keys} from "src/app/engine"
|
||||
import {Keys} from "src/app/engine"
|
||||
|
||||
export let entity
|
||||
|
||||
const id = toHex(entity)
|
||||
const channel = Nip28.channels.key(id).derived(defaultTo({id}))
|
||||
const messages = Nip28.messages.derived(filter(whereEq({channel: id})))
|
||||
const channel = channels.key(id)
|
||||
|
||||
publishNip28ChannelChecked(id)
|
||||
|
||||
const join = () => joinNip28Channel($channel.id)
|
||||
const join = () => joinNip28Channel(id)
|
||||
|
||||
const leave = () => leaveNip28Channel($channel.id)
|
||||
const leave = () => leaveNip28Channel(id)
|
||||
|
||||
const edit = () => {
|
||||
modal.push({type: "chat/edit", channel: $channel})
|
||||
}
|
||||
const edit = () => modal.push({type: "chat/edit", channel: $channel})
|
||||
|
||||
const sendMessage = content => publishNip28Message(id, content).result
|
||||
|
||||
@ -46,35 +44,36 @@
|
||||
onDestroy(() => {
|
||||
publishNip28ChannelChecked(id)
|
||||
|
||||
if (!$channel.joined) {
|
||||
Nip28.messages.reject(m => m.channel === id)
|
||||
// Save on memory by deleting messages we don't care about
|
||||
if (!$channel?.nip28?.joined) {
|
||||
channel.update(assoc("messages", []))
|
||||
}
|
||||
})
|
||||
|
||||
$: picture = imgproxy($channel.picture, {w: 96, h: 96})
|
||||
$: picture = imgproxy($channel?.meta?.picture, {w: 96, h: 96})
|
||||
|
||||
document.title = $channel.name || "Coracle Chat"
|
||||
document.title = $channel?.meta?.name || "Coracle Chat"
|
||||
</script>
|
||||
|
||||
<Channel {messages} {sendMessage}>
|
||||
<Channel messages={$channel?.messages || []} {sendMessage}>
|
||||
<div slot="header" class="flex h-16 items-start gap-4 overflow-hidden p-2">
|
||||
<div class="flex items-center gap-4 pt-1">
|
||||
<Anchor type="unstyled" class="fa fa-arrow-left cursor-pointer text-2xl" href="/chat" />
|
||||
<ImageCircle size={10} src={picture} />
|
||||
</div>
|
||||
<div class="flex h-12 flex-grow flex-col overflow-hidden pt-px">
|
||||
<div class="font-bold">{$channel.name || ""}</div>
|
||||
<div>{$channel.about || ""}</div>
|
||||
<div class="font-bold">{$channel?.meta?.name || ""}</div>
|
||||
<div>{$channel?.meta?.about || ""}</div>
|
||||
</div>
|
||||
<div class="flex h-12 flex-col pt-px">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div class="flex gap-2">
|
||||
{#if $channel.pubkey === Keys.pubkey.get()}
|
||||
{#if $channel?.nip28?.owner === Keys.pubkey.get()}
|
||||
<button class="cursor-pointer text-sm" on:click={edit}>
|
||||
<i class="fa-solid fa-edit" /> Edit
|
||||
</button>
|
||||
{/if}
|
||||
{#if $channel.joined}
|
||||
{#if $channel?.nip28?.joined}
|
||||
<Anchor theme="button" killEvent class="flex items-center gap-2" on:click={leave}>
|
||||
<i class="fa fa-right-from-bracket" />
|
||||
<span>Leave</span>
|
||||
|
@ -11,8 +11,8 @@
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import NoteById from "src/app/shared/NoteById.svelte"
|
||||
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
|
||||
import {getUserRelayUrls, follows} from "src/engine2"
|
||||
import engine, {Network, Keys} from "src/app/engine"
|
||||
import {getUserRelayUrls, follows, Subscription} from "src/engine2"
|
||||
import engine, {Keys} from "src/app/engine"
|
||||
|
||||
type LabelGroup = {
|
||||
label: string
|
||||
@ -63,13 +63,15 @@
|
||||
const showGroup = ({label, ids, hints}) => modal.push({type: "label/detail", label, ids, hints})
|
||||
|
||||
onMount(() => {
|
||||
const sub = Network.subscribe({
|
||||
const sub = new Subscription({
|
||||
relays: getUserRelayUrls("read"),
|
||||
filter: {
|
||||
kinds: [1985],
|
||||
"#L": ["#t", "ugc"],
|
||||
authors: $follows.concat($pubkey),
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
kinds: [1985],
|
||||
"#L": ["#t", "ugc"],
|
||||
authors: $follows.concat($pubkey),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return () => sub.close()
|
||||
|
@ -12,8 +12,8 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Modal from "src/partials/Modal.svelte"
|
||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||
import {relays, loadPubkeys, getUserRelayUrls} from "src/engine2"
|
||||
import {Env, Keys, Network} from "src/app/engine"
|
||||
import {relays, pool, loadPubkeys, getUserRelayUrls} from "src/engine2"
|
||||
import {Env, Keys} from "src/app/engine"
|
||||
import {loadAppData} from "src/app/state"
|
||||
|
||||
const pubkey = Keys.pubkey.get()
|
||||
@ -75,7 +75,7 @@
|
||||
|
||||
navigate("/notes")
|
||||
} else {
|
||||
Network.pool.remove(relay.url)
|
||||
pool.remove(relay.url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -16,8 +16,8 @@
|
||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||
import NoteContent from "src/app/shared/NoteContent.svelte"
|
||||
import RelaySearch from "src/app/shared/RelaySearch.svelte"
|
||||
import {publishNote, displayRelay, getUserRelayUrls, mention} from "src/engine2"
|
||||
import {Directory, Network, Keys} from "src/app/engine"
|
||||
import {Publisher, publishNote, displayRelay, getUserRelayUrls, mention} from "src/engine2"
|
||||
import {Directory, Keys} from "src/app/engine"
|
||||
import {modal} from "src/partials/state"
|
||||
import {toastProgress} from "src/app/state"
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
tags.push(mention(quote.pubkey))
|
||||
|
||||
// Re-broadcast the note we're quoting
|
||||
Network.publish({relays: $relays, event: quote})
|
||||
Publisher.publish({relays: $relays, event: quote})
|
||||
}
|
||||
|
||||
publishNote(content, tags, $relays).on("progress", toastProgress)
|
||||
|
@ -4,12 +4,11 @@
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import CopyValue from "src/partials/CopyValue.svelte"
|
||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||
import {getPubkeyHints} from "src/engine2"
|
||||
import {Nip05} from "src/app/engine"
|
||||
import {getPubkeyHints, displayHandle, people} from "src/engine2"
|
||||
|
||||
export let pubkey
|
||||
|
||||
const handle = Nip05.getHandle(pubkey)
|
||||
const person = people.key(pubkey)
|
||||
const relays = getPubkeyHints(3, pubkey, "write")
|
||||
const nprofile = nip19.nprofileEncode({pubkey, relays})
|
||||
</script>
|
||||
@ -19,12 +18,12 @@
|
||||
<h1 class="staatliches text-2xl">Details</h1>
|
||||
<CopyValue label="Link" value={nprofile} />
|
||||
<CopyValue label="Public Key" encode={nip19.npubEncode} value={pubkey} />
|
||||
{#if handle}
|
||||
{@const display = Nip05.displayHandle(handle)}
|
||||
{#if $person?.handle}
|
||||
{@const display = displayHandle($person.handle)}
|
||||
<CopyValue label="Nostr Address" value={display} />
|
||||
<Content size="inherit" gap="gap-2">
|
||||
<strong>Nostr Address Relays</strong>
|
||||
{#each handle.profile.relays || [] as url}
|
||||
{#each $person.handle.profile.relays || [] as url}
|
||||
<RelayCard relay={{url}} />
|
||||
{:else}
|
||||
<p class="flex gap-2 items-center">
|
||||
|
@ -4,12 +4,7 @@ import {Crypt} from "./components/Crypt"
|
||||
import {Directory} from "./components/Directory"
|
||||
import {Events} from "./components/Events"
|
||||
import {Keys} from "./components/Keys"
|
||||
import {Network} from "./components/Network"
|
||||
import {Nip04} from "./components/Nip04"
|
||||
import {Nip05} from "./components/Nip05"
|
||||
import {Nip28} from "./components/Nip28"
|
||||
import {Nip44} from "./components/Nip44"
|
||||
import {Settings} from "./components/Settings"
|
||||
|
||||
export class Engine {
|
||||
Env: Env
|
||||
@ -18,12 +13,7 @@ export class Engine {
|
||||
Directory = new Directory()
|
||||
Events = new Events()
|
||||
Keys = new Keys()
|
||||
Network = new Network()
|
||||
Nip04 = new Nip04()
|
||||
Nip05 = new Nip05()
|
||||
Nip28 = new Nip28()
|
||||
Nip44 = new Nip44()
|
||||
Settings = new Settings()
|
||||
|
||||
constructor(Env: Env) {
|
||||
this.Env = Env
|
||||
|
@ -1,215 +0,0 @@
|
||||
import {omit} from "ramda"
|
||||
import {Pool, Plex, Relays, Executor} from "paravel"
|
||||
import {noop, ensurePlural, union, difference} from "hurdak"
|
||||
import {warn, error, info} from "src/util/logger"
|
||||
import {normalizeRelayUrl} from "src/util/nostr"
|
||||
import type {Event, Filter} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import {Subscription} from "src/engine/util/Subscription"
|
||||
|
||||
export type Progress = {
|
||||
event: Event
|
||||
succeeded: Set<string>
|
||||
failed: Set<string>
|
||||
timeouts: Set<string>
|
||||
completed: Set<string>
|
||||
pending: Set<string>
|
||||
}
|
||||
|
||||
export type PublishOpts = {
|
||||
event: Event
|
||||
relays: string[]
|
||||
onProgress?: (p: Progress) => void
|
||||
timeout?: number
|
||||
verb?: string
|
||||
}
|
||||
|
||||
export type SubscribeOpts = {
|
||||
relays: string[]
|
||||
filter: Filter[] | Filter
|
||||
onEvent?: (event: Event) => void
|
||||
onEose?: (url: string) => void
|
||||
onClose?: () => void
|
||||
timeout?: number
|
||||
shouldProcess?: boolean
|
||||
}
|
||||
|
||||
export class Network {
|
||||
engine: Engine
|
||||
pool = new Pool()
|
||||
authHandler: (url: string, challenge: string) => void
|
||||
|
||||
relayIsLowQuality = (url: string) => this.pool.get(url, {autoConnect: false})?.meta?.quality < 0.6
|
||||
|
||||
getUrls = (relays: string[]) => {
|
||||
if (this.engine.Env.FORCE_RELAYS?.length > 0) {
|
||||
return this.engine.Env.FORCE_RELAYS
|
||||
}
|
||||
|
||||
if (relays.length === 0) {
|
||||
error(`Attempted to connect to zero urls`)
|
||||
}
|
||||
|
||||
const urls = new Set(relays.map(normalizeRelayUrl))
|
||||
|
||||
if (urls.size !== relays.length) {
|
||||
warn(`Attempted to connect to non-unique relays`)
|
||||
}
|
||||
|
||||
return Array.from(urls)
|
||||
}
|
||||
|
||||
getExecutor = (urls: string[], {bypassBoot = false} = {}) => {
|
||||
let target
|
||||
|
||||
const muxUrl = this.engine.Settings.getSetting("multiplextr_url")
|
||||
|
||||
// Try to use our multiplexer, but if it fails to connect fall back to relays. If
|
||||
// we're only connecting to a single relay, just do it directly, unless we already
|
||||
// have a connection to the multiplexer open, in which case we're probably doing
|
||||
// AUTH with a single relay.
|
||||
if (muxUrl && (urls.length > 1 || this.pool.has(muxUrl))) {
|
||||
const connection = this.pool.get(muxUrl)
|
||||
|
||||
if (connection.socket.isHealthy()) {
|
||||
target = new Plex(urls, connection)
|
||||
}
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
target = new Relays(urls.map(url => this.pool.get(url)))
|
||||
}
|
||||
|
||||
const executor = new Executor(target)
|
||||
|
||||
if (this.authHandler) {
|
||||
executor.handleAuth({onAuth: this.authHandler, onOk: noop})
|
||||
}
|
||||
|
||||
return executor
|
||||
}
|
||||
|
||||
publish = ({
|
||||
relays,
|
||||
event,
|
||||
onProgress,
|
||||
timeout = 3000,
|
||||
verb = "EVENT",
|
||||
}: PublishOpts): Promise<Progress> => {
|
||||
const urls = this.getUrls(relays)
|
||||
const executor = this.getExecutor(urls, {bypassBoot: verb === "AUTH"})
|
||||
|
||||
if (event.wrap) {
|
||||
throw new Error("Can't publish unwrapped event")
|
||||
}
|
||||
|
||||
info(`Publishing to ${urls.length} relays`, event, urls)
|
||||
|
||||
return new Promise(resolve => {
|
||||
const timeouts = new Set<string>()
|
||||
const succeeded = new Set<string>()
|
||||
const failed = new Set<string>()
|
||||
|
||||
const getProgress = () => {
|
||||
const completed = union(timeouts, succeeded, failed)
|
||||
const pending = difference(new Set(urls), completed)
|
||||
|
||||
return {event, succeeded, failed, timeouts, completed, pending}
|
||||
}
|
||||
|
||||
const attemptToResolve = () => {
|
||||
const progress = getProgress()
|
||||
|
||||
if (progress.pending.size === 0) {
|
||||
resolve(progress)
|
||||
sub.unsubscribe()
|
||||
executor.target.cleanup()
|
||||
}
|
||||
|
||||
onProgress?.(progress)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
for (const url of urls) {
|
||||
if (!succeeded.has(url) && !failed.has(url)) {
|
||||
timeouts.add(url)
|
||||
}
|
||||
}
|
||||
|
||||
attemptToResolve()
|
||||
}, timeout)
|
||||
|
||||
const sub = executor.publish(omit(["seen_on"], event), {
|
||||
verb,
|
||||
onOk: (url: string) => {
|
||||
succeeded.add(url)
|
||||
timeouts.delete(url)
|
||||
failed.delete(url)
|
||||
attemptToResolve()
|
||||
},
|
||||
onError: (url: string) => {
|
||||
failed.add(url)
|
||||
timeouts.delete(url)
|
||||
attemptToResolve()
|
||||
},
|
||||
})
|
||||
|
||||
// Report progress to start
|
||||
attemptToResolve()
|
||||
})
|
||||
}
|
||||
|
||||
subscribe = ({
|
||||
relays,
|
||||
filter,
|
||||
onEose,
|
||||
onEvent,
|
||||
onClose,
|
||||
timeout,
|
||||
shouldProcess = true,
|
||||
}: SubscribeOpts) => {
|
||||
const urls = this.getUrls(relays)
|
||||
const subscription = new Subscription({
|
||||
executor: this.getExecutor(urls),
|
||||
filters: ensurePlural(filter),
|
||||
relays: urls,
|
||||
timeout,
|
||||
})
|
||||
|
||||
info(`Starting subscription with ${urls.length} relays`, {filter, urls})
|
||||
|
||||
if (onEose) subscription.on("eose", onEose)
|
||||
if (onClose) subscription.on("close", onClose)
|
||||
|
||||
subscription.on("event", event => {
|
||||
if (shouldProcess) {
|
||||
this.engine.Events.queue.push(event)
|
||||
}
|
||||
|
||||
onEvent?.(event)
|
||||
})
|
||||
|
||||
return subscription
|
||||
}
|
||||
|
||||
count = async (filter: Filter | Filter[]) => {
|
||||
const filters = ensurePlural(filter)
|
||||
const executor = this.getExecutor(this.getUrls(this.engine.Env.COUNT_RELAYS))
|
||||
|
||||
return new Promise(resolve => {
|
||||
const sub = executor.count(filters, {
|
||||
onCount: (url: string, {count}: {count: number}) => resolve(count),
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(0)
|
||||
sub.unsubscribe()
|
||||
executor.target.cleanup()
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import {last} from "ramda"
|
||||
import {tryFunc, Fetch} from "hurdak"
|
||||
import {now, tryJson} from "src/util/misc"
|
||||
import type {Handle} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import {collection} from "src/engine/util/store"
|
||||
|
||||
export class Nip05 {
|
||||
handles = collection<Handle>("pubkey")
|
||||
|
||||
getHandle = (pubkey: string) => this.handles.key(pubkey).get()
|
||||
|
||||
displayHandle = (handle: Handle) =>
|
||||
handle.address.startsWith("_@") ? last(handle.address.split("@")) : handle.address
|
||||
|
||||
initialize(engine: Engine) {
|
||||
engine.Events.addHandler(0, e => {
|
||||
tryJson(async () => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const handle = this.handles.key(e.pubkey)
|
||||
|
||||
if (!kind0.nip05 || e.created_at < (handle.get()?.created_at || 0)) {
|
||||
return
|
||||
}
|
||||
|
||||
const body = {handle: kind0.nip05}
|
||||
const url = engine.Settings.dufflepud("handle/info")
|
||||
const profile = (await tryFunc(() => Fetch.postJson(url, body))) as null | {
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
if (profile?.pubkey !== e.pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
handle.set({
|
||||
profile: profile,
|
||||
pubkey: e.pubkey,
|
||||
address: kind0.nip05,
|
||||
created_at: e.created_at,
|
||||
updated_at: now(),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
import {switcherFn, tryFunc} from "hurdak"
|
||||
import {tryJson} from "src/util/misc"
|
||||
import {LRUCache} from "src/util/lruCache"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import * as nip44 from "src/engine/util/nip44"
|
||||
|
||||
export type Nip44Opts = {
|
||||
pk?: string
|
||||
sk?: string
|
||||
}
|
||||
|
||||
const sharedSecretCache = new LRUCache<string, Uint8Array>(100)
|
||||
|
||||
// Deriving shared secret is an expensive computation
|
||||
function getSharedSecret(sk: string, pk: string) {
|
||||
const cacheKey = `${sk.slice(0, 8)}:${pk}`
|
||||
|
||||
let result = sharedSecretCache.get(cacheKey)
|
||||
|
||||
if (!result) {
|
||||
result = nip44.getSharedSecret(sk, pk)
|
||||
|
||||
sharedSecretCache.set(cacheKey, result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export class Nip44 {
|
||||
engine: Engine
|
||||
|
||||
async encrypt(message: string, {pk, sk}: Nip44Opts = {}) {
|
||||
if (!pk) {
|
||||
pk = this.engine.Keys.pubkey.get()
|
||||
}
|
||||
|
||||
if (sk) {
|
||||
return nip44.encrypt(getSharedSecret(sk, pk), message)
|
||||
}
|
||||
|
||||
return switcherFn(this.engine.Keys.method.get(), {
|
||||
privkey: () => {
|
||||
const privkey = this.engine.Keys.privkey.get() as string
|
||||
const secret = getSharedSecret(privkey, pk)
|
||||
|
||||
return nip44.encrypt(secret, message)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async decrypt(message, {pk, sk}: Nip44Opts = {}) {
|
||||
if (!pk) {
|
||||
pk = this.engine.Keys.pubkey.get()
|
||||
}
|
||||
|
||||
if (sk) {
|
||||
return nip44.decrypt(getSharedSecret(sk, pk), message)
|
||||
}
|
||||
|
||||
return switcherFn(this.engine.Keys.method.get(), {
|
||||
privkey: () => {
|
||||
const privkey = this.engine.Keys.privkey.get() as string
|
||||
const secret = getSharedSecret(privkey, pk)
|
||||
|
||||
return tryFunc(() => nip44.decrypt(secret, message)) || `<Failed to decrypt message>`
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async encryptJson(data: any, opts?: Nip44Opts) {
|
||||
return this.encrypt(JSON.stringify(data), opts)
|
||||
}
|
||||
|
||||
async decryptJson(data: string, opts?: Nip44Opts) {
|
||||
return tryJson(async () => JSON.parse(await this.decrypt(data, opts)))
|
||||
}
|
||||
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import {Tags, appDataKeys} from "src/util/nostr"
|
||||
import {writable} from "src/engine/util/store"
|
||||
import type {Writable} from "src/engine/util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
|
||||
export class Settings {
|
||||
engine: Engine
|
||||
settings: Writable<Record<string, any>>
|
||||
|
||||
getSetting = (k: string) => this.settings.get()[k]
|
||||
|
||||
imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
|
||||
const base = this.getSetting("imgproxy_url")
|
||||
|
||||
if (!url || url.match("gif$")) {
|
||||
return url
|
||||
}
|
||||
|
||||
try {
|
||||
return base && url ? `${base}/x/s:${w}:${h}/${btoa(url)}` : url
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
dufflepud = (path: string) => `${this.getSetting("dufflepud_url")}/${path}`
|
||||
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
|
||||
this.settings = writable<Record<string, any>>({
|
||||
last_updated: 0,
|
||||
relay_limit: 10,
|
||||
default_zap: 21,
|
||||
show_media: true,
|
||||
report_analytics: true,
|
||||
imgproxy_url: engine.Env.IMGPROXY_URL,
|
||||
dufflepud_url: engine.Env.DUFFLEPUD_URL,
|
||||
multiplextr_url: engine.Env.MULTIPLEXTR_URL,
|
||||
})
|
||||
|
||||
engine.Events.addHandler(30078, async e => {
|
||||
if (
|
||||
engine.Keys.canSign.get() &&
|
||||
Tags.from(e).getMeta("d") === appDataKeys.USER_SETTINGS &&
|
||||
e.created_at > this.getSetting("last_updated")
|
||||
) {
|
||||
const updates = await engine.Crypt.decryptJson(e.content)
|
||||
|
||||
if (updates) {
|
||||
this.settings.update($settings => ({
|
||||
...$settings,
|
||||
...updates,
|
||||
last_updated: e.created_at,
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -6,8 +6,4 @@ export {Crypt} from "./components/Crypt"
|
||||
export {Directory} from "./components/Directory"
|
||||
export {Events} from "./components/Events"
|
||||
export {Keys} from "./components/Keys"
|
||||
export {Network} from "./components/Network"
|
||||
export {Nip04} from "./components/Nip04"
|
||||
export {Nip05} from "./components/Nip05"
|
||||
export {Nip28} from "./components/Nip28"
|
||||
export {Nip44} from "./components/Nip44"
|
||||
|
@ -1,43 +0,0 @@
|
||||
import {base64} from "@scure/base"
|
||||
import {randomBytes} from "@noble/hashes/utils"
|
||||
import {secp256k1} from "@noble/curves/secp256k1"
|
||||
import {sha256} from "@noble/hashes/sha256"
|
||||
import {xchacha20} from "@noble/ciphers/chacha"
|
||||
|
||||
export const utf8Decoder = new TextDecoder()
|
||||
|
||||
export const utf8Encoder = new TextEncoder()
|
||||
|
||||
export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array =>
|
||||
sha256(secp256k1.getSharedSecret(privkey, "02" + pubkey).subarray(1, 33))
|
||||
|
||||
export function encrypt(key: Uint8Array, text: string, v = 1) {
|
||||
if (v !== 1) {
|
||||
throw new Error("NIP44: unknown encryption version")
|
||||
}
|
||||
|
||||
const nonce = randomBytes(24)
|
||||
const plaintext = utf8Encoder.encode(text)
|
||||
const ciphertext = xchacha20(key, nonce, plaintext)
|
||||
|
||||
const payload = new Uint8Array(25 + ciphertext.length)
|
||||
payload.set([v], 0)
|
||||
payload.set(nonce, 1)
|
||||
payload.set(ciphertext, 25)
|
||||
|
||||
return base64.encode(payload)
|
||||
}
|
||||
|
||||
export function decrypt(key: Uint8Array, payload: string) {
|
||||
const data = base64.decode(payload)
|
||||
|
||||
if (data[0] !== 1) {
|
||||
throw new Error(`NIP44: unknown encryption version: ${data[0]}`)
|
||||
}
|
||||
|
||||
const nonce = data.slice(1, 25)
|
||||
const ciphertext = data.slice(25)
|
||||
const plaintext = xchacha20(key, nonce, ciphertext)
|
||||
|
||||
return utf8Decoder.decode(plaintext)
|
||||
}
|
@ -21,11 +21,9 @@ export const displayRelay = ({url}: Relay) => last(url.split("://"))
|
||||
export const searchRelays = relays.derived($relays => fuzzy($relays, {keys: ["url"]}))
|
||||
|
||||
export const searchableRelays = relays.derived($relays => {
|
||||
const searchableRelayUrls = $relays
|
||||
.filter(r => (r.info?.supported_nips || []).includes(50))
|
||||
.map(prop("url"))
|
||||
const urls = $relays.filter(r => (r.info?.supported_nips || []).includes(50)).map(prop("url"))
|
||||
|
||||
return uniq(env.get().SEARCH_RELAYS.concat(searchableRelayUrls)).slice(0, 8) as string[]
|
||||
return uniq(env.get().SEARCH_RELAYS.concat(urls)).slice(0, 8) as string[]
|
||||
})
|
||||
|
||||
export const getPubkeyRelays = (pubkey: string, mode: string = null) => {
|
||||
|
18
src/engine2/requests/count.ts
Normal file
18
src/engine2/requests/count.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type {Filter} from "src/engine2/model"
|
||||
import {getUrls, getExecutor, searchableRelays} from "src/engine2/queries"
|
||||
|
||||
export const count = async (filters: Filter[]) => {
|
||||
const executor = getExecutor(getUrls(searchableRelays.get()))
|
||||
|
||||
return new Promise(resolve => {
|
||||
const sub = executor.count(filters, {
|
||||
onCount: (url: string, {count}: {count: number}) => resolve(count),
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(0)
|
||||
sub.unsubscribe()
|
||||
executor.target.cleanup()
|
||||
}, 3000)
|
||||
})
|
||||
}
|
@ -2,6 +2,8 @@ export * from "./context"
|
||||
export * from "./cursor"
|
||||
export * from "./feed"
|
||||
export * from "./pubkeys"
|
||||
export * from "./count"
|
||||
export * from "./load"
|
||||
export * from "./subscription"
|
||||
export * from "./thread"
|
||||
export * from "./nip04"
|
||||
|
76
src/engine2/requests/load.ts
Normal file
76
src/engine2/requests/load.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import {matchFilters} from "nostr-tools"
|
||||
import {prop, groupBy, uniq} from "ramda"
|
||||
import {batch} from "hurdak"
|
||||
import {subscribe} from "./subscription"
|
||||
import type {Event, Filter} from "src/engine2/model"
|
||||
|
||||
export type LoadOpts = {
|
||||
relays: string[]
|
||||
filters: Filter[]
|
||||
onEvent?: (e: Event) => void
|
||||
onEose?: (url: string) => void
|
||||
onClose?: (events: Event[]) => void
|
||||
}
|
||||
|
||||
export const calculateGroup = ({since, until, ...filter}: Filter) => {
|
||||
const group = Object.keys(filter)
|
||||
|
||||
if (since) {
|
||||
group.push(`since:${since}`)
|
||||
}
|
||||
|
||||
if (until) {
|
||||
group.push(`until:${until}`)
|
||||
}
|
||||
|
||||
return group.sort().join("-")
|
||||
}
|
||||
|
||||
export const combineFilters = filters => {
|
||||
const result = []
|
||||
|
||||
for (const group of Object.values(groupBy(calculateGroup, filters))) {
|
||||
const newFilter = {}
|
||||
|
||||
for (const k of Object.keys(group[0])) {
|
||||
newFilter[k] = uniq(group.flatMap(prop(k)))
|
||||
}
|
||||
|
||||
result.push(newFilter)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const load = batch(500, (requests: LoadOpts[]) => {
|
||||
const relays = uniq(requests.flatMap(prop("relays")))
|
||||
const filters = combineFilters(requests.flatMap(prop("filters")))
|
||||
|
||||
const sub = subscribe({relays, filters, timeout: 3000})
|
||||
|
||||
sub.on("event", (e: Event) => {
|
||||
for (const req of requests) {
|
||||
if (!req.onEvent) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (matchFilters(req.filters, e)) {
|
||||
req.onEvent(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
sub.on("eose", url => {
|
||||
for (const req of requests) {
|
||||
req.onEose?.(url)
|
||||
}
|
||||
})
|
||||
|
||||
sub.on("close", events => {
|
||||
for (const req of requests) {
|
||||
req.onClose?.(events)
|
||||
}
|
||||
})
|
||||
|
||||
return sub
|
||||
})
|
@ -7,7 +7,7 @@ import type {Event, Filter} from "src/engine2/model"
|
||||
import {getUrls, getExecutor} from "src/engine2/queries"
|
||||
import {projections} from "src/engine2/projections"
|
||||
|
||||
type SubscriptionOpts = {
|
||||
export type SubscriptionOpts = {
|
||||
relays: string[]
|
||||
filters: Filter[]
|
||||
timeout?: number
|
||||
@ -102,3 +102,19 @@ export class Subscription extends EventEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SubscribeOpts = SubscriptionOpts & {
|
||||
onEvent?: (e: Event) => void
|
||||
onEose?: (url: string) => void
|
||||
onClose?: (events: Event[]) => void
|
||||
}
|
||||
|
||||
export const subscribe = (opts: SubscribeOpts) => {
|
||||
const sub = new Subscription(opts)
|
||||
|
||||
if (opts.onEvent) sub.on("event", opts.onEvent)
|
||||
if (opts.onEose) sub.on("eose", opts.onEose)
|
||||
if (opts.onClose) sub.on("close", opts.onClose)
|
||||
|
||||
return sub
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user