diff --git a/.gitignore b/.gitignore index f9940ed2..f1ca5165 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ package-lock.json .envrc lib test.html +bench.js diff --git a/README.md b/README.md index 93e05955..6ebe5b79 100644 --- a/README.md +++ b/README.md @@ -210,8 +210,8 @@ Importing the entirety of `nostr-tools` may bloat your build, so you should prob ```js import { generateSecretKey, finalizeEvent, verifyEvent } from 'nostr-tools/pure' -import SimplePool from 'nostr-tools/pool-pure' -import Relay, { Subscription } from 'nostr-tools/relay-pure' +import { SimplePool } from 'nostr-tools/pool-pure' +import { Relay, Subscription } from 'nostr-tools/relay' import { matchFilter } from 'nostr-tools/filter' import { decode, nprofileEncode, neventEncode, npubEncode } from 'nostr-tools/nip19' // and so on and so forth @@ -232,11 +232,18 @@ initNostrWasm().then(setNostrWasm) // see https://www.npmjs.com/package/nostr-wasm for options ``` -If you're going to use `Relay` and `SimplePool` you must also import `nostr-tools/relay-wasm` and/or `nostr-tools/pool-wasm` instead of the defaults: +If you're going to use `Relay` and `SimplePool` you must also import `nostr-tools/abstract-relay` and/or `nostr-tools/abstract-pool` instead of the defaults and then instantiate them by passing the `verifyEvent`: ```js -import Relay, { Subscription } from 'nostr-tools/relay-wasm' -import SimplePool from 'nostr-tools/pool-wasm' +import { setNostrWasm, verifyEvent } from 'nostr-tools/wasm' +import { AbstractRelay } from 'nostr-tools/abstract-relay' +import { AbstractSimplePool } from 'nostr-tools/abstract-pool' +import { initNostrWasm } from 'nostr-wasm' + +initNostrWasm().then(setNostrWasm) + +const relay = AbstractRelay.connect('wss://relayable.org', { verifyEvent }) +const pool = new AbstractSimplePool({ verifyEvent }) ``` This may be faster than the pure-JS [noble libraries](https://paulmillr.com/noble/) used by default and in `nostr-tools/pure`. diff --git a/trusted-pool.ts b/abstract-pool.ts similarity index 87% rename from trusted-pool.ts rename to abstract-pool.ts index 90d92b06..d37ceea9 100644 --- a/trusted-pool.ts +++ b/abstract-pool.ts @@ -1,8 +1,9 @@ -import Relay, { SubscriptionParams, Subscription } from './trusted-relay.ts' +import { AbstractRelay as AbstractRelay, SubscriptionParams, Subscription } from './abstract-relay.ts' import { normalizeURL } from './utils.ts' import type { Event, Nostr } from './core.ts' import { type Filter } from './filter.ts' +import { alwaysTrue } from './helpers.ts' export type SubCloser = { close: () => void } @@ -12,25 +13,25 @@ export type SubscribeManyParams = Omit & { id?: string } -export default class TrustedSimplePool { - private relays = new Map() - public seenOn = new Map>() +export class AbstractSimplePool { + private relays = new Map() + public seenOn = new Map>() public trackRelays: boolean = false - public verifyEvent: Nostr['verifyEvent'] | undefined + public verifyEvent: Nostr['verifyEvent'] public trustedRelayURLs = new Set() - constructor(opts: { verifyEvent?: Nostr['verifyEvent'] } = {}) { + constructor(opts: { verifyEvent: Nostr['verifyEvent'] }) { this.verifyEvent = opts.verifyEvent } - async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise { + async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise { url = normalizeURL(url) let relay = this.relays.get(url) if (!relay) { - relay = new Relay(url, { - verifyEvent: this.trustedRelayURLs.has(url) ? undefined : this.verifyEvent, + relay = new AbstractRelay(url, { + verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent, }) if (params?.connectionTimeout) relay.connectionTimeout = params.connectionTimeout this.relays.set(url, relay) @@ -48,7 +49,7 @@ export default class TrustedSimplePool { subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser { if (this.trackRelays) { - params.receivedEvent = (relay: Relay, id: string) => { + params.receivedEvent = (relay: AbstractRelay, id: string) => { let set = this.seenOn.get(id) if (!set) { set = new Set() @@ -99,7 +100,7 @@ export default class TrustedSimplePool { return } - let relay: Relay + let relay: AbstractRelay try { relay = await this.ensureRelay(url, { connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined, diff --git a/trusted-relay.ts b/abstract-relay.ts similarity index 90% rename from trusted-relay.ts rename to abstract-relay.ts index 981df8fd..cb788736 100644 --- a/trusted-relay.ts +++ b/abstract-relay.ts @@ -4,10 +4,10 @@ import type { Event, EventTemplate, Nostr } from './core.ts' import { matchFilters, type Filter } from './filter.ts' import { getHex64, getSubscriptionId } from './fakejson.ts' import { Queue, normalizeURL } from './utils.ts' -import { nip42 } from './index.ts' +import { makeAuthEvent } from './nip42.ts' import { yieldThread } from './helpers.ts' -export default class TrustedRelay { +export class AbstractRelay { public readonly url: string private _connected: boolean = false @@ -16,10 +16,10 @@ export default class TrustedRelay { public baseEoseTimeout: number = 4400 public connectionTimeout: number = 4400 + public openSubs = new Map() private connectionTimeoutHandle: ReturnType | undefined private connectionPromise: Promise | undefined - private openSubs = new Map() private openCountRequests = new Map() private openEventPublishes = new Map() private ws: WebSocket | undefined @@ -27,15 +27,15 @@ export default class TrustedRelay { private queueRunning = false private challenge: string | undefined private serial: number = 0 - private verifyEvent: Nostr['verifyEvent'] | undefined + private verifyEvent: Nostr['verifyEvent'] - constructor(url: string, opts: { verifyEvent?: Nostr['verifyEvent'] } = {}) { + constructor(url: string, opts: { verifyEvent: Nostr['verifyEvent'] }) { this.url = normalizeURL(url) this.verifyEvent = opts.verifyEvent } - static async connect(url: string, opts: { verifyEvent?: Nostr['verifyEvent'] } = {}) { - const relay = new TrustedRelay(url, opts) + static async connect(url: string, opts: { verifyEvent: Nostr['verifyEvent'] }) { + const relay = new AbstractRelay(url, opts) await relay.connect() return relay } @@ -163,7 +163,7 @@ export default class TrustedRelay { case 'EVENT': { const so = this.openSubs.get(data[1] as string) as Subscription const event = data[2] as Event - if ((this.verifyEvent ? this.verifyEvent(event) : true) && matchFilters(so.filters, event)) { + if (this.verifyEvent(event) && matchFilters(so.filters, event)) { so.onevent(event) } return @@ -200,7 +200,6 @@ export default class TrustedRelay { if (!so) return so.closed = true so.close(data[2] as string) - this.openSubs.delete(id) return } case 'NOTICE': @@ -226,7 +225,7 @@ export default class TrustedRelay { public async auth(signAuthEvent: (authEvent: EventTemplate) => Promise) { if (!this.challenge) throw new Error("can't perform auth, no challenge was received") - const evt = nip42.makeAuthEvent(this.url, this.challenge) + const evt = makeAuthEvent(this.url, this.challenge) await signAuthEvent(evt) this.send('["AUTH",' + JSON.stringify(evt) + ']') } @@ -268,17 +267,25 @@ export default class TrustedRelay { this._connected = false this.ws?.close() } + + // this method simulates receiving a message from the websocket + public _push(msg: string) { + this.incomingMessageQueue.enqueue(msg) + if (!this.queueRunning) { + this.runQueue() + } + } } export class Subscription { - public readonly relay: TrustedRelay + public readonly relay: AbstractRelay public readonly id: string public closed: boolean = false public eosed: boolean = false public filters: Filter[] public alreadyHaveEvent: ((id: string) => boolean) | undefined - public receivedEvent: ((relay: TrustedRelay, id: string) => void) | undefined + public receivedEvent: ((relay: AbstractRelay, id: string) => void) | undefined public onevent: (evt: Event) => void public oneose: (() => void) | undefined @@ -287,7 +294,7 @@ export class Subscription { public eoseTimeout: number private eoseTimeoutHandle: ReturnType | undefined - constructor(relay: TrustedRelay, id: string, filters: Filter[], params: SubscriptionParams) { + constructor(relay: AbstractRelay, id: string, filters: Filter[], params: SubscriptionParams) { this.relay = relay this.filters = filters this.id = id @@ -328,6 +335,7 @@ export class Subscription { this.relay.send('["CLOSE",' + JSON.stringify(this.id) + ']') this.closed = true } + this.relay.openSubs.delete(this.id) this.onclose?.(reason) } } @@ -337,7 +345,7 @@ export type SubscriptionParams = { oneose?: () => void onclose?: (reason: string) => void alreadyHaveEvent?: (id: string) => boolean - receivedEvent?: (relay: TrustedRelay, id: string) => void + receivedEvent?: (relay: AbstractRelay, id: string) => void eoseTimeout?: number } diff --git a/benchmark.ts b/benchmark.ts new file mode 100644 index 00000000..82703f2c --- /dev/null +++ b/benchmark.ts @@ -0,0 +1,71 @@ +import { initNostrWasm } from 'nostr-wasm' +import { NostrEvent } from './core' +import { finalizeEvent, generateSecretKey } from './pure' +import { setNostrWasm, verifyEvent } from './wasm' +import { AbstractRelay } from './abstract-relay.ts' +import { Relay as PureRelay } from './relay.ts' +import { alwaysTrue } from './helpers.ts' + +const RUNS = 400 + +let messages: string[] = [] +let baseContent = '' +for (let i = 0; i < RUNS; i++) { + baseContent += 'a' +} +const secretKey = generateSecretKey() +for (let i = 0; i < RUNS / 100; i++) { + const tags = [] + for (let t = 0; t < i; t++) { + tags.push(['t', 'nada']) + } + const event = { created_at: Math.round(Date.now()) / 1000, kind: 1, content: baseContent.slice(0, RUNS - i), tags } + const signed = finalizeEvent(event, secretKey) + messages.push(JSON.stringify(['EVENT', '_', signed])) +} + +setNostrWasm(await initNostrWasm()) + +const pureRelay = new PureRelay('wss://pure.com/') +const trustedRelay = new AbstractRelay('wss://trusted.com/', { verifyEvent: alwaysTrue }) +const wasmRelay = new AbstractRelay('wss://wasm.com/', { verifyEvent }) + +const run = (relay: AbstractRelay) => async () => { + return new Promise(resolve => { + let received = 0 + let sub = relay.prepareSubscription([{}], { + onevent(_: NostrEvent) { + received++ + if (received === messages.length - 1) { + resolve() + sub.closed = true + sub.close() + } + }, + id: '_', + }) + for (let e = 0; e < messages.length; e++) { + relay._push(messages[e]) + } + }) +} + +const benchmarks: Record Promise; runs: number[] }> = { + trusted: { test: run(trustedRelay), runs: [] }, + pure: { test: run(pureRelay), runs: [] }, + wasm: { test: run(wasmRelay), runs: [] }, +} + +for (let b = 0; b < 50; b++) { + for (let name in benchmarks) { + const { test, runs } = benchmarks[name] + const before = performance.now() + await test() + runs.push(performance.now() - before) + } +} + +for (let name in benchmarks) { + const { runs } = benchmarks[name] + console.log(name, runs.reduce((a, b) => a + b, 0) / runs.length) +} diff --git a/build.js b/build.js index 0ce633a6..b46642d8 100755 --- a/build.js +++ b/build.js @@ -10,6 +10,7 @@ const entryPoints = fs file !== 'core.ts' && file !== 'test-helpers.ts' && file !== 'helpers.ts' && + file !== 'benchmarks.ts' && !file.endsWith('.test.ts') && fs.statSync(join(process.cwd(), file)).isFile(), ) diff --git a/bun.lockb b/bun.lockb index 62dc90de..6cde0595 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/core.ts b/core.ts index 462e8ce8..d8be6aad 100644 --- a/core.ts +++ b/core.ts @@ -19,6 +19,7 @@ export interface Event { [verifiedSymbol]?: boolean } +export type NostrEvent = Event export type EventTemplate = Pick export type UnsignedEvent = Pick diff --git a/helpers.ts b/helpers.ts index 9931320d..7419d152 100644 --- a/helpers.ts +++ b/helpers.ts @@ -1,3 +1,5 @@ +import { verifiedSymbol, type Event, type Nostr } from './core.ts' + export async function yieldThread() { return new Promise(resolve => { const ch = new MessageChannel() @@ -7,3 +9,8 @@ export async function yieldThread() { ch.port1.start() }) } + +export const alwaysTrue: Nostr['verifyEvent'] = (t: Event) => { + t[verifiedSymbol] = true + return t[verifiedSymbol] +} diff --git a/package.json b/package.json index cc79d9c4..02213e1e 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "type": "module", "name": "nostr-tools", "version": "2.1.0", "description": "Tools for making a Nostr client.", @@ -39,35 +40,25 @@ "require": "./lib/cjs/filter.js", "types": "./lib/types/filter.d.ts" }, - "./trusted-relay": { - "import": "./lib/esm/trusted-relay.js", - "require": "./lib/cjs/trusted-relay.js", - "types": "./lib/types/trusted-relay.d.ts" + "./abstract-relay": { + "import": "./lib/esm/abstract-relay.js", + "require": "./lib/cjs/abstract-relay.js", + "types": "./lib/types/abstract-relay.d.ts" }, - "./relay-wasm": { - "import": "./lib/esm/relay-wasm.js", - "require": "./lib/cjs/relay-wasm.js", - "types": "./lib/types/relay-wasm.d.ts" + "./relay": { + "import": "./lib/esm/relay.js", + "require": "./lib/cjs/relay.js", + "types": "./lib/types/relay.d.ts" }, - "./relay-pure": { - "import": "./lib/esm/relay-pure.js", - "require": "./lib/cjs/relay-pure.js", - "types": "./lib/types/relay-pure.d.ts" + "./abstract-pool": { + "import": "./lib/esm/abstract-pool.js", + "require": "./lib/cjs/abstract-pool.js", + "types": "./lib/types/abstract-pool.d.ts" }, - "./trusted-pool": { - "import": "./lib/esm/trusted-pool.js", - "require": "./lib/cjs/trusted-pool.js", - "types": "./lib/types/trusted-pool.d.ts" - }, - "./pool-wasm": { - "import": "./lib/esm/pool-wasm.js", - "require": "./lib/cjs/pool-wasm.js", - "types": "./lib/types/pool-wasm.d.ts" - }, - "./pool-pure": { - "import": "./lib/esm/pool-pure.js", - "require": "./lib/cjs/pool-pure.js", - "types": "./lib/types/pool-pure.d.ts" + "./pool": { + "import": "./lib/esm/pool.js", + "require": "./lib/cjs/pool.js", + "types": "./lib/types/pool.d.ts" }, "./references": { "import": "./lib/esm/references.js", @@ -183,6 +174,7 @@ "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", + "mitata": "^0.1.6", "nostr-wasm": "v0.0.3" }, "peerDependencies": { diff --git a/pool-pure.ts b/pool-pure.ts deleted file mode 100644 index b33de080..00000000 --- a/pool-pure.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { verifyEvent } from './pure.ts' -import TrustedSimplePool from './trusted-pool.ts' - -export default class PureSimplePool extends TrustedSimplePool { - constructor() { - super({ verifyEvent }) - } -} - -export * from './trusted-pool.ts' diff --git a/pool-wasm.ts b/pool-wasm.ts index 0e964eb7..8e444c5e 100644 --- a/pool-wasm.ts +++ b/pool-wasm.ts @@ -1,7 +1,7 @@ import { verifyEvent } from './wasm.ts' -import TrustedSimplePool from './trusted-pool.ts' +import { TrustedSimplePool } from './trusted-pool.ts' -export default class WasmSimplePool extends TrustedSimplePool { +export class SimplePool extends TrustedSimplePool { constructor() { super({ verifyEvent }) } diff --git a/pool.test.ts b/pool.test.ts index c7c003f1..6a1ea367 100644 --- a/pool.test.ts +++ b/pool.test.ts @@ -2,7 +2,7 @@ import { test, expect, afterAll } from 'bun:test' import { finalizeEvent, type Event } from './pure.ts' import { generateSecretKey, getPublicKey } from './pure.ts' -import { SimplePool } from './pool.ts' +import SimplePool from './pool-pure.ts' let pool = new SimplePool() diff --git a/pool.ts b/pool.ts new file mode 100644 index 00000000..d8333ce2 --- /dev/null +++ b/pool.ts @@ -0,0 +1,10 @@ +import { verifyEvent } from './pure.ts' +import { AbstractSimplePool } from './abstract-pool.ts' + +export class SimplePool extends AbstractSimplePool { + constructor() { + super({ verifyEvent }) + } +} + +export * from './abstract-pool.ts' diff --git a/relay-wasm.ts b/relay-wasm.ts deleted file mode 100644 index ef7d3028..00000000 --- a/relay-wasm.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { verifyEvent } from './wasm.ts' -import TrustedRelay from './trusted-relay.ts' - -export default class WasmRelay extends TrustedRelay { - constructor(url: string) { - super(url, { verifyEvent }) - } - - static async connect(url: string) { - const relay = new WasmRelay(url) - await relay.connect() - return relay - } -} - -export * from './trusted-relay.ts' diff --git a/relay-pure.ts b/relay.ts similarity index 53% rename from relay-pure.ts rename to relay.ts index 46688552..0de4e11a 100644 --- a/relay-pure.ts +++ b/relay.ts @@ -1,16 +1,16 @@ import { verifyEvent } from './pure.ts' -import TrustedRelay from './trusted-relay.ts' +import { AbstractRelay } from './abstract-relay.ts' -export default class PureRelay extends TrustedRelay { +export class Relay extends AbstractRelay { constructor(url: string) { super(url, { verifyEvent }) } static async connect(url: string) { - const relay = new PureRelay(url) + const relay = new Relay(url) await relay.connect() return relay } } -export * from './trusted-relay.ts' +export * from './abstract-relay.ts'