From e3cf09ce500a074419088bf8c860d48a48cb6bb8 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Fri, 6 Jan 2023 14:19:59 -0800 Subject: [PATCH] Upgrade nostr-tools, add agent --- .eslintrc.cjs | 1 + README.md | 6 +++ package-lock.json | Bin 260351 -> 244475 bytes package.json | 2 +- src/agent/index.js | 4 ++ src/agent/keys.js | 33 ++++++++++++++ src/agent/pool.js | 109 +++++++++++++++++++++++++++++++++++++++++++++ src/relay/pool.js | 74 ++++++++++-------------------- 8 files changed, 177 insertions(+), 52 deletions(-) create mode 100644 src/agent/index.js create mode 100644 src/agent/keys.js create mode 100644 src/agent/pool.js 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 f3a68fd321eb0483afb0ef8b9edd3581f23718a7..bc23b3773738fcc23755760ef87049c9379bf626 100644 GIT binary patch delta 1045 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 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, }