unsplit, backwards-compatibility, wasm relay and pool must be configured manually from the abstract classes.

This commit is contained in:
fiatjaf 2023-12-21 19:57:28 -03:00
parent a4ae964ee6
commit 7f11c0c618
No known key found for this signature in database
GPG Key ID: BAD43C4BE5C1A3A1
16 changed files with 162 additions and 89 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ package-lock.json
.envrc .envrc
lib lib
test.html test.html
bench.js

View File

@ -210,8 +210,8 @@ Importing the entirety of `nostr-tools` may bloat your build, so you should prob
```js ```js
import { generateSecretKey, finalizeEvent, verifyEvent } from 'nostr-tools/pure' import { generateSecretKey, finalizeEvent, verifyEvent } from 'nostr-tools/pure'
import SimplePool from 'nostr-tools/pool-pure' import { SimplePool } from 'nostr-tools/pool-pure'
import Relay, { Subscription } from 'nostr-tools/relay-pure' import { Relay, Subscription } from 'nostr-tools/relay'
import { matchFilter } from 'nostr-tools/filter' import { matchFilter } from 'nostr-tools/filter'
import { decode, nprofileEncode, neventEncode, npubEncode } from 'nostr-tools/nip19' import { decode, nprofileEncode, neventEncode, npubEncode } from 'nostr-tools/nip19'
// and so on and so forth // and so on and so forth
@ -232,11 +232,18 @@ initNostrWasm().then(setNostrWasm)
// see https://www.npmjs.com/package/nostr-wasm for options // 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 ```js
import Relay, { Subscription } from 'nostr-tools/relay-wasm' import { setNostrWasm, verifyEvent } from 'nostr-tools/wasm'
import SimplePool from 'nostr-tools/pool-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`. This may be faster than the pure-JS [noble libraries](https://paulmillr.com/noble/) used by default and in `nostr-tools/pure`.

View File

