mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-09 00:28:51 +00:00
215 lines
5.7 KiB
JavaScript
215 lines
5.7 KiB
JavaScript
/* global WebSocket */
|
|
|
|
import 'websocket-polyfill'
|
|
|
|
import { verifySignature, validateEvent } from './event.js'
|
|
import { matchFilters } from './filter.js'
|
|
|
|
export function normalizeRelayURL(url) {
|
|
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 function relayInit(url) {
|
|
let relay = normalizeRelayURL(url) // set relay url
|
|
|
|
var ws, resolveOpen, untilOpen, wasClosed, closed
|
|
var openSubs = {}
|
|
var listeners = {
|
|
event: { '_': [] },
|
|
eose: { '_': [] },
|
|
connection: { '_': [] },
|
|
disconnection: { '_': [] },
|
|
error: { '_': [] },
|
|
notice: { '_': [] },
|
|
}
|
|
let attemptNumber = 1
|
|
let nextAttemptSeconds = 1
|
|
|
|
function resetOpenState() {
|
|
untilOpen = new Promise(resolve => {
|
|
resolveOpen = resolve
|
|
})
|
|
}
|
|
|
|
function connectRelay() {
|
|
ws = new WebSocket(relay)
|
|
|
|
ws.onopen = () => {
|
|
listeners.connection._.forEach(cb => cb({ type: 'connection', relay }))
|
|
resolveOpen()
|
|
|
|
// restablish old subscriptions
|
|
if (wasClosed) {
|
|
wasClosed = false
|
|
for (let id in openSubs) {
|
|
sub(openSubs[id], id)
|
|
}
|
|
}
|
|
}
|
|
ws.onerror = error => {
|
|
listeners.error._.forEach(cb => cb({ type: 'error', relay, error }))
|
|
}
|
|
ws.onclose = async () => {
|
|
listeners.disconnection._.forEach(cb => cb({ type: 'disconnection', relay }))
|
|
if (closed) return
|
|
resetOpenState()
|
|
attemptNumber++
|
|
nextAttemptSeconds += attemptNumber ** 3
|
|
if (nextAttemptSeconds > 14400) {
|
|
nextAttemptSeconds = 14400 // 4 hours
|
|
}
|
|
console.log(
|
|
`relay ${relay} connection closed. reconnecting in ${nextAttemptSeconds} seconds.`
|
|
)
|
|
setTimeout(async () => {
|
|
try {
|
|
connectRelay()
|
|
} catch (err) { }
|
|
}, nextAttemptSeconds * 1000)
|
|
|
|
wasClosed = true
|
|
}
|
|
|
|
ws.onmessage = async e => {
|
|
var data
|
|
try {
|
|
data = JSON.parse(e.data)
|
|
} catch (err) {
|
|
data = e.data
|
|
}
|
|
|
|
if (data.length >= 1) {
|
|
switch (data[0]) {
|
|
case 'EVENT':
|
|
if (data.length !== 3) return // ignore empty or malformed EVENT
|
|
|
|
let id = data[1]
|
|
let event = data[2]
|
|
if (validateEvent(event) && openSubs[id] &&
|
|
(openSubs[id].skipVerification || verifySignature(event)) &&
|
|
matchFilters(openSubs[id].filter, event)
|
|
) {
|
|
if (listeners.event[id]?.length) listeners.event[id].forEach(cb => cb({ type: 'event', relay, id, event }))
|
|
if (listeners.event._.length) listeners.event._.forEach(cb => cb({ type: 'event', relay, id, event }))
|
|
}
|
|
return
|
|
case 'EOSE': {
|
|
if (data.length !== 2) return // ignore empty or malformed EOSE
|
|
|
|
let id = data[1]
|
|
if (listeners.eose[id]?.length) listeners.eose[data[1]].forEach(cb => cb({ type: 'eose', relay, id }))
|
|
if (listeners.eose._.length) listeners.eose._.forEach(cb => cb({ type: 'eose', relay, id }))
|
|
return
|
|
}
|
|
case 'NOTICE':
|
|
if (data.length !== 2) return // ignore empty or malformed NOTICE
|
|
|
|
let notice = data[1]
|
|
if (listeners.notice._.length) listeners.notice._.forEach(cb => cb({ type: 'notice', relay, notice }))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resetOpenState()
|
|
|
|
async function connect() {
|
|
if (ws?.readyState && ws.readyState === 1) return // ws already open
|
|
try {
|
|
connectRelay()
|
|
} catch (err) { }
|
|
}
|
|
|
|
async function trySend(params) {
|
|
let msg = JSON.stringify(params)
|
|
|
|
await untilOpen
|
|
ws.send(msg)
|
|
}
|
|
|
|
const sub = ({ filter, beforeSend, skipVerification }, id = Math.random().toString().slice(2)) => {
|
|
var filters = []
|
|
if (Array.isArray(filter)) {
|
|
filters = filter
|
|
} else {
|
|
filters.push(filter)
|
|
}
|
|
filter = filters
|
|
|
|
if (beforeSend) {
|
|
const beforeSendResult = beforeSend({ filter, relay, id })
|
|
filter = beforeSendResult.filter
|
|
}
|
|
|
|
openSubs[id] = {
|
|
filter,
|
|
beforeSend,
|
|
skipVerification,
|
|
}
|
|
trySend(['REQ', id, ...filter])
|
|
|
|
return {
|
|
sub: ({
|
|
filter = openSubs[id].filter,
|
|
beforeSend = openSubs[id].beforeSend,
|
|
skipVerification = openSubs[id].skipVerification }
|
|
) => sub({ filter, beforeSend, skipVerification }, id),
|
|
unsub: () => {
|
|
delete openSubs[id]
|
|
delete listeners.event[id]
|
|
delete listeners.eose[id]
|
|
trySend(['CLOSE', id])
|
|
}
|
|
}
|
|
}
|
|
|
|
function on(type, cb, id = '_') {
|
|
listeners[type][id] = listeners[type][id] || []
|
|
listeners[type][id].push(cb)
|
|
}
|
|
|
|
function off(type, cb, id = '_') {
|
|
if (!listeners[type][id].length) return
|
|
let index = listeners[type][id].indexOf(cb)
|
|
if (index !== -1) listeners[type][id].splice(index, 1)
|
|
}
|
|
|
|
return {
|
|
url,
|
|
sub,
|
|
on,
|
|
off,
|
|
async publish(event, statusCallback) {
|
|
try {
|
|
await trySend(['EVENT', event])
|
|
if (statusCallback) {
|
|
let id = `monitor-${event.id.slice(0, 5)}`
|
|
statusCallback(0)
|
|
let { unsub } = sub({ filter: { ids: [event.id] } }, id)
|
|
on('event', () => {
|
|
statusCallback(1)
|
|
unsub()
|
|
clearTimeout(willUnsub)
|
|
}, id)
|
|
let willUnsub = setTimeout(unsub, 5000)
|
|
}
|
|
} catch (err) {
|
|
if (statusCallback) statusCallback(-1)
|
|
}
|
|
},
|
|
connect,
|
|
close() {
|
|
closed = true // prevent ws from trying to reconnect
|
|
ws.close()
|
|
},
|
|
get status() {
|
|
return ws.readyState
|
|
}
|
|
}
|
|
}
|