/* global WebSocket */ import {Event, verifySignature, validateEvent} from './event' import {Filter, matchFilters} from './filter' export type Relay = { url: string status: number connect: () => void close: () => void 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 } export type Pub = { on: (type: 'ok' | 'seen' | 'failed', cb: any) => void off: (type: 'ok' | 'seen' | 'failed', cb: any) => void } export type 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 = { skipVerification?: boolean id?: string } export function relayInit(url: string): Relay { var ws: WebSocket var resolveClose: () => void var untilOpen: Promise var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {} var listeners: { connect: Array<() => void> disconnect: Array<() => void> error: Array<() => void> notice: Array<(msg: string) => void> } = { connect: [], disconnect: [], error: [], notice: [] } var subListeners: { [subid: string]: { event: Array<(event: Event) => void> eose: Array<() => void> } } = {} var pubListeners: { [eventid: string]: { ok: Array<() => void> seen: Array<() => void> failed: Array<(reason: string) => void> } } = {} async function connectRelay(): Promise { return new Promise((resolve, reject) => { ws = new WebSocket(url) ws.onopen = () => { listeners.connect.forEach(cb => cb()) resolve() } ws.onerror = () => { listeners.error.forEach(cb => cb()) reject() } ws.onclose = async () => { listeners.disconnect.forEach(cb => cb()) resolveClose() } 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].filters, event) ) { openSubs[id] subListeners[id]?.event.forEach(cb => cb(event)) } return case 'EOSE': { if (data.length !== 2) return // ignore empty or malformed EOSE let id = data[1] subListeners[id]?.eose.forEach(cb => cb()) return } case 'OK': { if (data.length < 3) return // ignore empty or malformed OK let id: string = data[1] let ok: boolean = data[2] let reason: string = data[3] || '' if (ok) pubListeners[id]?.ok.forEach(cb => cb()) else pubListeners[id]?.failed.forEach(cb => cb(reason)) return } case 'NOTICE': if (data.length !== 2) return // ignore empty or malformed NOTICE let notice = data[1] listeners.notice.forEach(cb => cb(notice)) return } } } }) } async function connect(): Promise { if (ws?.readyState && ws.readyState === 1) return // ws already open await connectRelay() } async function trySend(params: [string, ...any]) { let msg = JSON.stringify(params) await untilOpen ws.send(msg) } const sub = ( filters: Filter[], { skipVerification = false, id = Math.random().toString().slice(2) }: SubscriptionOptions = {} ): Sub => { let subid = id openSubs[subid] = { id: subid, filters, skipVerification } trySend(['REQ', subid, ...filters]) return { sub: (newFilters, newOpts = {}) => sub(newFilters || filters, { skipVerification: newOpts.skipVerification || skipVerification, id: subid }), unsub: () => { delete openSubs[subid] delete subListeners[subid] trySend(['CLOSE', subid]) }, on: (type: 'event' | 'eose', cb: any): void => { subListeners[subid] = subListeners[subid] || { event: [], eose: [] } subListeners[subid][type].push(cb) }, off: (type: 'event' | 'eose', cb: any): void => { let idx = subListeners[subid][type].indexOf(cb) if (idx >= 0) subListeners[subid][type].splice(idx, 1) } } } return { url, sub, on: ( type: 'connect' | 'disconnect' | 'error' | 'notice', cb: any ): void => { listeners[type].push(cb) if (type === 'connect' && ws?.readyState === 1) { cb() } }, off: ( type: 'connect' | 'disconnect' | 'error' | 'notice', cb: any ): void => { let index = listeners[type].indexOf(cb) if (index !== -1) listeners[type].splice(index, 1) }, publish(event: Event): Pub { if (!event.id) throw new Error(`event ${event} has no id`) let id = event.id var sent = false var mustMonitor = false trySend(['EVENT', event]) .then(() => { sent = true if (mustMonitor) { startMonitoring() mustMonitor = false } }) .catch(() => {}) const startMonitoring = () => { let monitor = sub([{ids: [id]}], { id: `monitor-${id.slice(0, 5)}` }) let willUnsub = setTimeout(() => { pubListeners[id].failed.forEach(cb => cb('event not seen after 5 seconds') ) monitor.unsub() }, 5000) monitor.on('event', () => { clearTimeout(willUnsub) pubListeners[id].seen.forEach(cb => cb()) }) } return { on: (type: 'ok' | 'seen' | 'failed', cb: any) => { pubListeners[id] = pubListeners[id] || { ok: [], seen: [], failed: [] } pubListeners[id][type].push(cb) if (type === 'seen') { if (sent) startMonitoring() else mustMonitor = true } }, off: (type: 'ok' | 'seen' | 'failed', cb: any) => { let idx = pubListeners[id][type].indexOf(cb) if (idx >= 0) pubListeners[id][type].splice(idx, 1) } } }, connect, close(): Promise { ws.close() return new Promise(resolve => { resolveClose = resolve }) }, get status() { return ws?.readyState ?? 3 } } }