Compare commits

..

34 Commits

Author SHA1 Message Date
fiatjaf
086f8830e3 catch fetch error on nip05. 2023-03-02 08:21:17 -03:00
Egge
e48d722227 Fixed readme for publishing with pool 2023-03-01 15:29:24 -03:00
Fernando López Guevara
0d77013aab chore(dx): add format script 💅 (#128) 2023-02-28 12:19:10 -03:00
BilligsterUser
4c415280aa ci emit types on publish 2023-02-27 22:17:52 -03:00
fiatjaf
4188aaf7c8 just type-check 2023-02-27 19:53:12 -03:00
BilligsterUser
673f4abab8 add type definition
fixes #138
2023-02-27 19:51:19 -03:00
BilligsterUser
bcefaa0757 update repo url 2023-02-27 19:49:00 -03:00
fiatjaf
649af36a86 one more nip19 test. 2023-02-27 16:10:26 -03:00
Simon
96a6f7af87 ensure kind has type 'number' in validateEvent 2023-02-27 14:22:36 -03:00
fiatjaf
a4c713efcb fix readme example.
fixes https://github.com/nbd-wtf/nostr-tools/issues/136
2023-02-27 12:43:24 -03:00
fiatjaf
9d345a8f01 configurable list and get timeout on relay. 2023-02-26 21:23:09 -03:00
fiatjaf
c362212778 make pool.publish() return a single Pub object. 2023-02-26 17:44:51 -03:00
fiatjaf
a8938a3a0f wait a second before failing to send on a not yet connected websocket. 2023-02-26 16:53:03 -03:00
fiatjaf
a21329da3f make timeouts configurable for pool. 2023-02-26 16:50:49 -03:00
fiatjaf
63f4a49a69 increase pool timeouts. 2023-02-26 15:05:26 -03:00
fiatjaf
27749d91b8 fix nip19: relays TLV items are optional. 2023-02-26 07:44:22 -03:00
michaelhall923
9530849f0a Fix pool sub example 2023-02-26 07:43:35 -03:00
fiatjaf
b8aa75b6e1 change a map to a forEach. 2023-02-24 09:41:26 -03:00
fiatjaf
344762820c handle connection failure on pool according to @chmac.
fixes https://github.com/nbd-wtf/nostr-tools/issues/130
2023-02-24 09:34:23 -03:00
Fernando López Guevara
f43d23d344 fix(relay): prevent accesing to ws if it is undefined 2023-02-23 15:20:10 -03:00
fiatjaf
bf55ad6b5a bump to v1.6.0 2023-02-20 22:51:49 -03:00
Moe Jangda
04a46b815c include the exports property in the root package.json to allow node environments to use cjs or esm bundles 2023-02-19 20:54:55 -03:00
Moe Jangda
165ff44dff include package.json with type: module near esm bundle so that it's usable 2023-02-19 20:54:55 -03:00
BilligsterUser
7bfd23af3c update close() usage
Signed-off-by: BilligsterUser <billigsteruser@protonmail.com>
2023-02-17 21:07:59 -03:00
fiatjaf
3d93ec8446 remove resolveClose, close() is now fire-and-forget. 2023-02-17 14:51:56 -03:00
fiatjaf
0f841138cd bump to v1.5.0 2023-02-17 14:31:08 -03:00
fiatjaf
336948b1d1 zap request validator. 2023-02-17 14:30:49 -03:00
Callum Macdonald
d46794c681 Also fix signEvent() type. 2023-02-17 14:30:27 -03:00
Callum Macdonald
93cef5d886 Type unsigned events. fix #117 2023-02-17 14:30:27 -03:00
fiatjaf
2324f9548e fail on null amount on zaprequest creation. 2023-02-16 13:56:47 -03:00
Roland Bewick
f9748d9cc3 doc: fix order of commands to connect to relay 2023-02-16 12:35:13 -03:00
Roland Bewick
3a22dd3da6 doc: add installation instructions 2023-02-16 12:29:09 -03:00
fiatjaf
d13039dc11 finishEvent() takes an EventTemplate and returns an Event. 2023-02-16 11:58:17 -03:00
fiatjaf
95b03902cc add support for naddr. 2023-02-16 11:27:50 -03:00
18 changed files with 318 additions and 241 deletions

View File

@@ -16,6 +16,7 @@ jobs:
- run: just install-dependencies
- run: just build
- run: just test
- run: just emit-types
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}

