optionally take an AbortSignal on subscriptions.

This commit is contained in:
fiatjaf
2026-01-22 21:49:39 -03:00
parent 6ebe59f123
commit 9078f45a64
2 changed files with 45 additions and 18 deletions

View File

@@ -23,6 +23,7 @@ export type AbstractPoolConstructorOptions = AbstractRelayConstructorOptions & {
export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose'> & { export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose'> & {
maxWait?: number maxWait?: number
abort?: AbortSignal
onclose?: (reasons: string[]) => void onclose?: (reasons: string[]) => void
onauth?: (event: EventTemplate) => Promise<VerifiedEvent> onauth?: (event: EventTemplate) => Promise<VerifiedEvent>
id?: string id?: string
@@ -50,7 +51,13 @@ export class AbstractSimplePool {
this.automaticallyAuth = opts.automaticallyAuth this.automaticallyAuth = opts.automaticallyAuth
} }
async ensureRelay(url: string, params?: { connectionTimeout?: number }): Promise<AbstractRelay> { async ensureRelay(
url: string,
params?: {
connectionTimeout?: number
abort?: AbortSignal
},
): Promise<AbstractRelay> {
url = normalizeURL(url) url = normalizeURL(url)
let relay = this.relays.get(url) let relay = this.relays.get(url)
@@ -66,7 +73,6 @@ export class AbstractSimplePool {
this.relays.delete(url) this.relays.delete(url)
} }
} }
if (params?.connectionTimeout) relay.connectionTimeout = params.connectionTimeout
this.relays.set(url, relay) this.relays.set(url, relay)
} }
@@ -77,7 +83,10 @@ export class AbstractSimplePool {
} }
} }
await relay.connect() await relay.connect({
timeout: params?.connectionTimeout,
abort: params?.abort,
})
return relay return relay
} }
@@ -176,6 +185,7 @@ export class AbstractSimplePool {
try { try {
relay = await this.ensureRelay(url, { relay = await this.ensureRelay(url, {
connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined, connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined,
abort: params.abort,
}) })
} catch (err) { } catch (err) {
handleClose(i, (err as any)?.message || String(err)) handleClose(i, (err as any)?.message || String(err))
@@ -198,6 +208,7 @@ export class AbstractSimplePool {
}, },
alreadyHaveEvent: localAlreadyHaveEventHandler, alreadyHaveEvent: localAlreadyHaveEventHandler,
eoseTimeout: params.maxWait, eoseTimeout: params.maxWait,
abort: params.abort,
}) })
}) })
.catch(err => { .catch(err => {
@@ -209,6 +220,7 @@ export class AbstractSimplePool {
}, },
alreadyHaveEvent: localAlreadyHaveEventHandler, alreadyHaveEvent: localAlreadyHaveEventHandler,
eoseTimeout: params.maxWait, eoseTimeout: params.maxWait,
abort: params.abort,
}) })
subs.push(subscription) subs.push(subscription)

View File

