From 7f11c0c618449579bac0b38ca2bc4cef6cbd4b2b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 21 Dec 2023 19:57:28 -0300 Subject: [PATCH] unsplit, backwards-compatibility, wasm relay and pool must be configured manually from the abstract classes. --- .gitignore | 1 + README.md | 17 ++++-- trusted-pool.ts => abstract-pool.ts | 23 +++++---- trusted-relay.ts => abstract-relay.ts | 36 ++++++++----- benchmark.ts | 71 ++++++++++++++++++++++++++ build.js | 1 + bun.lockb | Bin 130355 -> 130695 bytes core.ts | 1 + helpers.ts | 7 +++ package.json | 44 +++++++--------- pool-pure.ts | 10 ---- pool-wasm.ts | 4 +- pool.test.ts | 2 +- pool.ts | 10 ++++ relay-wasm.ts | 16 ------ relay-pure.ts => relay.ts | 8 +-- 16 files changed, 162 insertions(+), 89 deletions(-) rename trusted-pool.ts => abstract-pool.ts (87%) rename trusted-relay.ts => abstract-relay.ts (90%) create mode 100644 benchmark.ts delete mode 100644 pool-pure.ts create mode 100644 pool.ts delete mode 100644 relay-wasm.ts rename relay-pure.ts => relay.ts (53%) 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 62dc90de35dd76079108148e09a1a1cd3bbef12b..6cde0595cfc609b042997218833d12caed783235 100755 GIT binary patch delta 22947 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 delta 22495 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 Vq@8QYZ`)>CYL(j7r%2Vy`Coj&R3!ia 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'