Skip to content

Commit

Permalink
unsplit, backwards-compatibility, wasm relay and pool must be configu…
Browse files Browse the repository at this point in the history
…red manually from the abstract classes.
  • Loading branch information
fiatjaf committed Dec 21, 2023
1 parent a4ae964 commit 7f11c0c
Show file tree
Hide file tree
Showing 16 changed files with 162 additions and 89 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ package-lock.json
.envrc
lib
test.html
bench.js
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`.
Expand Down
23 changes: 12 additions & 11 deletions trusted-pool.ts → abstract-pool.ts
Original file line number Diff line number Diff line change
@@ -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 }

Expand All @@ -12,25 +13,25 @@ export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose' | 'id'> & {
id?: string
}

export default class TrustedSimplePool {
private relays = new Map<string, Relay>()
public seenOn = new Map<string, Set<Relay>>()
export class AbstractSimplePool {
private relays = new Map<string, AbstractRelay>()
public seenOn = new Map<string, Set<AbstractRelay>>()
public trackRelays: boolean = false

public verifyEvent: Nostr['verifyEvent'] | undefined
public verifyEvent: Nostr['verifyEvent']
public trustedRelayURLs = new Set<string>()

constructor(opts: { verifyEvent?: Nostr['verifyEvent'] } = {}) {
constructor(opts: { verifyEvent: Nostr['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)

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)
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
36 changes: 22 additions & 14 deletions trusted-relay.ts → abstract-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -16,26 +16,26 @@ export default class TrustedRelay {

public baseEoseTimeout: number = 4400
public connectionTimeout: number = 4400
public openSubs = new Map<string, Subscription>()
private connectionTimeoutHandle: ReturnType<typeof setTimeout> | undefined

private connectionPromise: Promise<void> | undefined
private openSubs = new Map<string, Subscription>()
private openCountRequests = new Map<string, CountResolver>()
private openEventPublishes = new Map<string, EventPublishResolver>()
private ws: WebSocket | undefined
private incomingMessageQueue = new Queue<string>()
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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand All @@ -226,7 +225,7 @@ export default class TrustedRelay {

public async auth(signAuthEvent: (authEvent: EventTemplate) => Promise<void>) {
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) + ']')
}
Expand Down Expand Up @@ -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
Expand All @@ -287,7 +294,7 @@ export class Subscription {
public eoseTimeout: number
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.filters = filters
this.id = id
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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
}

Expand Down
71 changes: 71 additions & 0 deletions benchmark.ts
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
Expand Down
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface Event {
[verifiedSymbol]?: boolean
}

export type NostrEvent = Event
export type EventTemplate = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at'>
export type UnsignedEvent = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'>

Expand Down
7 changes: 7 additions & 0 deletions helpers.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -7,3 +9,8 @@ export async function yieldThread() {
ch.port1.start()
})
}

export const alwaysTrue: Nostr['verifyEvent'] = (t: Event) => {
t[verifiedSymbol] = true
return t[verifiedSymbol]
}
44 changes: 18 additions & 26 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"type": "module",
"name": "nostr-tools",
"version": "2.1.0",
"description": "Tools for making a Nostr client.",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
Loading

0 comments on commit 7f11c0c

Please sign in to comment.