@@ -35,7 +35,6 @@ export class AbstractRelay {
public onauth: undefined | ((evt: EventTemplate) => Promise<VerifiedEvent>) public onauth: undefined | ((evt: EventTemplate) => Promise<VerifiedEvent>)
public baseEoseTimeout: number = 4400 public baseEoseTimeout: number = 4400
public connectionTimeout: number = 4400
public publishTimeout: number = 4400 public publishTimeout: number = 4400
public pingFrequency: number = 29000 public pingFrequency: number = 29000
public pingTimeout: number = 20000 public pingTimeout: number = 20000
@@ -43,7 +42,6 @@ export class AbstractRelay {
public openSubs: Map<string, Subscription> = new Map() public openSubs: Map<string, Subscription> = new Map()
public enablePing: boolean | undefined public enablePing: boolean | undefined
public enableReconnect: boolean public enableReconnect: boolean
private connectionTimeoutHandle: ReturnType<typeof setTimeout> | undefined
private reconnectTimeoutHandle: ReturnType<typeof setTimeout> | undefined private reconnectTimeoutHandle: ReturnType<typeof setTimeout> | undefined
private pingIntervalHandle: ReturnType<typeof setInterval> | undefined private pingIntervalHandle: ReturnType<typeof setInterval> | undefined
private reconnectAttempts: number = 0 private reconnectAttempts: number = 0
@@ -70,9 +68,12 @@ export class AbstractRelay {
this.enableReconnect = opts.enableReconnect || false this.enableReconnect = opts.enableReconnect || false
} }
static async connect(url: string, opts: AbstractRelayConstructorOptions): Promise<AbstractRelay> { static async connect(
url: string,
opts: AbstractRelayConstructorOptions & Parameters<AbstractRelay['connect']>[0],
): Promise<AbstractRelay> {
const relay = new AbstractRelay(url, opts) const relay = new AbstractRelay(url, opts)
await relay.connect() await relay.connect(opts)
return relay return relay
} }
@@ -131,23 +132,31 @@ export class AbstractRelay {
} }
} }
public async connect(): Promise<void> { public async connect(opts?: { timeout?: number; abort?: AbortSignal }): Promise<void> {
let connectionTimeoutHandle: ReturnType<typeof setTimeout> | undefined
if (this.connectionPromise) return this.connectionPromise if (this.connectionPromise) return this.connectionPromise
this.challenge = undefined this.challenge = undefined
this.authPromise = undefined this.authPromise = undefined
this.connectionPromise = new Promise((resolve, reject) => { this.connectionPromise = new Promise((resolve, reject) => {
this.connectionTimeoutHandle = setTimeout(() => { if (opts?.timeout) {
connectionTimeoutHandle = setTimeout(() => {
reject('connection timed out') reject('connection timed out')
this.connectionPromise = undefined this.connectionPromise = undefined
this.onclose?.() this.onclose?.()
this.closeAllSubscriptions('relay connection timed out') this.closeAllSubscriptions('relay connection timed out')
}, this.connectionTimeout) }, opts.timeout)
}
if (opts?.abort) {
opts.abort.onabort = reject
}
try { try {
this.ws = new this._WebSocket(this.url) this.ws = new this._WebSocket(this.url)
} catch (err) { } catch (err) {
clearTimeout(this.connectionTimeoutHandle) clearTimeout(connectionTimeoutHandle)
reject(err) reject(err)
return return
} }
@@ -157,7 +166,7 @@ export class AbstractRelay {
clearTimeout(this.reconnectTimeoutHandle) clearTimeout(this.reconnectTimeoutHandle)
this.reconnectTimeoutHandle = undefined this.reconnectTimeoutHandle = undefined
} }
clearTimeout(this.connectionTimeoutHandle) clearTimeout(connectionTimeoutHandle)
this._connected = true this._connected = true
const isReconnection = this.reconnectAttempts > 0 const isReconnection = this.reconnectAttempts > 0
@@ -183,13 +192,13 @@ export class AbstractRelay {
} }
this.ws.onerror = ev => { this.ws.onerror = ev => {
clearTimeout(this.connectionTimeoutHandle) clearTimeout(connectionTimeoutHandle)
reject((ev as any).message || 'websocket error') reject((ev as any).message || 'websocket error')
this.handleHardClose('relay connection errored') this.handleHardClose('relay connection errored')
} }
this.ws.onclose = ev => { this.ws.onclose = ev => {
clearTimeout(this.connectionTimeoutHandle) clearTimeout(connectionTimeoutHandle)
reject((ev as any).message || 'websocket closed') reject((ev as any).message || 'websocket closed')
this.handleHardClose('relay connection closed') this.handleHardClose('relay connection closed')
} }
@@ -437,6 +446,11 @@ export class AbstractRelay {
): Subscription { ): Subscription {
const sub = this.prepareSubscription(filters, params) const sub = this.prepareSubscription(filters, params)
sub.fire() sub.fire()
if (params.abort) {
params.abort.onabort = () => sub.close(String(params.abort!.reason || '<aborted>'))
}
return sub return sub
} }
@@ -563,6 +577,7 @@ export type SubscriptionParams = {
alreadyHaveEvent?: (id: string) => boolean alreadyHaveEvent?: (id: string) => boolean
receivedEvent?: (relay: AbstractRelay, id: string) => void receivedEvent?: (relay: AbstractRelay, id: string) => void
eoseTimeout?: number eoseTimeout?: number
abort?: AbortSignal
} }
export type CountResolver = { export type CountResolver = {