View File

@@ -4,6 +4,12 @@ Tools for developing [Nostr](https://github.com/fiatjaf/nostr) clients.
Only depends on _@scure_ and _@noble_ packages.
## Installation
```bash
npm install nostr-tools # or yarn add nostr-tools
```
## Usage
### Generating a private key and a public key
@@ -53,8 +59,6 @@ import {
} from 'nostr-tools'
const relay = relayInit('wss://relay.example.com')
await relay.connect()
relay.on('connect', () => {
console.log(`connected to ${relay.url}`)
})
@@ -62,6 +66,8 @@ relay.on('error', () => {
console.log(`failed to connect to ${relay.url}`)
})
await relay.connect()
// let's query for an event that exists
let sub = relay.sub([
{
@@ -113,7 +119,7 @@ let event = await relay.get({
ids: ['44e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245']
})
await relay.close()
relay.close()
```
To use this on Node.js you first must install `websocket-polyfill` and import it:
@@ -131,25 +137,27 @@ const pool = new SimplePool()
let relays = ['wss://relay.example.com', 'wss://relay.example2.com']
let relay = await pool.ensureRelay('wss://relay.example3.com')
let sub = pool.sub(
[...relays, 'wss://relay.example3.com'],
[
{
authors: [
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
]
}
]
)
let subs = pool.sub([...relays, relay], {
authors: ['32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245']
sub.on('event', event => {
// this will only be called once the first time the event is received
// ...
})
subs.forEach(sub =>
sub.on('event', event => {
// this will only be called once the first time the event is received
// ...
})
)
let pubs = pool.publish(relays, newEvent)
pubs.forEach(pub =>
pub.on('ok', () => {
// ...
})
)
pubs.on('ok', () => {
// this may be called multiple times, once for every relay that accepts the event
// ...
})
let events = await pool.list(relays, [{kinds: [0, 1]}])
let event = await pool.get(relays, {

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
const fs = require('fs')
const esbuild = require('esbuild')
let common = {
@@ -11,11 +12,16 @@ let common = {
esbuild
.build({
...common,
outfile: 'lib/nostr.esm.js',
outfile: 'lib/esm/nostr.mjs',
format: 'esm',
packages: 'external'
})
.then(() => console.log('esm build success.'))
.then(() => {
const packageJson = JSON.stringify({type: 'module'})
fs.writeFileSync(`${__dirname}/lib/esm/package.json`, packageJson, 'utf8')
console.log('esm build success.')
})
esbuild
.build({

View File

@@ -2,6 +2,7 @@ import * as secp256k1 from '@noble/secp256k1'
import {sha256} from '@noble/hashes/sha256'
import {utf8Encoder} from './utils'
import {getPublicKey} from './keys'
/* eslint-disable no-unused-vars */
export enum Kind {
@@ -32,8 +33,11 @@ export type EventTemplate = {
created_at: number
}
export type Event = EventTemplate & {
export type UnsignedEvent = EventTemplate & {
pubkey: string
}
export type Event = UnsignedEvent & {
id: string
sig: string
}
@@ -47,7 +51,15 @@ export function getBlankEvent(): EventTemplate {
}
}
export function serializeEvent(evt: Event): string {
export function finishEvent(t: EventTemplate, privateKey: string): Event {
let event = t as Event
event.pubkey = getPublicKey(privateKey)
event.id = getEventHash(event)
event.sig = signEvent(event, privateKey)
return event
}
export function serializeEvent(evt: UnsignedEvent): string {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties")
@@ -61,13 +73,14 @@ export function serializeEvent(evt: Event): string {
])
}
export function getEventHash(event: Event): string {
export function getEventHash(event: UnsignedEvent): string {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)))
return secp256k1.utils.bytesToHex(eventHash)
}
export function validateEvent(event: Event): boolean {
export function validateEvent(event: UnsignedEvent): boolean {
if (typeof event !== 'object') return false
if (typeof event.kind !== 'number') return false
if (typeof event.content !== 'string') return false
if (typeof event.created_at !== 'number') return false
if (typeof event.pubkey !== 'string') return false
@@ -93,7 +106,7 @@ export function verifySignature(event: Event & {sig: string}): boolean {
)
}
export function signEvent(event: Event, key: string): string {
export function signEvent(event: UnsignedEvent, key: string): string {
return secp256k1.utils.bytesToHex(
secp256k1.schnorr.signSync(getEventHash(event), key)
)

View File

@@ -9,8 +9,15 @@ build:
test: build
jest
testOnly file: build
test-only file: build
jest {{file}}
emit-types:
tsc
publish: build
emit-types
npm publish
format:
prettier --plugin-search-dir . --write .

140
magic.ts
View File

@@ -1,140 +0,0 @@
import {Relay, relayInit} from './relay'
import {Event} from './event'
import {normalizeURL} from './utils'
export default function (
writeableRelays: string[],
fallbackRelays: string[],
safeRelays: string[]
) {
return new MagicPool(fallbackRelays, writeableRelays, safeRelays)
}
class MagicPool {
private _conn: {[url: string]: Relay}
private _fallback: {[url: string]: Relay}
private _write: {[url: string]: Relay}
private _safe: {[url: string]: Relay}
private _profileRelays: {[pubkey: string]: RelayTableScore}
private _tempCache: {[id: string]: Event}
constructor(
fallbackRelays: string[],
writeableRelays: string[],
safeRelays: string[] = [
'wss://eden.nostr.land',
'wss://nostr.milou.lol',
'wss://relay.minds.com/nostr/v1/ws'
]
) {
this._conn = {}
this._write = {}
this._fallback = {}
this._profileRelays = {}
this._tempCache = {}
const hasEventId = (id: string): boolean => id in this._tempCache
const init = (url: string) => {
this._conn[normalizeURL(url)] = relayInit(normalizeURL(url), hasEventId)
}
fallbackRelays.forEach(init)
writeableRelays.forEach(init)
safeRelays.forEach(init)
this._write = Object.fromEntries(
writeableRelays.map(url => [
normalizeURL(url),
this._conn[normalizeURL(url)]
])
)
this._fallback = Object.fromEntries(
fallbackRelays.map(url => [
normalizeURL(url),
this._conn[normalizeURL(url)]
])
)
this._safe = Object.fromEntries(
safeRelays.map(url => [normalizeURL(url), this._conn[normalizeURL(url)]])
)
}
publish(event: Event) {
return Promise.all(
Object.entries(this._write).map(
([url, relay]) =>
new Promise(async resolve => {
await relay.connect()
let pub = relay.publish(event)
let to = setTimeout(() => {
let end = setTimeout(() => {
resolve({url, success: false, reason: 'timeout'})
}, 2500)
pub.on('seen', () => {
clearTimeout(end)
resolve({url, success: true, reason: 'seen'})
})
}, 2500)
pub.on('ok', () => {
clearTimeout(to)
resolve({url, success: true, reason: 'ok'})
})
pub.on('failed', (reason: string) => {
clearTimeout(to)
resolve({url, success: false, reason})
})
})
)
)
}
profile(
pubkey: string,
onUpdate: (events: Event[]) => void
): {
page(n: number): void
} {
var relays = new Set()
let rts = this._profileRelays[pubkey]
if (rts) {
relays = rts.get(3)
}
let fallback = Object.values(this._fallback)
for (let i = 0; i < fallback.length; i++) {
if (relays.size < 3) {
relays.add(fallback[Math.floor(Math.random() * fallback.length)])
} else break
}
// start subscription
for (let r in relays) {
r.
}
return {
page(n: number) {}
}
}
}
class RelayTableScore {
seen: string[] = []
hinted: string[] = []
explicit: string[] = []
get(n: number): Set<string> {
let relays = new Set<string>()
for (let i = 0; i < n; i++) {
for (let j = 0; j < 3; j++) {
let v = [this.seen, this.explicit, this.hinted][j][i]
if (v) {
relays.add(v)
if (relays.size >= n) return relays
}
}
}
return relays
}
}

View File

@@ -38,9 +38,14 @@ export async function queryProfile(
if (!name.match(/^[A-Za-z0-9-_]+$/)) return null
let res = await (
await _fetch(`https://${domain}/.well-known/nostr.json?name=${name}`)
).json()
let res
try {
res = await (
await _fetch(`https://${domain}/.well-known/nostr.json?name=${name}`)
).json()
} catch (err) {
return null
}
if (!res?.names?.[name]) return null

View File

@@ -4,12 +4,16 @@ const {nip06} = require('./lib/nostr.cjs')
test('generate private key from a mnemonic', async () => {
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const privateKey = nip06.privateKeyFromSeedWords(mnemonic)
expect(privateKey).toEqual('c26cf31d8ba425b555ca27d00ca71b5008004f2f662470f8c8131822ec129fe2')
expect(privateKey).toEqual(
'c26cf31d8ba425b555ca27d00ca71b5008004f2f662470f8c8131822ec129fe2'
)
})
test('generate private key from a mnemonic and passphrase', async () => {
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const passphrase = '123'
const privateKey = nip06.privateKeyFromSeedWords(mnemonic, passphrase)
expect(privateKey).toEqual('55a22b8203273d0aaf24c22c8fbe99608e70c524b17265641074281c8b978ae4')
expect(privateKey).toEqual(
'55a22b8203273d0aaf24c22c8fbe99608e70c524b17265641074281c8b978ae4'
)
})

View File

@@ -7,7 +7,10 @@ import {
} from '@scure/bip39'
import {HDKey} from '@scure/bip32'
export function privateKeyFromSeedWords(mnemonic: string, passphrase?: string): string {
export function privateKeyFromSeedWords(
mnemonic: string,
passphrase?: string
): string {
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
let privateKey = root.derive(`m/44'/1237'/0'/0/0`).privateKey
if (!privateKey) throw new Error('could not derive private key')

View File

@@ -34,3 +34,55 @@ test('encode and decode nprofile', () => {
expect(data.relays).toContain(relays[0])
expect(data.relays).toContain(relays[1])
})
test('encode and decode naddr', () => {
let pk = getPublicKey(generatePrivateKey())
let relays = [
'wss://relay.nostr.example.mydomain.example.com',
'wss://nostr.banana.com'
]
let naddr = nip19.naddrEncode({
pubkey: pk,
relays,
kind: 30023,
identifier: 'banana'
})
expect(naddr).toMatch(/naddr1\w+/)
console.log(naddr)
let {type, data} = nip19.decode(naddr)
expect(type).toEqual('naddr')
expect(data.pubkey).toEqual(pk)
expect(data.relays).toContain(relays[0])
expect(data.relays).toContain(relays[1])
expect(data.kind).toEqual(30023)
expect(data.identifier).toEqual('banana')
})
test('decode naddr from habla.news', () => {
let {type, data} = nip19.decode(
'naddr1qq98yetxv4ex2mnrv4esygrl54h466tz4v0re4pyuavvxqptsejl0vxcmnhfl60z3rth2xkpjspsgqqqw4rsf34vl5'
)
expect(type).toEqual('naddr')
expect(data.pubkey).toEqual(
'7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194'
)
expect(data.kind).toEqual(30023)
expect(data.identifier).toEqual('references')
})
test('decode naddr from go-nostr with different TLV ordering', () => {
let {type, data} = nip19.decode(
'naddr1qqrxyctwv9hxzq3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqp65wqfwwaehxw309aex2mrp0yhxummnw3ezuetcv9khqmr99ekhjer0d4skjm3wv4uxzmtsd3jjucm0d5q3vamnwvaz7tmwdaehgu3wvfskuctwvyhxxmmd0zfmwx'
)
expect(type).toEqual('naddr')
expect(data.pubkey).toEqual(
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'
)
expect(data.relays).toContain(
'wss://relay.nostr.example.mydomain.example.com'
)
expect(data.relays).toContain('wss://nostr.banana.com')
expect(data.kind).toEqual(30023)
expect(data.identifier).toEqual('banana')
})

View File

@@ -15,46 +15,75 @@ export type EventPointer = {
relays?: string[]
}
export type AddressPointer = {
identifier: string
pubkey: string
kind: number
relays?: string[]
}
export function decode(nip19: string): {
type: string
data: ProfilePointer | EventPointer | string
data: ProfilePointer | EventPointer | AddressPointer | string
} {
let {prefix, words} = bech32.decode(nip19, Bech32MaxSize)
let data = new Uint8Array(bech32.fromWords(words))
if (prefix === 'nprofile') {
let tlv = parseTLV(data)
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nprofile')
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
switch (prefix) {
case 'nprofile': {
let tlv = parseTLV(data)
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nprofile')
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
return {
type: 'nprofile',
data: {
pubkey: secp256k1.utils.bytesToHex(tlv[0][0]),
relays: tlv[1].map(d => utf8Decoder.decode(d))
return {
type: 'nprofile',
data: {
pubkey: secp256k1.utils.bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : []
}
}
}
}
case 'nevent': {
let tlv = parseTLV(data)
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nevent')
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
if (prefix === 'nevent') {
let tlv = parseTLV(data)
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nevent')
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
return {
type: 'nevent',
data: {
id: secp256k1.utils.bytesToHex(tlv[0][0]),
relays: tlv[1].map(d => utf8Decoder.decode(d))
return {
type: 'nevent',
data: {
id: secp256k1.utils.bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : []
}
}
}
}
if (prefix === 'nsec' || prefix === 'npub' || prefix === 'note') {
return {type: prefix, data: secp256k1.utils.bytesToHex(data)}
}
case 'naddr': {
let tlv = parseTLV(data)
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for naddr')
if (!tlv[2]?.[0]) throw new Error('missing TLV 2 for naddr')
if (tlv[2][0].length !== 32) throw new Error('TLV 2 should be 32 bytes')
if (!tlv[3]?.[0]) throw new Error('missing TLV 3 for naddr')
if (tlv[3][0].length !== 4) throw new Error('TLV 3 should be 4 bytes')
throw new Error(`unknown prefix ${prefix}`)
return {
type: 'naddr',
data: {
identifier: utf8Decoder.decode(tlv[0][0]),
pubkey: secp256k1.utils.bytesToHex(tlv[2][0]),
kind: parseInt(secp256k1.utils.bytesToHex(tlv[3][0]), 16),
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : []
}
}
}
case 'nsec':
case 'npub':
case 'note':
return {type: prefix, data: secp256k1.utils.bytesToHex(data)}
default:
throw new Error(`unknown prefix ${prefix}`)
}
}
type TLV = {[t: number]: Uint8Array[]}
@@ -110,6 +139,20 @@ export function neventEncode(event: EventPointer): string {
return bech32.encode('nevent', words, Bech32MaxSize)
}
export function naddrEncode(addr: AddressPointer): string {
let kind = new ArrayBuffer(4)
new DataView(kind).setUint32(0, addr.kind, false)
let data = encodeTLV({
0: [utf8Encoder.encode(addr.identifier)],
1: (addr.relays || []).map(url => utf8Encoder.encode(url)),
2: [secp256k1.utils.hexToBytes(addr.pubkey)],
3: [new Uint8Array(kind)]
})
let words = bech32.toWords(data)
return bech32.encode('naddr', words, Bech32MaxSize)
}
function encodeTLV(tlv: TLV): Uint8Array {
let entries: Uint8Array[] = []

View File

@@ -1,6 +1,6 @@
import {bech32} from '@scure/base'
import {Event, EventTemplate} from './event'
import {Event, EventTemplate, validateEvent, verifySignature} from './event'
import {utf8Decoder} from './utils'
var _fetch: any
@@ -50,17 +50,20 @@ export function makeZapRequest({
}: {
profile: string
event: string | null
amount: string
amount: number
comment: string
relays: string[]
}): EventTemplate {
if (!amount) throw new Error('amount not given')
if (!profile) throw new Error('profile not given')
let zr = {
kind: 9734,
created_at: Math.round(Date.now() / 1000),
content: comment,
tags: [
['p', profile],
['amount', amount],
['amount', amount.toString()],
['relays', ...relays]
]
}
@@ -72,6 +75,34 @@ export function makeZapRequest({
return zr
}
export function validateZapRequest(zapRequestString: string): string | null {
let zapRequest: Event
try {
zapRequest = JSON.parse(zapRequestString)
} catch (err) {
return 'Invalid zap request JSON.'
}
if (!validateEvent(zapRequest))
return 'Zap request is not a valid Nostr event.'
if (!verifySignature(zapRequest)) return 'Invalid signature on zap request.'
let p = zapRequest.tags.find(([t, v]) => t === 'p' && v)
if (!p) return "Zap request doesn't have a 'p' tag."
if (!p[1].match(/^[a-f0-9]{64}$/))
return "Zap request 'p' tag is not valid hex."
let e = zapRequest.tags.find(([t, v]) => t === 'e' && v)
if (e && !e[1].match(/^[a-f0-9]{64}$/))
return "Zap request 'e' tag is not valid hex."
let relays = zapRequest.tags.find(([t, v]) => t === 'relays' && v)
if (!relays) return "Zap request doesn't have a 'relays' tag."
return null
}
export function makeZapReceipt({
zapRequest,
preimage,

View File

@@ -1,13 +1,21 @@
{
"name": "nostr-tools",
"version": "1.4.2",
"version": "1.7.2",
"description": "Tools for making a Nostr client.",
"repository": {
"type": "git",
"url": "https://github.com/fiatjaf/nostr-tools.git"
"url": "https://github.com/nbd-wtf/nostr-tools.git"
},
"files": [
"./lib/**/*"
],
"types": "./lib/index.d.ts",
"main": "lib/nostr.cjs.js",
"module": "lib/nostr.esm.js",
"module": "lib/esm/nostr.mjs",
"exports": {
"import": "./lib/esm/nostr.mjs",
"require": "./lib/nostr.cjs.js"
},
"dependencies": {
"@noble/hashes": "1.0.0",
"@noble/secp256k1": "^1.7.1",

View File

@@ -19,8 +19,8 @@ let relays = [
'wss://nostr.zebedee.cloud/'
]
afterAll(async () => {
await pool.close([
afterAll(() => {
pool.close([
...relays,
'wss://nostr-relay.untethr.me',
'wss://offchain.pub',

61
pool.ts
View File

@@ -8,17 +8,20 @@ export class SimplePool {
private _conn: {[url: string]: Relay}
private _seenOn: {[id: string]: Set<string>} = {} // a map of all events we've seen in each relay
constructor() {
private eoseSubTimeout: number
private getTimeout: number
constructor(options: {eoseSubTimeout?: number; getTimeout?: number} = {}) {
this._conn = {}
this.eoseSubTimeout = options.eoseSubTimeout || 3400
this.getTimeout = options.getTimeout || 3400
}
async close(relays: string[]): Promise<void> {
await Promise.all(
relays.map(async url => {
let relay = this._conn[normalizeURL(url)]
if (relay) await relay.close()
})
)
close(relays: string[]): void {
relays.forEach(url => {
let relay = this._conn[normalizeURL(url)]
if (relay) relay.close()
})
}
async ensureRelay(url: string): Promise<Relay> {
@@ -26,7 +29,10 @@ export class SimplePool {
const existing = this._conn[nm]
if (existing) return existing
const relay = relayInit(nm)
const relay = relayInit(nm, {
getTimeout: this.getTimeout * 0.9,
listTimeout: this.getTimeout * 0.9
})
this._conn[nm] = relay
await relay.connect()
@@ -53,10 +59,16 @@ export class SimplePool {
let eoseTimeout = setTimeout(() => {
eoseSent = true
for (let cb of eoseListeners.values()) cb()
}, 2400)
}, this.eoseSubTimeout)
relays.forEach(async relay => {
let r = await this.ensureRelay(relay)
let r
try {
r = await this.ensureRelay(relay)
} catch (err) {
handleEose()
return
}
if (!r) return
let s = r.sub(filters, modifiedOpts)
s.on('event', (event: Event) => {
@@ -65,14 +77,17 @@ export class SimplePool {
})
s.on('eose', () => {
if (eoseSent) return
handleEose()
})
subs.push(s)
function handleEose() {
eosesMissing--
if (eosesMissing === 0) {
clearTimeout(eoseTimeout)
for (let cb of eoseListeners.values()) cb()
}
})
subs.push(s)
}
})
let greaterSub: Sub = {
@@ -113,7 +128,7 @@ export class SimplePool {
let timeout = setTimeout(() => {
sub.unsub()
resolve(null)
}, 1500)
}, this.getTimeout)
sub.on('event', (event: Event) => {
resolve(event)
clearTimeout(timeout)
@@ -143,13 +158,23 @@ export class SimplePool {
})
}
publish(relays: string[], event: Event): Pub[] {
return relays.map(relay => {
publish(relays: string[], event: Event): Pub {
let pubs = relays.map(relay => {
let r = this._conn[normalizeURL(relay)]
if (!r) return badPub(relay)
let s = r.publish(event)
return s
return r.publish(event)
})
return {
on(type, cb) {
pubs.forEach((pub, i) => {
pub.on(type, () => cb(relays[i]))
})
},
off() {
// do nothing here, FIXME
}
}
}
seenOn(id: string): string[] {

View File

@@ -15,8 +15,8 @@ beforeAll(() => {
relay.connect()
})
afterAll(async () => {
await relay.close()
afterAll(() => {
relay.close()
})
test('connectivity', () => {

View File

@@ -10,7 +10,7 @@ export type Relay = {
url: string
status: number
connect: () => Promise<void>
close: () => Promise<void>
close: () => void
sub: (filters: Filter[], opts?: SubscriptionOptions) => Sub
list: (filters: Filter[], opts?: SubscriptionOptions) => Promise<Event[]>
get: (filter: Filter, opts?: SubscriptionOptions) => Promise<Event | null>
@@ -35,9 +35,16 @@ export type SubscriptionOptions = {
alreadyHaveEvent?: null | ((id: string, relay: string) => boolean)
}
export function relayInit(url: string): Relay {
export function relayInit(
url: string,
options: {
getTimeout?: number
listTimeout?: number
} = {}
): Relay {
let {listTimeout = 3000, getTimeout = 3000} = options
var ws: WebSocket
var resolveClose: () => void
var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {}
var listeners: {
connect: Array<() => void>
@@ -78,7 +85,6 @@ export function relayInit(url: string): Relay {
}
ws.onclose = async () => {
listeners.disconnect.forEach(cb => cb())
resolveClose && resolveClose()
}
let incomingMessageQueue: string[] = []
@@ -165,14 +171,23 @@ export function relayInit(url: string): Relay {
})
}
function connected() {
return ws?.readyState === 1
}
async function connect(): Promise<void> {
if (ws?.readyState && ws.readyState === 1) return // ws already open
if (connected()) return // ws already open
await connectRelay()
}
async function trySend(params: [string, ...any]) {
let msg = JSON.stringify(params)
if (!connected()) {
await new Promise(resolve => setTimeout(resolve, 1000))
if (!connected()) {
return
}
}
try {
ws.send(msg)
} catch (err) {
@@ -245,7 +260,7 @@ export function relayInit(url: string): Relay {
let timeout = setTimeout(() => {
s.unsub()
resolve(events)
}, 1500)
}, listTimeout)
s.on('eose', () => {
s.unsub()
clearTimeout(timeout)
@@ -261,7 +276,7 @@ export function relayInit(url: string): Relay {
let timeout = setTimeout(() => {
s.unsub()
resolve(null)
}, 1500)
}, getTimeout)
s.on('event', (event: Event) => {
s.unsub()
clearTimeout(timeout)
@@ -291,16 +306,12 @@ export function relayInit(url: string): Relay {
}
},
connect,
close(): Promise<void> {
close(): void {
listeners = {connect: [], disconnect: [], error: [], notice: []}
subListeners = {}
pubListeners = {}
if (ws.readyState > 1) return Promise.resolve()
ws.close()
return new Promise<void>(resolve => {
resolveClose = resolve
})
ws?.close()
},
get status() {
return ws?.readyState ?? 3

View File

@@ -9,7 +9,7 @@
"skipLibCheck": true,
"esModuleInterop": true,
"emitDeclarationOnly": true,
"outDir": "dist",
"outDir": "lib",
"rootDir": "."
}
}