@ -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 { normalizeURL } from './utils.ts'
import type { Event, Nostr } from './core.ts' import type { Event, Nostr } from './core.ts'
import { type Filter } from './filter.ts' import { type Filter } from './filter.ts'
import { alwaysTrue } from './helpers.ts'
export type SubCloser = { close: () => void } export type SubCloser = { close: () => void }
@ -12,25 +13,25 @@ export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose' | 'id'> & {
id?: string id?: string
} }
export default class TrustedSimplePool { export class AbstractSimplePool {
private relays = new Map<string, Relay>() private relays = new Map<string, AbstractRelay>()
public seenOn = new Map<string, Set<Relay>>() public seenOn = new Map<string, Set<AbstractRelay>>()
public trackRelays: boolean = false public trackRelays: boolean = false
public verifyEvent: Nostr['verifyEvent'] | undefined public verifyEvent: Nostr['verifyEvent']
public trustedRelayURLs = new Set<string>() public trustedRelayURLs = new Set<string>()
constructor(opts: { verifyEvent?: Nostr['verifyEvent'] } = {}) { constructor(opts: { verifyEvent: Nostr['verifyEvent'] }) {
this.verifyEvent = opts.verifyEvent this.verifyEvent = opts.verifyEvent
} }
async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<Relay> { async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<AbstractRelay> {
url = normalizeURL(url) url = normalizeURL(url)
let relay = this.relays.get(url) let relay = this.relays.get(url)
if (!relay) { if (!relay) {
relay = new Relay(url, { relay = new AbstractRelay(url, {
verifyEvent: this.trustedRelayURLs.has(url) ? undefined : this.verifyEvent, verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
}) })
if (params?.connectionTimeout) relay.connectionTimeout = params.connectionTimeout if (params?.connectionTimeout) relay.connectionTimeout = params.connectionTimeout
this.relays.set(url, relay) this.relays.set(url, relay)
@ -48,7 +49,7 @@ export default class TrustedSimplePool {
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser { subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
if (this.trackRelays) { if (this.trackRelays) {
params.receivedEvent = (relay: Relay, id: string) => { params.receivedEvent = (relay: AbstractRelay, id: string) => {
let set = this.seenOn.get(id) let set = this.seenOn.get(id)
if (!set) { if (!set) {
set = new Set() set = new Set()
@ -99,7 +100,7 @@ export default class TrustedSimplePool {
return return
} }
let relay: Relay let relay: AbstractRelay
try { try {
relay = await this.ensureRelay(url, { relay = await this.ensureRelay(url, {
connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined, connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined,

View File

@ -4,10 +4,10 @@ import type { Event, EventTemplate, Nostr } from './core.ts'
import { matchFilters, type Filter } from './filter.ts' import { matchFilters, type Filter } from './filter.ts'
import { getHex64, getSubscriptionId } from './fakejson.ts' import { getHex64, getSubscriptionId } from './fakejson.ts'
import { Queue, normalizeURL } from './utils.ts' import { Queue, normalizeURL } from './utils.ts'
import { nip42 } from './index.ts' import { makeAuthEvent } from './nip42.ts'
import { yieldThread } from './helpers.ts' import { yieldThread } from './helpers.ts'
export default class TrustedRelay { export class AbstractRelay {
public readonly url: string public readonly url: string
private _connected: boolean = false private _connected: boolean = false
@ -16,10 +16,10 @@ export default class TrustedRelay {
public baseEoseTimeout: number = 4400 public baseEoseTimeout: number = 4400
public connectionTimeout: number = 4400 public connectionTimeout: number = 4400
public openSubs = new Map<string, Subscription>()
private connectionTimeoutHandle: ReturnType<typeof setTimeout> | undefined private connectionTimeoutHandle: ReturnType<typeof setTimeout> | undefined
private connectionPromise: Promise<void> | undefined private connectionPromise: Promise<void> | undefined
private openSubs = new Map<string, Subscription>()
private openCountRequests = new Map<string, CountResolver>() private openCountRequests = new Map<string, CountResolver>()
private openEventPublishes = new Map<string, EventPublishResolver>() private openEventPublishes = new Map<string, EventPublishResolver>()
private ws: WebSocket | undefined private ws: WebSocket | undefined
@ -27,15 +27,15 @@ export default class TrustedRelay {
private queueRunning = false private queueRunning = false
private challenge: string | undefined private challenge: string | undefined
private serial: number = 0 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.url = normalizeURL(url)
this.verifyEvent = opts.verifyEvent this.verifyEvent = opts.verifyEvent
} }
static async connect(url: string, opts: { verifyEvent?: Nostr['verifyEvent'] } = {}) { static async connect(url: string, opts: { verifyEvent: Nostr['verifyEvent'] }) {
const relay = new TrustedRelay(url, opts) const relay = new AbstractRelay(url, opts)
await relay.connect() await relay.connect()
return relay return relay
} }
@ -163,7 +163,7 @@ export default class TrustedRelay {
case 'EVENT': { case 'EVENT': {
const so = this.openSubs.get(data[1] as string) as Subscription const so = this.openSubs.get(data[1] as string) as Subscription
const event = data[2] as Event 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) so.onevent(event)
} }
return return
@ -200,7 +200,6 @@ export default class TrustedRelay {
if (!so) return if (!so) return
so.closed = true so.closed = true
so.close(data[2] as string) so.close(data[2] as string)
this.openSubs.delete(id)
return return
} }
case 'NOTICE': case 'NOTICE':
@ -226,7 +225,7 @@ export default class TrustedRelay {
public async auth(signAuthEvent: (authEvent: EventTemplate) => Promise<void>) { public async auth(signAuthEvent: (authEvent: EventTemplate) => Promise<void>) {
if (!this.challenge) throw new Error("can't perform auth, no challenge was received") 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) await signAuthEvent(evt)
this.send('["AUTH",' + JSON.stringify(evt) + ']') this.send('["AUTH",' + JSON.stringify(evt) + ']')
} }
@ -268,17 +267,25 @@ export default class TrustedRelay {
this._connected = false this._connected = false
this.ws?.close() 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 { export class Subscription {
public readonly relay: TrustedRelay public readonly relay: AbstractRelay
public readonly id: string public readonly id: string
public closed: boolean = false public closed: boolean = false
public eosed: boolean = false public eosed: boolean = false
public filters: Filter[] public filters: Filter[]
public alreadyHaveEvent: ((id: string) => boolean) | undefined 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 onevent: (evt: Event) => void
public oneose: (() => void) | undefined public oneose: (() => void) | undefined
@ -287,7 +294,7 @@ export class Subscription {
public eoseTimeout: number public eoseTimeout: number
private eoseTimeoutHandle: ReturnType<typeof setTimeout> | undefined private eoseTimeoutHandle: ReturnType<typeof setTimeout> | undefined
constructor(relay: TrustedRelay, id: string, filters: Filter[], params: SubscriptionParams) { constructor(relay: AbstractRelay, id: string, filters: Filter[], params: SubscriptionParams) {
this.relay = relay this.relay = relay
this.filters = filters this.filters = filters
this.id = id this.id = id
@ -328,6 +335,7 @@ export class Subscription {
this.relay.send('["CLOSE",' + JSON.stringify(this.id) + ']') this.relay.send('["CLOSE",' + JSON.stringify(this.id) + ']')
this.closed = true this.closed = true
} }
this.relay.openSubs.delete(this.id)
this.onclose?.(reason) this.onclose?.(reason)
} }
} }
@ -337,7 +345,7 @@ export type SubscriptionParams = {
oneose?: () => void oneose?: () => void
onclose?: (reason: string) => void onclose?: (reason: string) => void
alreadyHaveEvent?: (id: string) => boolean alreadyHaveEvent?: (id: string) => boolean
receivedEvent?: (relay: TrustedRelay, id: string) => void receivedEvent?: (relay: AbstractRelay, id: string) => void
eoseTimeout?: number eoseTimeout?: number
} }

71
benchmark.ts Normal file
View File

@ -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<void>(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<string, { test: () => Promise<void>; 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)
}

View File

@ -10,6 +10,7 @@ const entryPoints = fs
file !== 'core.ts' && file !== 'core.ts' &&
file !== 'test-helpers.ts' && file !== 'test-helpers.ts' &&
file !== 'helpers.ts' && file !== 'helpers.ts' &&
file !== 'benchmarks.ts' &&
!file.endsWith('.test.ts') && !file.endsWith('.test.ts') &&
fs.statSync(join(process.cwd(), file)).isFile(), fs.statSync(join(process.cwd(), file)).isFile(),
) )

BIN
bun.lockb

Binary file not shown.

View File

@ -19,6 +19,7 @@ export interface Event {
[verifiedSymbol]?: boolean [verifiedSymbol]?: boolean
} }
export type NostrEvent = Event
export type EventTemplate = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at'> export type EventTemplate = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at'>
export type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'> export type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'>

View File

@ -1,3 +1,5 @@
import { verifiedSymbol, type Event, type Nostr } from './core.ts'
export async function yieldThread() { export async function yieldThread() {
return new Promise(resolve => { return new Promise(resolve => {
const ch = new MessageChannel() const ch = new MessageChannel()
@ -7,3 +9,8 @@ export async function yieldThread() {
ch.port1.start() ch.port1.start()
}) })
} }
export const alwaysTrue: Nostr['verifyEvent'] = (t: Event) => {
t[verifiedSymbol] = true
return t[verifiedSymbol]
}

View File

@ -1,4 +1,5 @@
{ {
"type": "module",
"name": "nostr-tools", "name": "nostr-tools",
"version": "2.1.0", "version": "2.1.0",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
@ -39,35 +40,25 @@
"require": "./lib/cjs/filter.js", "require": "./lib/cjs/filter.js",
"types": "./lib/types/filter.d.ts" "types": "./lib/types/filter.d.ts"
}, },
"./trusted-relay": { "./abstract-relay": {
"import": "./lib/esm/trusted-relay.js", "import": "./lib/esm/abstract-relay.js",
"require": "./lib/cjs/trusted-relay.js", "require": "./lib/cjs/abstract-relay.js",
"types": "./lib/types/trusted-relay.d.ts" "types": "./lib/types/abstract-relay.d.ts"
}, },
"./relay-wasm": { "./relay": {
"import": "./lib/esm/relay-wasm.js", "import": "./lib/esm/relay.js",
"require": "./lib/cjs/relay-wasm.js", "require": "./lib/cjs/relay.js",
"types": "./lib/types/relay-wasm.d.ts" "types": "./lib/types/relay.d.ts"
}, },
"./relay-pure": { "./abstract-pool": {
"import": "./lib/esm/relay-pure.js", "import": "./lib/esm/abstract-pool.js",
"require": "./lib/cjs/relay-pure.js", "require": "./lib/cjs/abstract-pool.js",
"types": "./lib/types/relay-pure.d.ts" "types": "./lib/types/abstract-pool.d.ts"
}, },
"./trusted-pool": { "./pool": {
"import": "./lib/esm/trusted-pool.js", "import": "./lib/esm/pool.js",
"require": "./lib/cjs/trusted-pool.js", "require": "./lib/cjs/pool.js",
"types": "./lib/types/trusted-pool.d.ts" "types": "./lib/types/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"
}, },
"./references": { "./references": {
"import": "./lib/esm/references.js", "import": "./lib/esm/references.js",
@ -183,6 +174,7 @@
"@scure/base": "1.1.1", "@scure/base": "1.1.1",
"@scure/bip32": "1.3.1", "@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1", "@scure/bip39": "1.2.1",
"mitata": "^0.1.6",
"nostr-wasm": "v0.0.3" "nostr-wasm": "v0.0.3"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -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'

View File

@ -1,7 +1,7 @@
import { verifyEvent } from './wasm.ts' 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() { constructor() {
super({ verifyEvent }) super({ verifyEvent })
} }

View File

@ -2,7 +2,7 @@ import { test, expect, afterAll } from 'bun:test'
import { finalizeEvent, type Event } from './pure.ts' import { finalizeEvent, type Event } from './pure.ts'
import { generateSecretKey, getPublicKey } from './pure.ts' import { generateSecretKey, getPublicKey } from './pure.ts'
import { SimplePool } from './pool.ts' import SimplePool from './pool-pure.ts'
let pool = new SimplePool() let pool = new SimplePool()

10
pool.ts Normal file
View File

@ -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'

View File

@ -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'

View File

@ -1,16 +1,16 @@
import { verifyEvent } from './pure.ts' 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) { constructor(url: string) {
super(url, { verifyEvent }) super(url, { verifyEvent })
} }
static async connect(url: string) { static async connect(url: string) {
const relay = new PureRelay(url) const relay = new Relay(url)
await relay.connect() await relay.connect()
return relay return relay
} }
} }
export * from './trusted-relay.ts' export * from './abstract-relay.ts'