Switch to monorepo setup
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
build
|
||||
normalize-url
|
||||
@@ -0,0 +1,119 @@
|
||||
import {Emitter, Queue} from '@coracle.social/lib'
|
||||
import {AuthStatus, ConnectionMeta} from './ConnectionMeta'
|
||||
import {Socket, isMessage, asMessage} from './Socket'
|
||||
import type {SocketMessage} from './Socket'
|
||||
|
||||
class SendQueue extends Queue {
|
||||
constructor(readonly cxn: Connection) {
|
||||
super()
|
||||
}
|
||||
|
||||
shouldSend(message: SocketMessage) {
|
||||
if (!this.cxn.socket.isReady()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const [verb, ...extra] = asMessage(message)
|
||||
|
||||
if (['AUTH', 'CLOSE'].includes(verb)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Allow relay requests through
|
||||
if (verb === 'EVENT' && extra[0].kind === 28934) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Only defer for auth if we're not multiplexing
|
||||
if (isMessage(message) && ![AuthStatus.Ok, AuthStatus.Pending].includes(this.cxn.meta.authStatus)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (verb === 'REQ') {
|
||||
return this.cxn.meta.pendingRequests.size < 8
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
handle(message: SocketMessage) {
|
||||
// If we ended up handling a CLOSE before we handled the REQ, don't send the REQ
|
||||
if (message[0] === 'CLOSE') {
|
||||
this.messages = this.messages.filter(m => !(m[0] === 'REQ' && m[1] === message[1]))
|
||||
}
|
||||
|
||||
this.cxn.onSend(message)
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveQueue extends Queue {
|
||||
constructor(readonly cxn: Connection) {
|
||||
super()
|
||||
}
|
||||
|
||||
handle(message: SocketMessage) {
|
||||
this.cxn.onReceive(message)
|
||||
}
|
||||
}
|
||||
|
||||
export class Connection extends Emitter {
|
||||
url: string
|
||||
socket: Socket
|
||||
sendQueue: SendQueue
|
||||
receiveQueue: ReceiveQueue
|
||||
meta: ConnectionMeta
|
||||
|
||||
constructor(url: string) {
|
||||
super()
|
||||
|
||||
this.url = url
|
||||
this.socket = new Socket(url, this)
|
||||
this.sendQueue = new SendQueue(this)
|
||||
this.receiveQueue = new ReceiveQueue(this)
|
||||
this.meta = new ConnectionMeta(this)
|
||||
this.setMaxListeners(100)
|
||||
}
|
||||
|
||||
send = (m: SocketMessage) => this.sendQueue.push(m)
|
||||
|
||||
onOpen = () => this.emit('open', this)
|
||||
|
||||
onClose = () => this.emit('close', this)
|
||||
|
||||
onError = () => this.emit('fault', this)
|
||||
|
||||
onMessage = (m: SocketMessage) => this.receiveQueue.push(m)
|
||||
|
||||
onSend = (message: SocketMessage) => {
|
||||
this.emit('send', this, message)
|
||||
this.socket.send(message)
|
||||
}
|
||||
|
||||
onReceive = (message: SocketMessage) => {
|
||||
this.emit('receive', this, message)
|
||||
}
|
||||
|
||||
ensureConnected = ({shouldReconnect = true}) => {
|
||||
if (shouldReconnect && !this.socket.isHealthy()) {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
if (this.socket.isPending()) {
|
||||
this.socket.connect()
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.socket.disconnect()
|
||||
this.sendQueue.clear()
|
||||
this.receiveQueue.clear()
|
||||
this.meta.clearPending()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.disconnect()
|
||||
this.removeAllListeners()
|
||||
this.sendQueue.stop()
|
||||
this.receiveQueue.stop()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import type {Event, Filter} from 'nostr-tools'
|
||||
import type {Connection} from './Connection'
|
||||
import type {Message} from './Socket'
|
||||
|
||||
export type PublishMeta = {
|
||||
sent: number
|
||||
event: Event
|
||||
}
|
||||
|
||||
export type RequestMeta = {
|
||||
sent: number
|
||||
filters: Filter[]
|
||||
eoseReceived: boolean
|
||||
}
|
||||
|
||||
export enum AuthStatus {
|
||||
Ok = 'ok',
|
||||
Pending = 'pending',
|
||||
Unauthorized = 'unauthorized',
|
||||
Forbidden = 'forbidden',
|
||||
}
|
||||
|
||||
export enum ConnectionStatus {
|
||||
Unauthorized = 'unauthorized',
|
||||
Forbidden = 'forbidden',
|
||||
Error = 'error',
|
||||
Closed = 'closed',
|
||||
Slow = 'slow',
|
||||
Ok = 'ok',
|
||||
}
|
||||
|
||||
export class ConnectionMeta {
|
||||
authStatus = AuthStatus.Pending
|
||||
pendingPublishes = new Map<string, PublishMeta>()
|
||||
pendingRequests = new Map<string, RequestMeta>()
|
||||
publishCount = 0
|
||||
requestCount = 0
|
||||
eventCount = 0
|
||||
lastOpen = 0
|
||||
lastClose = 0
|
||||
lastFault = 0
|
||||
lastPublish = 0
|
||||
lastRequest = 0
|
||||
lastEvent = 0
|
||||
responseCount = 0
|
||||
responseTimer = 0
|
||||
|
||||
constructor(readonly cxn: Connection) {
|
||||
cxn.on('open', () => {
|
||||
this.lastOpen = Date.now()
|
||||
})
|
||||
|
||||
cxn.on('close', () => {
|
||||
this.lastClose = Date.now()
|
||||
})
|
||||
|
||||
cxn.on('fault', () => {
|
||||
this.lastFault = Date.now()
|
||||
})
|
||||
|
||||
cxn.on('send', (cxn: Connection, message: Message) => {
|
||||
if (message[0] === 'REQ') this.onSendRequest(message)
|
||||
if (message[0] === 'CLOSE') this.onSendClose(message)
|
||||
if (message[0] === 'EVENT') this.onSendEvent(message)
|
||||
})
|
||||
|
||||
cxn.on('receive', (cxn: Connection, message: Message) => {
|
||||
if (message[0] === 'OK') this.onReceiveOk(message)
|
||||
if (message[0] === 'AUTH') this.onReceiveAuth(message)
|
||||
if (message[0] === 'EVENT') this.onReceiveEvent(message)
|
||||
if (message[0] === 'EOSE') this.onReceiveEose(message)
|
||||
if (message[0] === 'CLOSED') this.onReceiveClosed(message)
|
||||
if (message[0] === 'NOTICE') this.onReceiveNotice(message)
|
||||
})
|
||||
}
|
||||
|
||||
onSendRequest([verb, subId, ...filters]: Message) {
|
||||
this.requestCount++
|
||||
this.lastRequest = Date.now()
|
||||
this.pendingRequests.set(subId, {
|
||||
filters,
|
||||
sent: Date.now(),
|
||||
eoseReceived: false,
|
||||
})
|
||||
}
|
||||
|
||||
onSendClose([verb, subId]: Message) {
|
||||
this.pendingRequests.delete(subId)
|
||||
}
|
||||
|
||||
onSendEvent([verb, event]: Message) {
|
||||
this.publishCount++
|
||||
this.lastPublish = Date.now()
|
||||
this.pendingPublishes.set(event.id, {sent: Date.now(), event})
|
||||
}
|
||||
|
||||
onReceiveOk([verb, eventId, ok, notice]: Message) {
|
||||
const publish = this.pendingPublishes.get(eventId)
|
||||
|
||||
if (ok) {
|
||||
this.authStatus = AuthStatus.Ok
|
||||
} else if (notice.startsWith('auth-required:')) {
|
||||
// Re-enqueue pending reqs when auth challenge is received
|
||||
const pub = this.pendingPublishes.get(eventId)
|
||||
|
||||
if (pub) {
|
||||
this.cxn.send(['EVENT', pub.event])
|
||||
}
|
||||
}
|
||||
|
||||
if (publish) {
|
||||
this.responseCount++
|
||||
this.responseTimer += Date.now() - publish.sent
|
||||
this.pendingPublishes.delete(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
onReceiveAuth([verb, eventId]: Message) {
|
||||
this.authStatus = AuthStatus.Unauthorized
|
||||
}
|
||||
|
||||
onReceiveEvent([verb, event]: Message) {
|
||||
this.eventCount++
|
||||
this.lastEvent = Date.now()
|
||||
}
|
||||
|
||||
onReceiveEose([verb, subId]: Message) {
|
||||
const request = this.pendingRequests.get(subId)
|
||||
|
||||
// Only count the first eose
|
||||
if (request && !request.eoseReceived) {
|
||||
request.eoseReceived = true
|
||||
|
||||
this.responseCount++
|
||||
this.responseTimer += Date.now() - request.sent
|
||||
}
|
||||
}
|
||||
|
||||
onReceiveClosed([verb, id, notice]: Message) {
|
||||
if (notice.startsWith('auth-required:')) {
|
||||
// Re-enqueue pending reqs when auth challenge is received
|
||||
const req = this.pendingRequests.get(id)
|
||||
|
||||
if (req) {
|
||||
this.cxn.send(['REQ', id, ...req.filters])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReceiveNotice([verb, notice]: Message) {
|
||||
console.warn('NOTICE', this.cxn.url, notice)
|
||||
}
|
||||
|
||||
clearPending = () => {
|
||||
this.pendingPublishes.clear()
|
||||
this.pendingRequests.clear()
|
||||
}
|
||||
|
||||
getSpeed = () => this.responseCount ? this.responseTimer / this.responseCount : 0
|
||||
|
||||
getStatus = () => {
|
||||
if (this.authStatus === AuthStatus.Unauthorized) return ConnectionStatus.Unauthorized
|
||||
if (this.authStatus === AuthStatus.Forbidden) return ConnectionStatus.Forbidden
|
||||
if (this.lastFault > this.lastOpen) return ConnectionStatus.Error
|
||||
if (this.lastClose > this.lastOpen) return ConnectionStatus.Closed
|
||||
if (this.getSpeed() > 1000) return ConnectionStatus.Slow
|
||||
|
||||
return ConnectionStatus.Ok
|
||||
}
|
||||
|
||||
getDescription = () => {
|
||||
switch (this.getStatus()) {
|
||||
case ConnectionStatus.Unauthorized: return 'Logging in'
|
||||
case ConnectionStatus.Forbidden: return 'Failed to log in'
|
||||
case ConnectionStatus.Error: return 'Failed to connect'
|
||||
case ConnectionStatus.Closed: return 'Waiting to reconnect'
|
||||
case ConnectionStatus.Slow: return 'Slow Connection'
|
||||
case ConnectionStatus.Ok: return 'Connected'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type {Event, Filter} from 'nostr-tools'
|
||||
import type {Emitter} from '@coracle.social/lib'
|
||||
import type {Connection} from './Connection'
|
||||
import type {Message} from './Socket'
|
||||
|
||||
export type Target = Emitter & {
|
||||
connections: Connection[]
|
||||
send: (...args: Message) => void
|
||||
cleanup: () => void
|
||||
}
|
||||
|
||||
type EventCallback = (url: string, event: Event) => void
|
||||
type EoseCallback = (url: string) => void
|
||||
type AuthCallback = (url: string, challenge: string) => void
|
||||
type OkCallback = (url: string, id: string, ...extra: any[]) => void
|
||||
type ErrorCallback = (url: string, id: string, ...extra: any[]) => void
|
||||
type SubscribeOpts = {onEvent?: EventCallback, onEose?: EoseCallback}
|
||||
type PublishOpts = {verb?: string, onOk?: OkCallback, onError?: ErrorCallback}
|
||||
type AuthOpts = {onAuth: AuthCallback, onOk: OkCallback}
|
||||
|
||||
const createSubId = (prefix: string) => [prefix, Math.random().toString().slice(2, 10)].join('-')
|
||||
|
||||
export class Executor {
|
||||
|
||||
constructor(readonly target: Target) {}
|
||||
|
||||
subscribe(filters: Filter[], {onEvent, onEose}: SubscribeOpts = {}) {
|
||||
let closed = false
|
||||
|
||||
const id = createSubId('REQ')
|
||||
const eventListener = (url: string, subid: string, e: Event) => subid === id && onEvent?.(url, e)
|
||||
const eoseListener = (url: string, subid: string) => subid === id && onEose?.(url)
|
||||
|
||||
this.target.on('EVENT', eventListener)
|
||||
this.target.on('EOSE', eoseListener)
|
||||
this.target.send("REQ", id, ...filters)
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
if (!closed) {
|
||||
this.target.send("CLOSE", id)
|
||||
this.target.off('EVENT', eventListener)
|
||||
this.target.off('EOSE', eoseListener)
|
||||
}
|
||||
|
||||
closed = true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
publish(event: Event, {verb = 'EVENT', onOk, onError}: PublishOpts = {}) {
|
||||
const okListener = (url: string, id: string, ...payload: any[]) => id === event.id && onOk?.(url, id, ...payload)
|
||||
const errorListener = (url: string, id: string, ...payload: any[]) => id === event.id && onError?.(url, id, ...payload)
|
||||
|
||||
this.target.on('OK', okListener)
|
||||
this.target.on('ERROR', errorListener)
|
||||
this.target.send(verb, event)
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
this.target.off('OK', okListener)
|
||||
this.target.off('ERROR', errorListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleAuth({onAuth, onOk}: AuthOpts) {
|
||||
this.target.on('AUTH', onAuth)
|
||||
this.target.on('OK', onOk)
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
this.target.off('AUTH', onAuth)
|
||||
this.target.off('OK', onOk)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import {Emitter} from '@coracle.social/lib'
|
||||
import {Connection} from "./Connection"
|
||||
|
||||
export class Pool extends Emitter {
|
||||
data: Map<string, Connection>
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.data = new Map()
|
||||
}
|
||||
has(url: string) {
|
||||
return this.data.has(url)
|
||||
}
|
||||
get(url: string, {autoConnect = true, reconnectAfter = 3000} = {}): Connection {
|
||||
let connection = this.data.get(url)
|
||||
|
||||
if (autoConnect) {
|
||||
if (!connection) {
|
||||
connection = new Connection(url)
|
||||
|
||||
this.data.set(url, connection)
|
||||
this.emit('init', connection)
|
||||
|
||||
connection.on('open', () => this.emit('open', connection))
|
||||
connection.on('close', () => this.emit('close', connection))
|
||||
}
|
||||
|
||||
connection.ensureConnected({
|
||||
shouldReconnect: connection.meta.lastClose < Date.now() - reconnectAfter,
|
||||
})
|
||||
}
|
||||
|
||||
return connection!
|
||||
}
|
||||
remove(url: string) {
|
||||
const connection = this.data.get(url)
|
||||
|
||||
if (connection) {
|
||||
connection.destroy()
|
||||
|
||||
this.data.delete(url)
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
for (const url of this.data.keys()) {
|
||||
this.remove(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import WebSocket from "isomorphic-ws"
|
||||
import {Deferred, defer} from '@coracle.social/lib'
|
||||
|
||||
export type Message = [string, ...any[]]
|
||||
|
||||
export type PlexMessage = [{relays: string[]}, Message]
|
||||
|
||||
export type SocketMessage = Message | PlexMessage
|
||||
|
||||
export const isMessage = (m: SocketMessage): boolean => typeof m[0] === 'string'
|
||||
|
||||
export const asMessage = (m: SocketMessage): Message => isMessage(m) ? m : m[1]
|
||||
|
||||
export type SocketOpts = {
|
||||
onOpen: () => void
|
||||
onClose: () => void
|
||||
onError: () => void
|
||||
onMessage: (message: SocketMessage) => void
|
||||
}
|
||||
|
||||
export class Socket {
|
||||
url: string
|
||||
ws?: WebSocket
|
||||
ready: Deferred<boolean>
|
||||
failedToConnect = false
|
||||
|
||||
constructor(url: string, readonly opts: SocketOpts) {
|
||||
this.url = url
|
||||
this.ready = defer()
|
||||
}
|
||||
|
||||
isPending() {
|
||||
return !this.ws && !this.failedToConnect
|
||||
}
|
||||
|
||||
isConnecting() {
|
||||
return this.ws?.readyState === WebSocket.CONNECTING
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this.ws?.readyState === WebSocket.OPEN
|
||||
}
|
||||
|
||||
isClosing() {
|
||||
return this.ws?.readyState === WebSocket.CLOSING
|
||||
}
|
||||
|
||||
isClosed() {
|
||||
return this.ws?.readyState === WebSocket.CLOSED
|
||||
}
|
||||
|
||||
isHealthy() {
|
||||
return this.isPending() || this.isConnecting() || this.isReady()
|
||||
}
|
||||
|
||||
onOpen = () => {
|
||||
this.ready.resolve(true)
|
||||
this.opts.onOpen()
|
||||
}
|
||||
|
||||
onError = () => {
|
||||
this.opts.onError()
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
onMessage = (event: {data: string}) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data as string)
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
this.opts.onMessage(message as Message)
|
||||
} else {
|
||||
console.warn("Invalid messages received:", message)
|
||||
}
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
send = (message: any) => {
|
||||
if (!this.ws) {
|
||||
throw new Error('Send attempted before socket was opened')
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(message))
|
||||
}
|
||||
|
||||
connect = () => {
|
||||
if (this.ws) {
|
||||
throw new Error(`Already attempted connection for ${this.url}`)
|
||||
}
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.url)
|
||||
this.ws.onopen = this.onOpen
|
||||
this.ws.onerror = this.onError
|
||||
this.ws.onmessage = this.onMessage
|
||||
this.ws.onclose = this.disconnect
|
||||
} catch (e) {
|
||||
this.failedToConnect = true
|
||||
}
|
||||
}
|
||||
|
||||
disconnect = () => {
|
||||
if (this.ws) {
|
||||
const currentWs = this.ws
|
||||
|
||||
this.ready.then(() => currentWs.close())
|
||||
this.ready = defer()
|
||||
this.opts.onClose()
|
||||
this.ws = undefined
|
||||
|
||||
// Resolve a different instance of the promise
|
||||
this.ready.resolve(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import {Emitter} from '@coracle.social/lib'
|
||||
import type {Filter} from '@coracle.social/util'
|
||||
import {matchFilters, hasValidSignature} from '@coracle.social/util'
|
||||
import type {Event} from 'nostr-tools'
|
||||
import type {Executor} from "./Executor"
|
||||
import type {Connection} from './Connection'
|
||||
|
||||
export type SubscriptionOpts = {
|
||||
executor: Executor
|
||||
filters: Filter[]
|
||||
timeout?: number
|
||||
closeOnEose?: boolean
|
||||
hasSeen?: (e: Event, url: string) => boolean
|
||||
shouldValidate?: (e: Event, url: string) => boolean
|
||||
}
|
||||
|
||||
export class Subscription extends Emitter {
|
||||
unsubscribe: () => void
|
||||
dead = new Set<string>()
|
||||
seen = new Set<string>()
|
||||
eose = new Set<string>()
|
||||
closeHandlers = new Map()
|
||||
opened = Date.now()
|
||||
closed?: number
|
||||
|
||||
constructor(readonly opts: SubscriptionOpts) {
|
||||
super()
|
||||
|
||||
const {executor, timeout, filters} = this.opts
|
||||
|
||||
// If we have a timeout, close the subscription automatically
|
||||
if (timeout) {
|
||||
setTimeout(this.close, timeout)
|
||||
}
|
||||
|
||||
// If one of our connections gets closed make sure to kill our sub
|
||||
executor.target.connections.forEach(con => {
|
||||
const handler = () => {
|
||||
this.dead.add(con.url)
|
||||
|
||||
if (this.dead.size === executor.target.connections.length) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
this.closeHandlers.set(con.url, handler)
|
||||
|
||||
con.on("close", handler)
|
||||
})
|
||||
|
||||
// Start our subscription
|
||||
const sub = executor.subscribe(filters, {
|
||||
onEvent: this.onEvent,
|
||||
onEose: this.onEose,
|
||||
})
|
||||
|
||||
this.unsubscribe = sub.unsubscribe
|
||||
}
|
||||
|
||||
hasSeen = (event: Event, url: string) => {
|
||||
if (this.opts.hasSeen) {
|
||||
return this.opts.hasSeen(event, url)
|
||||
}
|
||||
|
||||
if (this.seen.has(event.id)) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.seen.add(event.id)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
hasValidSignature = (event: Event, url: string) => {
|
||||
if (this.opts.shouldValidate && !this.opts.shouldValidate(event, url)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return hasValidSignature(event)
|
||||
}
|
||||
|
||||
onEvent = (url: string, event: Event) => {
|
||||
// If we've seen this event, don't re-validate
|
||||
// Otherwise, check the signature and filters
|
||||
if (this.hasSeen(event, url)) {
|
||||
this.emit("duplicate", event, url)
|
||||
} else {
|
||||
if (!this.hasValidSignature(event, url)) {
|
||||
this.emit("invalid-signature", event, url)
|
||||
} else if (!matchFilters(this.opts.filters, event)) {
|
||||
this.emit("failed-filter", event, url)
|
||||
} else {
|
||||
this.emit("event", event, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEose = (url: string) => {
|
||||
const {executor, closeOnEose} = this.opts
|
||||
|
||||
this.emit("eose", url)
|
||||
|
||||
this.eose.add(url)
|
||||
|
||||
if (closeOnEose && this.eose.size >= executor.target.connections.length) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
close = () => {
|
||||
if (!this.closed) {
|
||||
const {target} = this.opts.executor
|
||||
|
||||
this.closed = Date.now()
|
||||
this.unsubscribe()
|
||||
this.emit("close")
|
||||
this.removeAllListeners()
|
||||
|
||||
target.connections.forEach((con: Connection) => con.off("close", this.closeHandlers.get(con.url)))
|
||||
target.cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from "./Connection"
|
||||
export * from "./ConnectionMeta"
|
||||
export * from "./Executor"
|
||||
export * from "./Pool"
|
||||
export * from "./Socket"
|
||||
export * from "./Subscription"
|
||||
export * from "./target/Multi"
|
||||
export * from "./target/Plex"
|
||||
export * from "./target/Relay"
|
||||
export * from "./target/Relays"
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@coracle.social/network",
|
||||
"version": "0.0.2",
|
||||
"author": "hodlbod",
|
||||
"license": "MIT",
|
||||
"description": "Utilities for connecting with nostr relays.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"types": "./build/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/index.d.ts",
|
||||
"import": "./build/index.mjs",
|
||||
"require": "./build/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "npm run lint && npm run rebuild && npm publish",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build": "tsc-multi",
|
||||
"clean": "gts clean",
|
||||
"lint": "gts lint",
|
||||
"fix": "gts fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/events": "^3.0.3",
|
||||
"gts": "^5.0.1",
|
||||
"tsc-multi": "^1.1.0",
|
||||
"typescript": "~5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coracle.social/util": "^0.0.2",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {Emitter} from '@coracle.social/lib'
|
||||
import type {Target} from '../Executor'
|
||||
import type {Message} from '../Socket'
|
||||
|
||||
export class Multi extends Emitter {
|
||||
constructor(readonly targets: Target[]) {
|
||||
super()
|
||||
|
||||
targets.forEach(t => {
|
||||
t.on('*', (verb, ...args) => this.emit(verb, ...args))
|
||||
})
|
||||
}
|
||||
|
||||
get connections() {
|
||||
return this.targets.flatMap(t => t.connections)
|
||||
}
|
||||
|
||||
send(...payload: Message) {
|
||||
this.targets.forEach(t => t.send(...payload))
|
||||
}
|
||||
|
||||
cleanup = () => {
|
||||
this.removeAllListeners()
|
||||
this.targets.forEach(t => t.cleanup())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {Emitter} from '@coracle.social/lib'
|
||||
import type {PlexMessage, Message} from '../Socket'
|
||||
import type {Connection} from '../Connection'
|
||||
|
||||
export class Plex extends Emitter {
|
||||
constructor(readonly urls: string[], readonly connection: Connection) {
|
||||
super()
|
||||
|
||||
this.connection.on('receive', this.onMessage)
|
||||
}
|
||||
|
||||
get connections() {
|
||||
return [this.connection]
|
||||
}
|
||||
|
||||
send = (...payload: Message) => {
|
||||
this.connection.send([{relays: this.urls}, payload])
|
||||
}
|
||||
|
||||
onMessage = (connection: Connection, [{relays}, [verb, ...payload]]: PlexMessage) => {
|
||||
this.emit(verb, relays[0], ...payload)
|
||||
}
|
||||
|
||||
cleanup = () => {
|
||||
this.removeAllListeners()
|
||||
this.connection.off('receive', this.onMessage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {Emitter} from '@coracle.social/lib'
|
||||
import type {Message} from '../Socket'
|
||||
import type {Connection} from '../Connection'
|
||||
|
||||
export class Relay extends Emitter {
|
||||
constructor(readonly connection: Connection) {
|
||||
super()
|
||||
|
||||
this.connection.on('receive', this.onMessage)
|
||||
}
|
||||
|
||||
get connections() {
|
||||
return [this.connection]
|
||||
}
|
||||
|
||||
send(...payload: Message) {
|
||||
this.connection.send(payload)
|
||||
}
|
||||
|
||||
onMessage = (connection: Connection, [verb, ...payload]: Message) => {
|
||||
this.emit(verb, connection.url, ...payload)
|
||||
}
|
||||
|
||||
cleanup = () => {
|
||||
this.removeAllListeners()
|
||||
this.connection.off('receive', this.onMessage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {Emitter} from '@coracle.social/lib'
|
||||
import type {Message} from '../Socket'
|
||||
import type {Connection} from '../Connection'
|
||||
|
||||
export class Relays extends Emitter {
|
||||
constructor(readonly connections: Connection[]) {
|
||||
super()
|
||||
|
||||
connections.forEach(connection => {
|
||||
connection.on('receive', this.onMessage)
|
||||
})
|
||||
}
|
||||
|
||||
send = (...payload: Message) => {
|
||||
this.connections.forEach(connection => {
|
||||
connection.send(payload)
|
||||
})
|
||||
}
|
||||
|
||||
onMessage = (connection: Connection, [verb, ...payload]: Message) => {
|
||||
this.emit(verb, connection.url, ...payload)
|
||||
}
|
||||
|
||||
cleanup = () => {
|
||||
this.removeAllListeners()
|
||||
this.connections.forEach(connection => {
|
||||
connection.off('receive', this.onMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"targets": [
|
||||
{"extname": ".cjs", "module": "commonjs"},
|
||||
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
|
||||
],
|
||||
"projects": ["tsconfig.json"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../node_modules/gts/tsconfig-google.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["es2019", "dom"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user