From 420a6910e9f3215da5e41b29d3f2d70226a12a0f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 17 Dec 2023 00:27:03 -0300 Subject: [PATCH] fix Queue, tweaks on relay.ts and make relay.test.ts pass. --- bun.lockb | Bin 137037 -> 129997 bytes nip42.test.ts | 7 +- package.json | 3 +- pool.test.ts | 1 - relay.test.ts | 173 +++++++++++++++++++---------------------------- relay.ts | 184 +++++++++++++++++++++++++++++--------------------- utils.test.ts | 15 ++-- utils.ts | 26 +++++-- 8 files changed, 207 insertions(+), 202 deletions(-) diff --git a/bun.lockb b/bun.lockb index 0bf875779f64e80cdf1dd5c1257d2f88b909ca6c..4fdf93f191ef8cd59cb2a49d98353593d5acd41b 100755 GIT binary patch delta 23942 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 K*ley{`2PV|0n { - const relay = relayInit('wss://nostr.wine') + const relay = relayConnect('wss://nostr.wine') const auth = makeAuthEvent(relay.url, 'chachacha') expect(auth.tags).toHaveLength(2) - expect(auth.tags[0]).toEqual(['relay', 'wss://nostr.wine']) + expect(auth.tags[0]).toEqual(['relay', 'wss://nostr.wine/']) expect(auth.tags[1]).toEqual(['challenge', 'chachacha']) expect(auth.kind).toEqual(22242) }) diff --git a/package.json b/package.json index ffd426f..d1ef8ed 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,6 @@ "node-fetch": "^2.6.9", "prettier": "^3.0.3", "tsd": "^0.22.0", - "typescript": "^5.0.4", - "websocket-polyfill": "^0.0.3" + "typescript": "^5.0.4" } } diff --git a/pool.test.ts b/pool.test.ts index e89b50b..176fb66 100644 --- a/pool.test.ts +++ b/pool.test.ts @@ -1,5 +1,4 @@ import { test, expect } from 'bun:test' -import 'websocket-polyfill' import { finishEvent, type Event } from './event.ts' import { generatePrivateKey, getPublicKey } from './keys.ts' diff --git a/relay.test.ts b/relay.test.ts index 32033ad..7e01a82 100644 --- a/relay.test.ts +++ b/relay.test.ts @@ -1,125 +1,99 @@ -import { test, expect } from 'bun:test' -import 'websocket-polyfill' +import { test, expect, afterEach, beforeEach } from 'bun:test' import { finishEvent } from './event.ts' import { generatePrivateKey, getPublicKey } from './keys.ts' -import { relayInit } from './relay.ts' +import { Relay } from './relay.ts' -let relay = relayInit('wss://relay.damus.io/') +let relay = new Relay('wss://public.relaying.io') -beforeAll(() => { +beforeEach(() => { relay.connect() }) -afterAll(() => { +afterEach(() => { relay.close() }) -test('connectivity', () => { - return expect( - new Promise(resolve => { - relay.on('connect', () => { - resolve(true) - }) - relay.on('error', () => { - resolve(false) - }) - }), - ).resolves.toBe(true) +test('connectivity', async () => { + await relay.connect() + expect(relay.connected).toBeTrue() }) test('querying', async () => { - var resolve1: (value: boolean) => void - var resolve2: (value: boolean) => void + let resolve1: () => void + let resolve2: () => void - let sub = relay.sub([ - { - ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'], - }, - ]) - sub.on('event', event => { - expect(event).toHaveProperty('id', 'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027') - resolve1(true) - }) - sub.on('eose', () => { - resolve2(true) - }) - - let [t1, t2] = await Promise.all([ - new Promise(resolve => { + let waiting = Promise.all([ + new Promise(resolve => { resolve1 = resolve }), - new Promise(resolve => { + new Promise(resolve => { resolve2 = resolve }), ]) - expect(t1).toEqual(true) - expect(t2).toEqual(true) + relay.subscribe( + [ + { + ids: ['3abc6cbb215af0412ab2c9c8895d96a084297890fd0b4018f8427453350ca2e4'], + }, + ], + { + onevent(event) { + expect(event).toHaveProperty('id', '3abc6cbb215af0412ab2c9c8895d96a084297890fd0b4018f8427453350ca2e4') + expect(event).toHaveProperty('content', '+') + expect(event).toHaveProperty('kind', 7) + resolve1() + }, + oneose() { + resolve2() + }, + }, + ) + + let [t1, t2] = await waiting + expect(t1).toBeUndefined() + expect(t2).toBeUndefined() }, 10000) -test('async iterator', async () => { - let sub = relay.sub([ - { - ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'], - }, - ]) - - for await (const event of sub.events) { - expect(event).toHaveProperty('id', 'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027') - break - } -}) - -test('get()', async () => { - let event = await relay.get({ - ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'], - }) - - expect(event).toHaveProperty('id', 'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027') -}) - -test('list()', async () => { - let events = await relay.list([ - { - authors: ['3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'], - kinds: [1], - limit: 2, - }, - ]) - - expect(events.length).toEqual(2) -}) - -test('listening (twice) and publishing', async () => { +test('listening and publishing and closing', async () => { let sk = generatePrivateKey() let pk = getPublicKey(sk) - var resolve1: (value: boolean) => void - var resolve2: (value: boolean) => void + var resolve1: (_: void) => void + var resolve2: (_: void) => void - let sub = relay.sub([ - { - kinds: [27572], - authors: [pk], - }, + let waiting = Promise.all([ + new Promise(resolve => { + resolve1 = resolve + }), + new Promise(resolve => { + resolve2 = resolve + }), ]) - sub.on('event', event => { - expect(event).toHaveProperty('pubkey', pk) - expect(event).toHaveProperty('kind', 27572) - expect(event).toHaveProperty('content', 'nostr-tools test suite') - resolve1(true) - }) - sub.on('event', event => { - expect(event).toHaveProperty('pubkey', pk) - expect(event).toHaveProperty('kind', 27572) - expect(event).toHaveProperty('content', 'nostr-tools test suite') - resolve2(true) - }) + let sub = await relay.subscribe( + [ + { + kinds: [23571], + authors: [pk], + }, + ], + { + onevent(event) { + expect(event).toHaveProperty('pubkey', pk) + expect(event).toHaveProperty('kind', 23571) + expect(event).toHaveProperty('content', 'nostr-tools test suite') + resolve1() + }, + onclose() { + resolve2() + }, + }, + ) let event = finishEvent( { - kind: 27572, + kind: 23571, created_at: Math.floor(Date.now() / 1000), tags: [], content: 'nostr-tools test suite', @@ -127,15 +101,10 @@ test('listening (twice) and publishing', async () => { sk, ) - relay.publish(event) - return expect( - Promise.all([ - new Promise(resolve => { - resolve1 = resolve - }), - new Promise(resolve => { - resolve2 = resolve - }), - ]), - ).resolves.toEqual([true, true]) + await relay.publish(event) + sub.close() + + let [t1, t2] = await waiting + expect(t1).toBeUndefined() + expect(t2).toBeUndefined() }) diff --git a/relay.ts b/relay.ts index 0011a98..82df7da 100644 --- a/relay.ts +++ b/relay.ts @@ -12,68 +12,13 @@ export function relayConnect(url: string) { return relay } -class Subscription { - public readonly relay: Relay - public readonly id: string - public closed: boolean = false - public eosed: boolean = false - - public alreadyHaveEvent: ((id: string) => boolean) | null = null - public receivedEvent: ((id: string) => boolean) | null = null - public readonly filters: Filter[] - - public onevent: (evt: Event) => void - public oneose: (() => void) | null = null - public onclose: ((reason: string) => void) | null = null - - constructor(relay: Relay, filters: Filter[], params: SubscriptionParams) { - this.relay = relay - this.filters = filters - this.id = params.id - this.onevent = params.onevent - this.oneose = params.oneose || null - this.onclose = params.onclose || null - this.alreadyHaveEvent = params.alreadyHaveEvent || null - this.receivedEvent = params.receivedEvent || null - } - - public close(reason: string) { - if (!this.closed) { - // if the connection was closed by the user calling .close() we will send a CLOSE message - // otherwise this._open will be already set to false so we will skip this - this.relay.send('["CLOSE",' + JSON.stringify(this.id) + ']') - this.closed = true - } - this.onclose?.(reason) - } -} - -type SubscriptionParams = { - id: string - onevent: (evt: Event) => void - oneose?: () => void - onclose?: (reason: string) => void - alreadyHaveEvent: ((id: string) => boolean) | null - receivedEvent: ((id: string) => boolean) | null -} - -type CountResolver = { - resolve: (count: number) => void - reject: (err: Error) => void -} - -type EventPublishResolver = { - resolve: (reason: string) => void - reject: (err: Error) => void -} - -class Relay { +export class Relay { public readonly url: string private _connected: boolean = false public trusted: boolean = false public onclose: (() => void) | null = null - public onnotice: (msg: string) => void = console.log + public onnotice: (msg: string) => void = msg => console.log(`NOTICE from ${this.url}: ${msg}`) private connectionPromise: Promise | undefined private openSubs = new Map() @@ -81,7 +26,7 @@ class Relay { private openEventPublishes = new Map() private ws: WebSocket | undefined private incomingMessageQueue = new Queue() - private handleNextInterval: ReturnType | null = null + private queueRunning = false private challenge: string | undefined private serial: number = 0 @@ -112,6 +57,8 @@ class Relay { public async connect(): Promise { if (this.connectionPromise) return this.connectionPromise + + this.challenge = undefined this.connectionPromise = new Promise((resolve, reject) => { try { this.ws = new WebSocket(this.url) @@ -125,8 +72,8 @@ class Relay { resolve() } - this.ws.onerror = () => { - reject() + this.ws.onerror = ev => { + reject((ev as any).message) if (this._connected) { this.onclose?.() this.closeAllSubscriptions('relay connection errored') @@ -143,19 +90,30 @@ class Relay { this.ws.onmessage = ev => { this.incomingMessageQueue.enqueue(ev.data as string) - if (!this.handleNextInterval) { - this.handleNextInterval = setInterval(this.handleNext.bind(this), 0) + if (!this.queueRunning) { + this.runQueue() } } }) + + return this.connectionPromise } - private handleNext() { + private async runQueue() { + this.queueRunning = true + while (true) { + if (false === this.handleNext()) { + break + } + await Promise.resolve() + } + this.queueRunning = false + } + + private handleNext(): undefined | false { const json = this.incomingMessageQueue.dequeue() if (!json) { - clearInterval(this.handleNextInterval as ReturnType) - this.handleNextInterval = null - return + return false } const subid = getSubscriptionId(json) @@ -249,36 +207,106 @@ class Relay { if (!this.challenge) throw new Error("can't perform auth, no challenge was received") const evt = nip42.makeAuthEvent(this.url, this.challenge) await Promise.all([signAuthEvent(evt), this.connect()]) - this.ws?.send('["AUTH",' + JSON.stringify(evt) + ']') + this.send('["AUTH",' + JSON.stringify(evt) + ']') } - public async publish(event: Event) { + public async publish(event: Event): Promise { await this.connect() - const ret = new Promise((resolve, reject) => { + const ret = new Promise((resolve, reject) => { this.openEventPublishes.set(event.id, { resolve, reject }) }) - this.ws?.send('["EVENT",' + JSON.stringify(event) + ']') + this.send('["EVENT",' + JSON.stringify(event) + ']') return ret } - public async count(filters: Filter[], params: { id?: string | null }) { + public async count(filters: Filter[], params: { id?: string | null }): Promise { await this.connect() this.serial++ const id = params?.id || 'count:' + this.serial - const ret = new Promise((resolve, reject) => { + const ret = new Promise((resolve, reject) => { this.openCountRequests.set(id, { resolve, reject }) }) - this.ws?.send('["COUNT","' + id + '"' + JSON.stringify(filters) + ']') + this.send('["COUNT","' + id + '",' + JSON.stringify(filters) + ']') return ret } - public async subscribe(filters: Filter[], params: SubscriptionParams & { id: string | undefined }) { + public async subscribe(filters: Filter[], params: Partial) { await this.connect() this.serial++ - params.id = params.id || 'sub:' + this.serial - const subscription = new Subscription(this, filters, params) - this.openSubs.set(params.id, subscription) - this.ws?.send('["REQ","' + params.id + '"' + JSON.stringify(filters) + ']') + const id = params.id || 'sub:' + this.serial + const subscription = new Subscription(this, filters, { + onevent: event => { + console.warn( + `onevent() callback not defined for subscription '${id}' in relay ${this.url}. event received:`, + event, + ) + }, + ...params, + id, + }) + this.openSubs.set(id, subscription) + this.send('["REQ","' + id + '",' + JSON.stringify(filters).substring(1)) return subscription } + + public close() { + this.closeAllSubscriptions('relay connection closed by us') + this._connected = false + this.ws?.close() + } +} + +export class Subscription { + public readonly relay: Relay + public readonly id: string + public closed: boolean = false + public eosed: boolean = false + + public alreadyHaveEvent: ((id: string) => boolean) | undefined + public receivedEvent: ((id: string) => boolean) | undefined + public readonly filters: Filter[] + + public onevent: (evt: Event) => void + public oneose: (() => void) | undefined + public onclose: ((reason: string) => void) | undefined + + constructor(relay: Relay, filters: Filter[], params: SubscriptionParams) { + this.relay = relay + this.filters = filters + this.id = params.id + this.onevent = params.onevent + this.oneose = params.oneose + this.onclose = params.onclose + this.alreadyHaveEvent = params.alreadyHaveEvent + this.receivedEvent = params.receivedEvent + } + + public close(reason: string = 'closed by caller') { + if (!this.closed) { + // if the connection was closed by the user calling .close() we will send a CLOSE message + // otherwise this._open will be already set to false so we will skip this + this.relay.send('["CLOSE",' + JSON.stringify(this.id) + ']') + this.closed = true + } + this.onclose?.(reason) + } +} + +export type SubscriptionParams = { + id: string + onevent: (evt: Event) => void + oneose?: () => void + onclose?: (reason: string) => void + alreadyHaveEvent?: (id: string) => boolean + receivedEvent?: (id: string) => boolean +} + +export type CountResolver = { + resolve: (count: number) => void + reject: (err: Error) => void +} + +export type EventPublishResolver = { + resolve: (reason: string) => void + reject: (err: Error) => void } diff --git a/utils.test.ts b/utils.test.ts index 4335c13..4a3d141 100644 --- a/utils.test.ts +++ b/utils.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'bun:test' import { buildEvent } from './test-helpers.ts' -import { MessageQueue, insertEventIntoAscendingList, insertEventIntoDescendingList } from './utils.ts' +import { Queue, insertEventIntoAscendingList, insertEventIntoDescendingList } from './utils.ts' import type { Event } from './event.ts' @@ -216,27 +216,25 @@ describe('inserting into a asc sorted list of events', () => { describe('enque a message into MessageQueue', () => { test('enque into an empty queue', () => { - const queue = new MessageQueue() + const queue = new Queue() queue.enqueue('node1') expect(queue.first!.value).toBe('node1') }) test('enque into a non-empty queue', () => { - const queue = new MessageQueue() + const queue = new Queue() queue.enqueue('node1') queue.enqueue('node3') queue.enqueue('node2') expect(queue.first!.value).toBe('node1') expect(queue.last!.value).toBe('node2') - expect(queue.size).toBe(3) }) test('dequeue from an empty queue', () => { - const queue = new MessageQueue() + const queue = new Queue() const item1 = queue.dequeue() expect(item1).toBe(null) - expect(queue.size).toBe(0) }) test('dequeue from a non-empty queue', () => { - const queue = new MessageQueue() + const queue = new Queue() queue.enqueue('node1') queue.enqueue('node3') queue.enqueue('node2') @@ -246,14 +244,13 @@ describe('enque a message into MessageQueue', () => { expect(item2).toBe('node3') }) test('dequeue more than in queue', () => { - const queue = new MessageQueue() + const queue = new Queue() queue.enqueue('node1') queue.enqueue('node3') const item1 = queue.dequeue() expect(item1).toBe('node1') const item2 = queue.dequeue() expect(item2).toBe('node3') - expect(queue.size).toBe(0) const item3 = queue.dequeue() expect(item3).toBe(null) }) diff --git a/utils.ts b/utils.ts index cce4f6b..ab6ddd4 100644 --- a/utils.ts +++ b/utils.ts @@ -94,11 +94,11 @@ export function insertEventIntoAscendingList(sortedArray: Event[], event: Event) export class QueueNode { public value: V - public next: QueueNode | null + public next: QueueNode | null = null + public prev: QueueNode | null = null constructor(message: V) { this.value = message - this.next = null } } @@ -114,9 +114,17 @@ export class Queue { enqueue(value: V): boolean { const newNode = new QueueNode(value) if (!this.last) { + // list is empty this.first = newNode this.last = newNode + } else if (this.last === this.first) { + // list has a single element + this.last = newNode + this.last.prev = this.first + this.first.next = newNode } else { + // list has elements, add as last + newNode.prev = this.last this.last.next = newNode this.last = newNode } @@ -126,10 +134,16 @@ export class Queue { dequeue(): V | null { if (!this.first) return null - let prev = this.first - this.first = prev.next - prev.next = null + if (this.first === this.last) { + const target = this.first + this.first = null + this.last = null + return target.value + } - return prev.value + const target = this.first + this.first = target.next + + return target.value } }