Files
welshman/packages/net/src/publish.ts
T
2026-05-28 11:30:54 -07:00

161 lines
4.1 KiB
TypeScript

import {fromPairs, once} from "@welshman/lib"
import {SignedEvent} from "@welshman/util"
import {RelayMessage, ClientMessageType, isRelayOk} from "./message.js"
import {AdapterEvent, AdapterContext, getAdapter} from "./adapter.js"
export enum PublishStatus {
Sending = "sending",
Pending = "pending",
Success = "success",
Failure = "failure",
Timeout = "timeout",
Aborted = "aborted",
}
export type PublishResult = {
status: PublishStatus
detail: string
relay: string
}
export type PublishOneOptions = {
event: SignedEvent
relay: string
signal?: AbortSignal
timeout?: number
context?: AdapterContext
onSuccess?: (result: PublishResult) => void
onFailure?: (result: PublishResult) => void
onPending?: (result: PublishResult) => void
onTimeout?: (result: PublishResult) => void
onAborted?: (result: PublishResult) => void
onComplete?: (result: PublishResult) => void
}
export const publishOne = (options: PublishOneOptions) =>
new Promise<PublishResult>(resolve => {
const adapter = getAdapter(options.relay, options.context)
const result = {
relay: options.relay,
status: PublishStatus.Pending,
detail: "",
}
options.onPending?.(result)
let timeoutId: ReturnType<typeof setTimeout> | null = null
const abort = () => {
if (result.status === PublishStatus.Pending) {
result.status = PublishStatus.Aborted
result.detail = "aborted"
options.onAborted?.(result)
}
cleanup()
}
const cleanup = once(() => {
options.signal?.removeEventListener("abort", abort)
options.onComplete?.(result)
clearTimeout(timeoutId)
adapter.cleanup()
resolve(result)
})
adapter.on(AdapterEvent.Receive, (message: RelayMessage, url: string) => {
if (isRelayOk(message)) {
const [_, id, ok, detail] = message
if (id !== options.event.id) return
if (ok) {
result.status = PublishStatus.Success
result.detail = detail
options.onSuccess?.(result)
} else {
result.status = PublishStatus.Failure
result.detail = detail
options.onFailure?.(result)
}
cleanup()
}
})
if (options.signal) {
options.signal.addEventListener("abort", abort)
}
timeoutId = setTimeout(() => {
if (result.status === PublishStatus.Pending) {
result.status = PublishStatus.Timeout
result.detail = "timed out"
options.onTimeout?.(result)
}
cleanup()
}, options.timeout || 10_000)
adapter.send([ClientMessageType.Event, options.event])
})
export type PublishResultsByRelay = Record<string, PublishResult>
export type PublishOptions = {
event: SignedEvent
relays: string[]
signal?: AbortSignal
timeout?: number
context?: AdapterContext
onSuccess?: (result: PublishResult) => void
onFailure?: (result: PublishResult) => void
onPending?: (result: PublishResult) => void
onTimeout?: (result: PublishResult) => void
onAborted?: (result: PublishResult) => void
onComplete?: (result: PublishResult) => void
}
export const publish = async (options: PublishOptions): Promise<PublishResultsByRelay> => {
const {event, timeout, signal, context} = options
const completed = new Set<string>()
const relays = new Set(options.relays)
if (relays.size !== options.relays.length) {
console.warn("Non-unique relays passed to publish")
}
return fromPairs(
await Promise.all(
options.relays.map(async relay => {
const result = await publishOne({
event,
relay,
signal,
timeout,
context,
onSuccess: options.onSuccess,
onFailure: options.onFailure,
onPending: options.onPending,
onTimeout: options.onTimeout,
onAborted: options.onAborted,
onComplete: (result: PublishResult) => {
completed.add(relay)
if (completed.size === relays.size) {
options.onComplete?.(result)
}
},
})
return [relay, result]
}),
),
)
}