diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0d6049c2..bd809a07 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -20,6 +20,7 @@ module.exports = { "rules": { "a11y-click-events-have-key-events": "off", "no-unused-vars": ["error", {args: "none"}], + "no-async-promise-executor": "off", }, "ignorePatterns": ["*.svg"] } diff --git a/README.md b/README.md index 7ed701b9..54193561 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,12 @@ If you like Coracle and want to support its development, you can donate sats via # Changelog +## Current + +- [ ] Upgrade nostr-tools + - [ ] Close connections that haven't been used in a while + - [ ] Move key management stuff + ## 0.2.6 - [x] Add support for at-mentions in note and reply composition diff --git a/package-lock.json b/package-lock.json index f3a68fd3..bc23b377 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 14e05105..5b41b861 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "extract-urls": "^1.3.2", "fuse.js": "^6.6.2", "hurdak": "github:ConsignCloud/hurdak", - "nostr-tools": "github:fiatjaf/nostr-tools#1b798b2", + "nostr-tools": "^1.1.1", "ramda": "^0.28.0", "svelte-link-preview": "^0.3.3", "svelte-loading-spinners": "^0.3.4", diff --git a/src/agent/index.js b/src/agent/index.js new file mode 100644 index 00000000..36e5b9ac --- /dev/null +++ b/src/agent/index.js @@ -0,0 +1,4 @@ +import keys from 'src/agent/keys' +import pool from 'src/agent/pool' + +export default {pool, keys} diff --git a/src/agent/keys.js b/src/agent/keys.js new file mode 100644 index 00000000..99a28a89 --- /dev/null +++ b/src/agent/keys.js @@ -0,0 +1,33 @@ +import {getPublicKey, getEventHash, signEvent} from 'nostr-tools' + +let pubkey +let privkey +let signingFunction + +const getPubkey = () => { + return pubkey || getPublicKey(privkey) +} + +const setPrivateKey = _privkey => { + privkey = _privkey +} + +const setPublicKey = _pubkey => { + signingFunction = async event => { + const {sig} = await window.nostr.signEvent(event) + + return sig + } + + pubkey = _pubkey +} + +const sign = async event => { + event.pubkey = pubkey + event.id = getEventHash(event) + event.sig = signingFunction ? await signingFunction(event) : signEvent(event, privkey) + + return event +} + +export default {getPubkey, setPrivateKey, setPublicKey, sign} diff --git a/src/agent/pool.js b/src/agent/pool.js new file mode 100644 index 00000000..c48b12b2 --- /dev/null +++ b/src/agent/pool.js @@ -0,0 +1,109 @@ +import {relayInit} from 'nostr-tools' +import {partial, uniqBy, prop} from 'ramda' +import {ensurePlural} from 'hurdak/lib/hurdak' + +const relays = {} + +const connect = async url => { + if (!relays[url]) { + relays[url] = relayInit(url) + relays[url].url = url + relays[url].stats = { + count: 0, + timer: 0, + timeouts: 0, + activeCount: 0, + } + + relays[url].on('disconnect', () => { + delete relays[url] + }) + + relays[url].connected = relays[url].connect() + } + + await relays[url].connected + + return relays[url] +} + +const publish = (urls, event) => { + urls.forEach(async url => { + const relay = await connect(url) + + relay.publish(event) + }) +} + +const sub = async (urls, filters) => { + const subs = await Promise.all( + urls.map(async url => { + const relay = await connect(url) + const sub = relay.sub(ensurePlural(filters)) + + sub.relay = relay + sub.relay.stats.activeCount += 1 + + if (sub.relay.stats.activeCount > 10) { + console.warning(`Relay ${url} has >10 active subscriptions`) + } + + return sub + }) + ) + + return { + unsub: () => { + subs.forEach(sub => { + sub.unsub() + sub.relay.stats.activeCount -= 1 + }) + }, + on: (type, cb) => { + subs.forEach(sub => { + sub.on(type, partial(cb, [sub.relay.url])) + }) + }, + } +} + +const all = (urls, filters) => { + return new Promise(async resolve => { + const subscription = await sub(urls, filters) + const now = Date.now() + const events = [] + const eose = [] + + const done = () => { + resolve(uniqBy(prop('id'), events)) + + // Keep track of relay timeouts + urls.forEach(url => { + if (!eose.includes(url)) { + relays[url].stats.count += 1 + relays[url].stats.timer += Date.now() - now + relays[url].stats.timeouts += 1 + } + }) + } + + subscription.on('event', (url, e) => events.push(e)) + + subscription.on('eose', url => { + eose.push(url) + + // Keep track of relay timing stats + relays[url].stats.count += 1 + relays[url].stats.timer += Date.now() - now + + if (eose.length === urls.length) { + done() + } + }) + + // If a relay takes too long, give up + setTimeout(done, 5000) + }) +} + +export default {relays, connect, publish, sub, all} diff --git a/src/relay/pool.js b/src/relay/pool.js index 663c8064..1bed5d4d 100644 --- a/src/relay/pool.js +++ b/src/relay/pool.js @@ -1,15 +1,13 @@ import {uniqBy, sortBy, find, propEq, prop, uniq} from 'ramda' import {get} from 'svelte/store' -import {relayPool, getPublicKey} from 'nostr-tools' import {noop, range, sleep} from 'hurdak/lib/hurdak' import {getTagValues, filterTags} from "src/util/nostr" +import agent from 'src/agent' import {db} from 'src/relay/db' // ============================================================================ // Utils/config -const pool = relayPool() - class Channel { constructor(name) { this.name = name @@ -21,8 +19,8 @@ class Channel { release() { this.status = 'idle' } - sub(filter, onEvent, onEose = noop, opts = {}) { - const relays = Object.keys(pool.relays) + async sub(filter, onEvent, onEose = noop, opts = {}) { + const relays = getRelays() // If we don't have any relays, we'll wait forever for an eose, but // we already know we're done. Use a timeout since callers are @@ -36,18 +34,20 @@ class Channel { // Start our subscription, wait for only our fastest relays to eose before calling it done. // We were waiting for all before, but that made the slowest relay a bottleneck. Waiting for // only one meant we might be settling for very incomplete data - const start = new Date().valueOf() const lastEvent = {} const eoseRelays = [] + // Create our subscription + const sub = await agent.pool.sub(relays, filter) + // Keep track of when we last heard from each relay, and close unresponsive ones - const cb = (e, r) => { + sub.on('event', (r, e) => { lastEvent[r] = new Date().valueOf() onEvent(e) - } + }) // If we have lots of relays, ignore the slowest ones - const onRelayEose = r => { + sub.on('eose', r => { eoseRelays.push(r) // If we have only a few, wait for all of them, otherwise ignore the slowest 1/5 @@ -55,28 +55,14 @@ class Channel { if (eoseRelays.length >= relays.length - threshold) { onEose() } - } - - // Create our subscription - const sub = pool.sub({filter, cb}, this.name, onRelayEose) - - // Watch for relays that are slow to respond and give up on them - const interval = !opts.timeout ? null : setInterval(() => { - for (const r of relays) { - if ((lastEvent[r] || start) < new Date().valueOf() - opts.timeout) { - onRelayEose(r) - } - } - }, 300) + }) // Clean everything up when we're done const done = () => { if (this.status === 'busy') { sub.unsub() + this.release() } - - clearInterval(interval) - this.release() } return {unsub: done} @@ -86,7 +72,7 @@ class Channel { return new Promise(async resolve => { const result = [] - const sub = this.sub( + const sub = await this.sub( filter, e => result.push(e), r => { @@ -122,39 +108,25 @@ const getChannel = async () => { const req = async (...args) => (await getChannel()).all(...args) const sub = async (...args) => (await getChannel()).sub(...args) -const getPubkey = () => { - return pool._pubkey || getPublicKey(pool._privkey) -} - const getRelays = () => { - return Object.keys(pool.relays) + return get(db.connections) } const addRelay = url => { - pool.addRelay(url) + agent.pool.connect(url) } -const removeRelay = url => { - pool.removeRelay(url) -} +const removeRelay = async url => { + const relay = await agent.pool.connect(url) -const setPrivateKey = privkey => { - pool.setPrivateKey(privkey) - pool._privkey = privkey -} - -const setPublicKey = pubkey => { - pool.registerSigningFunction(async event => { - const {sig} = await window.nostr.signEvent(event) - - return sig - }) - - pool._pubkey = pubkey + relay.close() } const publishEvent = event => { - pool.publish(event) + const relays = getRelays() + + event = agent.keys.sign(event) + agent.publish(relays, event) db.events.process(event) } @@ -240,6 +212,6 @@ const syncNetwork = async () => { } export default { - getPubkey, getRelays, addRelay, removeRelay, setPrivateKey, setPublicKey, - publishEvent, loadEvents, listenForEvents, syncNetwork, loadPeople, + getRelays, addRelay, removeRelay, publishEvent, loadEvents, listenForEvents, + syncNetwork, loadPeople, }