UNPKG

@fails-components/webtransport

Version:

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

401 lines (378 loc) 12.3 kB
import { WebTransportBase } from './webtransportbase.js' import { HttpWTSession } from './session.js' import { HttpClient } from './client.js' import { Http2WebTransportBrowser } from './http2/browser/browser.js' import { logger } from './utils.js' import { WebTransportError } from './error.js' const log = logger(`webtransport:browser()`) /** * @typedef {import('./dom').WebTransport} WebTransport * @typedef {import('./dom').WebTransportCloseInfo} WebTransportCloseInfo * @typedef {import('./dom').WebTransportBidirectionalStream} WebTransportBidirectionalStream * @typedef {import('./dom').WebTransportSendStream} WebTransportSendStream * @typedef {import('./dom').WebTransportSendStreamOptions} WebTransportSendStreamOptions * @typedef {import('./dom').WebTransportReceiveStream} WebTransportReceiveStream * @typedef {import('./dom').WebTransportSendGroup} WebTransportSendGroup */ /** * @template T * @typedef {import('node:stream/web').ReadableStream<T>} ReadableStream<T> */ let serverCertificateHashesNotSupported = false let webtransportSupported = false // @ts-ignore if (globalThis.WebTransport) { webtransportSupported = true try { // @ts-ignore // eslint-disable-next-line no-undef const transport = new WebTransport('https://127.0.0.1:23333/test', { serverCertificateHashes: [] }) transport.ready .then(() => { try { transport.close() // eslint-disable-next-line no-empty, no-unused-vars } catch (error) {} }) .catch(() => {}) } catch (error) { // @ts-ignore if (error?.name === 'NotSupportedError') { // note: we also do not support this, but http2 is a different transport // so we assume that the UDP and TCP part have different capabilities log('serverCertificateHashesNotSupported') serverCertificateHashesNotSupported = true } } } /** * @typedef {import('./dom').WebTransport} WebTransportInterface * * @implements {WebTransportInterface} */ export class WebTransportPonyfill extends WebTransportBase { /** * @param{{client: HttpClient, sessionint: HttpWTSession, ourl: URL}} args */ startUpConnection({ client, sessionint, ourl }) { const path = ourl.pathname + (ourl.search ?? '') client .handleConnection({ createTransport: true, path }) .then(() => client.createWTSession(sessionint, path)) .catch((error) => { client.closeHookSession() sessionint.readyReject(error) sessionint.closedReject(error) }) } get supportsReliableOnly() { return true } /** * @param{import('./types.js').HttpWebTransportInit} args * @return {{sessionint: HttpWTSession, client: HttpClient}} */ createClient(args) { this.curtype = 'websocket' const client = new HttpClient({ // eslint-disable-next-line no-unused-vars createReliableClient: (client) => { // @ts-ignore return new Http2WebTransportBrowser({ ...args }) }, ...args }) const sessionint = new HttpWTSession({ /* object: args.session, */ datagramsReadableMode: args.datagramsReadableMode, parentobj: client }) return { client, sessionint } } } export class WebTransportPolyfill { /** * @param {string} url * @param {import('./dom.js').WebTransportOptions} [args] */ constructor(url, args) { this.curtype = 'native' this.closeset = false this.allowFallback = true this.initiatedFallback = false this.args = args this.closed = new Promise((resolve, reject) => { this.closeRes = resolve this.closeRej = reject }) this.ready = new Promise((resolve, reject) => { this.readyRes = resolve this.readyRej = reject }) this.draining = new Promise((resolve, reject) => { this.drainingRes = resolve this.drainingRej = reject }) const initiateFallback = () => { this.initiatedFallback = true this.curtype = 'websocket' this.curtransport = new WebTransportPonyfill(url, args) this.curtransport.ready .then((val) => this.readyRes(val)) .catch((error) => this.readyRej(error)) this.curtransport.closed .then((val) => this.closeRes(val)) .catch((error) => this.closeRej(error)) this.curtransport.draining .then((val) => this.drainingRes(val)) .catch((error) => this.drainingRej(error)) } if ( webtransportSupported && (!args?.serverCertificateHashes || !serverCertificateHashesNotSupported) ) { /** @type {WebTransport|WebTransportPonyfill} */ // @ts-ignore // eslint-disable-next-line no-undef this.curtransport = new WebTransport(url, args) // if browser takes too long for waiting for client, we use the ponyfill setTimeout(() => { if (this.allowFallback && !this.closeset) { if ( !this.initiatedFallback && !this.curtransport?.supportsReliableOnly // way how browser signals support for http/2, no polyfill needed in this cases ) { const oldtransport = this.curtransport if (oldtransport) oldtransport.ready .then(async () => { oldtransport.close() }) .catch(() => {}) initiateFallback() } } }, 2000) this.curtransport.ready .then((val) => { this.allowFallback = false this.readyRes(val) }) .catch((error) => { if (this.allowFallback && !this.closeset) { if ( !this.initiatedFallback && !this.curtransport?.supportsReliableOnly // way how browser signals support for http/2, no polyfill needed in this cases ) { initiateFallback() } } else { this.readyRej(error) } }) this.curtransport.closed .then((val) => { if (this.curtype === 'native') this.closeRes(val) }) .catch((error) => { if (this.allowFallback && !this.closeset) { if ( !this.initiatedFallback && !this.curtransport?.supportsReliableOnly // way how browser signals support for http/2, no polyfill needed in this cases ) { initiateFallback() } } else { this.closeRej(error) } }) if (this.curtransport.draining) { // @ts-ignore this.curtransport.draining .then((/** @type {any} */ val) => { if (this.curtype === 'native') this.drainingRes(val) }) .catch((/** @type {WebTransportError} */ error) => { if (this.curtype === 'native') this.drainingRej(error) }) } } else { initiateFallback() } /** @type {import('./dom').WebTransportDatagramDuplexStream} */ // @ts-ignore this.datagrams = { // @ts-ignore get writable() { // @ts-ignore if (!this.datagramwritablepolyfilled_) { console.warn('datagrams.writable is deprecated') } // @ts-ignore return (this.datagramwritablepolyfilled_ ||= this.createWritable()) } } Object.defineProperties(this.datagrams, { maxDatagramSize: { get: () => { return this.curtransport.datagrams.maxDatagramSize } } }) // @ts-ignore this.datagrams.readable = new ReadableStream({ // eslint-disable-next-line no-unused-vars start: async (controller) => { await this.ready this.datagramsReader = this.curtransport.datagrams.readable.getReader() }, pull: async (controller) => { const { value, done } = await this.datagramsReader.read() if (value) controller.enqueue(value) if (done) controller.close() }, cancel: async (reason) => { await this.datagramsReader.cancel(reason) } }) // @ts-ignore this.datagrams.createWritable = (options) => { // @ts-ignore Must not exist for older browser if (this.curtransport.datagrams.createWritable) { return this.curtransport.datagrams.createWritable(options) } // we need to fallback to the old default writable // @ts-ignore if (!this.curtransport.datagrams.writable) { throw new WebTransportError('No way to send out datagrams') } let sendOrder = options?.sendOrder let sendGroup = options?.sendGroup const retWritable = new WritableStream({ // eslint-disable-next-line no-unused-vars start: async (controller) => { await this.ready if (!this.datagramsWriter) { this.datagramsWriter = // @ts-ignore this.curtransport.datagrams.writable.getWriter() } }, // eslint-disable-next-line no-unused-vars write: async (chunk, controller) => { await this.datagramsWriter.write(chunk) }, abort: async (reason) => { await this.datagramsWriter.abort(reason) }, close: async () => { await this.datagramsWriter.close() } }) Object.defineProperties(retWritable, { sendOrder: { get: () => { return sendOrder }, /** * @param {number} value */ set: (value) => { sendOrder = value } }, sendGroup: { get: () => { return sendGroup }, /** * @param {WebTransportSendGroup} value */ set: (value) => { if (value !== sendGroup) { sendGroup = value } } } }) return retWritable } this.incomingBidirectionalStreams = new ReadableStream({ // eslint-disable-next-line no-unused-vars start: async (controller) => { await this.ready this.incomingBidirectionalStreamsReader = this.curtransport.incomingBidirectionalStreams.getReader() }, pull: async (controller) => { const { value, done } = await this.incomingBidirectionalStreamsReader.read() if (value) controller.enqueue(value) if (done) controller.close() }, cancel: async (reason) => { await this.incomingBidirectionalStreamsReader.cancel(reason) } }) this.incomingUnidirectionalStreams = new ReadableStream({ // eslint-disable-next-line no-unused-vars start: async (controller) => { await this.ready this.incomingUnidirectionalStreamsReader = this.curtransport.incomingUnidirectionalStreams.getReader() }, pull: async (controller) => { const { value, done } = await this.incomingUnidirectionalStreamsReader.read() if (value) controller.enqueue(value) if (done) controller.close() }, cancel: async (reason) => { await this.incomingUnidirectionalStreamsReader.cancel(reason) } }) } get congestionControl() { // @ts-ignore return this.curtransport?.congestionControl || undefined } get reliability() { // @ts-ignore return this.curtransport?.reliability || undefined } get supportsReliableOnly() { return true } get protocol() { return this.curtransport?.protocol || undefined } getStats() { // @ts-ignore return this.curtransport.getStats() } /** * @param {WebTransportCloseInfo} [closeinfo] */ close(closeinfo) { this.closeset = true this.curtransport.close(closeinfo) } /** * @param {WebTransportSendStreamOptions} [opts] * @returns {Promise<WebTransportBidirectionalStream>} */ async createBidirectionalStream(opts) { await this.ready return await this.curtransport.createBidirectionalStream(opts) } /** * @param {WebTransportSendStreamOptions} [opts] * @returns {Promise<WebTransportSendStream>} */ async createUnidirectionalStream(opts) { await this.ready return await this.curtransport.createUnidirectionalStream(opts) } }