From 53b0091bf47e9c6adfa5339e4721666fca499656 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Dec 2022 15:24:54 -0300 Subject: [PATCH] some fixes on relay.ts and tests. --- .gitignore | 1 + build.cjs | 11 ++--- relay.test.js | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++ relay.ts | 74 +++++++++++++++++-------------- 4 files changed, 166 insertions(+), 37 deletions(-) create mode 100644 relay.test.js diff --git a/.gitignore b/.gitignore index f86ec31..e14ae2f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ package-lock.json standalone cjs esm +test.html diff --git a/build.cjs b/build.cjs index a6a625b..1b0999c 100755 --- a/build.cjs +++ b/build.cjs @@ -11,10 +11,6 @@ let common = { stream: require.resolve('readable-stream') }) ], - define: { - window: 'self', - global: 'self' - }, sourcemap: 'external' } @@ -31,6 +27,11 @@ esbuild ...common, outdir: 'standalone/', format: 'iife', - globalName: 'NostrTools' + globalName: 'NostrTools', + define: { + window: 'self', + global: 'self', + process: '{"env": {}}' + } }) .then(() => console.log('standalone build success.')) diff --git a/relay.test.js b/relay.test.js new file mode 100644 index 0000000..222ec5d --- /dev/null +++ b/relay.test.js @@ -0,0 +1,117 @@ +/* eslint-env jest */ + +const { + relayInit, + generatePrivateKey, + getPublicKey, + getEventHash, + signEvent +} = require('./cjs') + +describe('relay interaction', () => { + let relay = relayInit('wss://nostr-pub.wellorder.net/') + + beforeAll(() => { + relay.connect() + }) + + afterAll(async () => { + await relay.close() + }) + + test('connectivity', () => { + return expect( + new Promise(resolve => { + relay.on('connect', () => { + resolve(true) + }) + relay.on('error', () => { + resolve(false) + }) + }) + ).resolves.toBe(true) + }) + + test('querying', () => { + var resolve1 + var resolve2 + + let sub = relay.sub([ + { + ids: [ + 'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027' + ] + } + ]) + sub.on('event', event => { + expect(event).toHaveProperty( + 'id', + 'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027' + ) + resolve1(true) + }) + sub.on('eose', () => { + resolve2(true) + }) + + return expect( + Promise.all([ + new Promise(resolve => { + resolve1 = resolve + }), + new Promise(resolve => { + resolve2 = resolve + }) + ]) + ).resolves.toEqual([true, true]) + }) + + test('listening (twice) and publishing', async () => { + let sk = generatePrivateKey() + let pk = getPublicKey(sk) + var resolve1 + var resolve2 + + let sub = relay.sub([ + { + kinds: [27572], + authors: [pk] + } + ]) + + 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 event = { + kind: 27572, + pubkey: pk, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: 'nostr-tools test suite' + } + event.id = getEventHash(event) + event.sig = await signEvent(event, sk) + + relay.publish(event) + return expect( + Promise.all([ + new Promise(resolve => { + resolve1 = resolve + }), + new Promise(resolve => { + resolve2 = resolve + }) + ]) + ).resolves.toEqual([true, true]) + }) +}) diff --git a/relay.ts b/relay.ts index bc021f4..708adac 100644 --- a/relay.ts +++ b/relay.ts @@ -5,20 +5,12 @@ import 'websocket-polyfill' import {Event, verifySignature, validateEvent} from './event' import {Filter, matchFilters} from './filter' -export function normalizeRelayURL(url: string): string { - let [host, ...qs] = url.trim().split('?') - if (host.slice(0, 4) === 'http') host = 'ws' + host.slice(4) - if (host.slice(0, 2) !== 'ws') host = 'wss://' + host - if (host.length && host[host.length - 1] === '/') host = host.slice(0, -1) - return [host, ...qs].join('?') -} - export type Relay = { url: string status: number connect: () => void close: () => void - sub: (opts: SubscriptionOptions) => Sub + sub: (filters: Filter[], opts: SubscriptionOptions) => Sub publish: (event: Event) => Pub on: (type: 'connect' | 'disconnect' | 'notice', cb: any) => void off: (type: 'connect' | 'disconnect' | 'notice', cb: any) => void @@ -28,27 +20,25 @@ export type Pub = { off: (type: 'ok' | 'seen' | 'failed', cb: any) => void } export type Sub = { - sub: (opts: SubscriptionOptions) => Sub + sub: (filters: Filter[], opts: SubscriptionOptions) => Sub unsub: () => void on: (type: 'event' | 'eose', cb: any) => void off: (type: 'event' | 'eose', cb: any) => void } type SubscriptionOptions = { - filters: Filter[] skipVerification?: boolean id?: string } export function relayInit(url: string): Relay { - let relay = normalizeRelayURL(url) // set relay url - var ws: WebSocket var resolveOpen: () => void + var resolveClose: () => void var untilOpen: Promise var wasClosed: boolean var closed: boolean - var openSubs: {[id: string]: SubscriptionOptions} = {} + var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {} var listeners: { connect: Array<() => void> disconnect: Array<() => void> @@ -65,16 +55,17 @@ export function relayInit(url: string): Relay { event: Array<(event: Event) => void> eose: Array<() => void> } - } + } = {} var pubListeners: { [eventid: string]: { ok: Array<() => void> seen: Array<() => void> failed: Array<(reason: string) => void> } - } + } = {} let attemptNumber = 1 let nextAttemptSeconds = 1 + let isConnected = false function resetOpenState() { untilOpen = new Promise(resolve => { @@ -83,26 +74,37 @@ export function relayInit(url: string): Relay { } function connectRelay() { - ws = new WebSocket(relay) + ws = new WebSocket(url) ws.onopen = () => { listeners.connect.forEach(cb => cb()) resolveOpen() + isConnected = true // restablish old subscriptions if (wasClosed) { wasClosed = false for (let id in openSubs) { - sub(openSubs[id]) + let {filters} = openSubs[id] + sub(filters, openSubs[id]) } } } ws.onerror = () => { + isConnected = false listeners.error.forEach(cb => cb()) } ws.onclose = async () => { + isConnected = false listeners.disconnect.forEach(cb => cb()) - if (closed) return + + if (closed) { + // we've closed this because we wanted, so end everything + resolveClose() + return + } + + // otherwise keep trying to reconnect resetOpenState() attemptNumber++ nextAttemptSeconds += attemptNumber ** 3 @@ -110,7 +112,7 @@ export function relayInit(url: string): Relay { nextAttemptSeconds = 14400 // 4 hours } console.log( - `relay ${relay} connection closed. reconnecting in ${nextAttemptSeconds} seconds.` + `relay ${url} connection closed. reconnecting in ${nextAttemptSeconds} seconds.` ) setTimeout(async () => { try { @@ -187,11 +189,13 @@ export function relayInit(url: string): Relay { ws.send(msg) } - const sub = ({ - filters, - skipVerification = false, - id = Math.random().toString().slice(2) - }: SubscriptionOptions): Sub => { + const sub = ( + filters: Filter[], + { + skipVerification = false, + id = Math.random().toString().slice(2) + }: SubscriptionOptions = {} + ): Sub => { let subid = id openSubs[subid] = { @@ -202,10 +206,11 @@ export function relayInit(url: string): Relay { trySend(['REQ', subid, ...filters]) return { - sub: ({ - filters = openSubs[subid].filters, - skipVerification = openSubs[subid].skipVerification - }) => sub({filters, skipVerification, id: subid}), + sub: (newFilters, newOpts = {}) => + sub(newFilters || filters, { + skipVerification: newOpts.skipVerification || skipVerification, + id: subid + }), unsub: () => { delete openSubs[subid] delete subListeners[subid] @@ -230,6 +235,9 @@ export function relayInit(url: string): Relay { sub, on: (type: 'connect' | 'disconnect' | 'notice', cb: any): void => { listeners[type].push(cb) + if (type === 'connect' && isConnected) { + cb() + } }, off: (type: 'connect' | 'disconnect' | 'notice', cb: any): void => { let index = listeners[type].indexOf(cb) @@ -253,8 +261,7 @@ export function relayInit(url: string): Relay { .catch(() => {}) const startMonitoring = () => { - let monitor = sub({ - filters: [{ids: [id]}], + let monitor = sub([{ids: [id]}], { id: `monitor-${id.slice(0, 5)}` }) let willUnsub = setTimeout(() => { @@ -290,9 +297,12 @@ export function relayInit(url: string): Relay { } }, connect, - close() { + close(): Promise { closed = true // prevent ws from trying to reconnect ws.close() + return new Promise(resolve => { + resolveClose = resolve + }) }, get status() { return ws.readyState