UNPKG

@fails-components/webtransport

Version:

A component to add webtransport support (server and client) to node.js using libquiche

220 lines (195 loc) 6.53 kB
import { WebTransportError } from './error.js' import { logger } from './utils.js' const pid = typeof process !== 'undefined' ? process.pid : 0 const log = logger(`webtransport:httpclient(${pid})`) /** * @typedef {import('./session').HttpWTSession} HttpWTSession * @typedef {import('./types').HttpClientInit} HttpClientInit * * Http3Client events * @typedef {import('./types').HttpClientEventHandler} HttpClientEventHandler * @typedef {import('./types').ClientConnectedEvent} ClientConnectedEvent * @typedef {import('./types').ClientWebtransportSupportEvent} ClientWebtransportSupportEvent * @typedef {import('./types').HttpWTSessionVisitorEvent} HttpWTSessionVisitorEvent */ /** * @implements {HttpClientEventHandler} */ export class HttpClient { /** * @param {HttpClientInit} args */ constructor(args) { /** @type {HttpClientInit| undefined} */ this.args = args /** @type {{ resolve: (value?: any) => void, reject: (err?: Error) => void} | null | undefined} */ this.sessionProm = null /** @type {Promise<void> | undefined} */ this.sessionobj = new Promise((resolve, reject) => { this.sessionProm = { resolve, reject } }).catch(() => {}) // add default handler if no one cares /** @type {HttpWTSession | null | undefined} */ this.sessionobjint = null this.closeHookSession = this.closeHookSession.bind(this) /** @type {{ resolve: (value?: any) => void, reject: (err?: Error) => void} | null | undefined} */ this.webtransportProm = null /** @type {{ resolve: (value?: any) => void, reject: (err?: Error) => void} | null | undefined} */ this.quicconnectedProm = null this._quicConnectTimeout = args.quicConnectTimeout ?? 8000 this._webTransportConnectTimeout = args.webTransportConnectTimeout ?? 2000 } /** * @param {Object} args * @param {boolean} args.createTransport * @param {string} args.path */ async handleConnection({ createTransport, path }) { if (createTransport) this.createTransportInt({ path }) this.quicconnected = new Promise((resolve, reject) => { this.quicconnectedProm = { resolve, reject } }) this.webtransport = new Promise((resolve, reject) => { this.webtransportProm = { resolve, reject } }) const timeout = setTimeout(() => { if (this.quicconnectedProm) { log.error('quic connection timeout') this.quicconnectedProm.reject( new WebTransportError('Opening handshake failed.') ) delete this.quicconnectedProm } }, this._quicConnectTimeout) try { await this.quicconnected } finally { clearTimeout(timeout) } } /** * @param {HttpWTSession} sessionobj * @param {string} path * @returns */ async createWTSession(sessionobj, path) { // now create Webtransport session const timeout = setTimeout(() => { if (this.webtransportProm) { log.error('webtransport connection timeout') this.webtransportProm.reject( new WebTransportError('Opening handshake failed.') ) delete this.webtransportProm } }, this._webTransportConnectTimeout) await this.webtransport // wait for webtransport support clearTimeout(timeout) // ok now we open the session this.sessionobjint = sessionobj this.transportInt.openWTSession(path) // we wait for a new session const sessobj = await this.sessionobj delete this.sessionobj return sessobj } closeHookSession() { if (this.transportInt != null) { this.transportInt.closeClient() } this.stopped = true } /** * @param {import('./types').SessionCloseEvent} args */ onClientError(args) { if (this.sessionobjint != null) { this.sessionobjint.onClose(args) } } /** * @param {ClientConnectedEvent} args */ onClientConnected(args) { this.transportIntSwitchToReliable = undefined if (this.quicconnectedProm) { if (args.success) this.quicconnectedProm.resolve() else this.quicconnectedProm.reject( new WebTransportError('Opening handshake failed.') ) delete this.quicconnectedProm } else if (args.success) throw new WebTransportError('Client connected with no pending promise') } /** * @param {ClientWebtransportSupportEvent} args */ // eslint-disable-next-line no-unused-vars onClientWebTransportSupport(args) { if (this.webtransportProm) { this.webtransportProm.resolve() delete this.webtransportProm } } /** * @param {HttpWTSessionVisitorEvent} args */ onHttpWTSessionVisitor(args) { // create Http Visitor if (args.session && this.sessionProm && this.sessionobjint) { this.sessionobjint.setSessionObj(args.session, !!args.reliable) args.session.jsobj.closeHook = this.closeHookSession delete this.sessionobjint this.sessionProm.resolve(args.session) delete this.sessionProm } else { throw new WebTransportError( 'Http3WTSessionVisitor no object session or nor sessionprom' ) } } /** * @param{{path: string}} [args] **/ createTransportInt(args) { const path = args?.path if (this.transportInt != null) { return } try { // @ts-ignore if (this.args?.forceReliable || !this.args.createUnreliableClient) { // internal option for unit tests only // @ts-ignore this.transportInt = this.args.createReliableClient(this) } else { // @ts-ignore this.transportInt = this.args.createUnreliableClient(this) if (!this.args?.requireUnreliable) { const args = this.args this.transportIntSwitchToReliable = () => { if (this.transportInt != null) { this.transportInt.closeClient() } // @ts-ignore this.transportInt = args.createReliableClient(this) this.transportInt.jsobj = this if (this.transportInt.createTransport) { this.transportInt.createTransport({ path }) } this.transportIntSwitchToReliable = undefined } } } } catch (/** @type {any} */ err) { const error = new WebTransportError('Opening handshake failed.') error.stack = err.stack throw error } delete this.args this.transportInt.jsobj = this if (this.transportInt.createTransport) { this.transportInt.createTransport({ path }) } } }