@fails-components/webtransport
Version:
A component to add webtransport support (server and client) to node.js using libquiche
356 lines (331 loc) • 10 kB
JavaScript
/**
* @typedef {import('http2').Http2Stream} Http2Stream
* @typedef {import('../dom.js').WebTransportSendGroup} WebTransportSendGroup
* @typedef {import('../dom.js').WebTransportSendStreamOptions} WebTransportSendStreamOptions
*/
import { ParserBase } from './parserbase.js'
import { FlowController } from './flowcontroller.js'
import { logger } from '../utils.js'
import { StreamIdManager } from './streamidmanager.js'
const pid = typeof process !== 'undefined' ? process.pid : 0
const log = logger(`webtransport:http2webtransportsession(${pid})`)
let processnextTick = (/** @type {{ (args: any[]): any }} */ func) =>
setTimeout(func, 0)
// @ts-ignore
if (typeof process !== 'undefined') processnextTick = process.nextTick
export class Http2WebTransportSession {
/**
* @param {{stream?: Http2Stream, ws?: WebSocket, isclient:boolean,
* createParser:import('../types.js').CreateParserFunction
* sendWindowOffset: Number,
* receiveWindowOffset: Number,
* shouldAutoTuneReceiveWindow: boolean
* receiveWindowSizeLimit: Number,
* initialBidirectionalSendStreams: Number,
* initialBidirectionalReceiveStreams: Number,
* initialUnidirectionalSendStreams: Number,
* initialUnidirectionalReceiveStreams: Number}} args
* */
constructor({
stream,
ws,
isclient,
createParser,
sendWindowOffset,
receiveWindowOffset,
shouldAutoTuneReceiveWindow,
receiveWindowSizeLimit,
initialBidirectionalSendStreams,
initialBidirectionalReceiveStreams,
initialUnidirectionalSendStreams,
initialUnidirectionalReceiveStreams
}) {
// @ts-ignore
this.jsobj = undefined // the creator will set this
if (stream) {
this.stream = stream
} else if (ws) {
this.ws = ws
} else throw new Error('Neither stream or websocket supplied')
this.capsParser = createParser(this)
this.isclient = isclient
this.flowController = new FlowController({
tocontrol: this,
sendWindowOffset,
receiveWindowOffset,
shouldAutoTuneReceiveWindow,
receiveWindowSizeLimit
})
this.streamIdMngrUni = new StreamIdManager({
delegate: this,
unidirectional: true,
isclient,
maxAllowedIncomingStreams: initialUnidirectionalReceiveStreams,
maxAllowedOutgoingStreams: initialUnidirectionalSendStreams
})
this.streamIdMngrBi = new StreamIdManager({
delegate: this,
unidirectional: false,
isclient,
maxAllowedIncomingStreams: initialBidirectionalReceiveStreams,
maxAllowedOutgoingStreams: initialBidirectionalSendStreams
})
/** @type {Array<Uint8Array>} */
this.datagramsWaiting_ = []
/** @type {Array<{sendOrder: number, sendGroupId: bigint}>} */
this.orderUniStreams = []
/** @type {Array<{sendOrder: number, sendGroupId: bigint}>} */
this.orderBiStreams = []
if (stream) {
if (isclient) {
stream.on('response', (headers) => {
processnextTick(() => {
if (headers[':status'] === 200) {
const beReady = {}
if (stream && headers['wt-protocol']) {
// http/2 case
// @ts-ignore
beReady.protocol = headers['wt-protocol']
}
// on ready
this.jsobj.onReady(beReady)
} else {
this.jsobj.onClose({
errorcode: headers[':status'],
error: 'Session stream errored'
})
}
})
})
} else {
processnextTick(() => {
this.jsobj.onReady({})
})
}
}
}
sendInitialParameters() {
let skip = false //skips the initial parameters at environment with settings
if (typeof process !== 'undefined') {
if (process.version) {
const majorVersion = parseInt(
process.version.split('.')[0].substring(1)
)
if (majorVersion >= 20) skip = true
}
}
if (!skip || this.capsParser.initialParametersMandatory()) {
this.flowController.sendWindowUpdate()
this.streamIdMngrBi.sendMaxStreamsFrameInitial()
this.streamIdMngrUni.sendMaxStreamsFrameInitial()
}
}
drainWrites() {
while (!this.capsParser.blocked && this.datagramsWaiting_.length > 0) {
const outChunk = this.datagramsWaiting_.shift()
this.capsParser.writeCapsule({
type: ParserBase.DATAGRAM,
headerVints: [],
payload: outChunk
})
}
if (this.datagramsWaiting_.length > 0) {
this.capsParser.scheduleDrainWrites()
}
}
/**
* @param {Uint8Array} chunk
* @return {{ code: "success" | "blocked" | "internalError" | "tooBig", message?: string | undefined; }}
*/
writeDatagram(chunk) {
if (chunk.byteLength > this.getMaxDatagramSize()) return { code: 'tooBig' }
if (this.capsParser.blocked) {
this.datagramsWaiting_.push(chunk)
this.capsParser.scheduleDrainWrites()
return { code: 'blocked' }
}
this.capsParser.writeCapsule({
type: ParserBase.DATAGRAM,
headerVints: [],
payload: chunk
})
return { code: 'success' }
}
trySendingUnidirectionalStreams() {
while (
this.orderUniStreams.length > 0 &&
this.streamIdMngrUni.canOpenNextOutgoingStream()
) {
const streamid = this.streamIdMngrUni.getNextOutgoingStreamId()
const priority = this.orderUniStreams.pop()
this.capsParser.writeCapsule({
type: ParserBase.WT_STREAM_WOFIN,
headerVints: [streamid],
payload: undefined
})
this.capsParser.newStream(
streamid,
priority || { sendGroupId: 0n, sendOrder: 0 }
)
}
}
/**
* @param {WebTransportSendStreamOptions} opts
*/
orderUnidiStream({ sendGroup, sendOrder, waitUntilAvailable }) {
const canopen = this.streamIdMngrUni.canOpenNextOutgoingStream()
const maxset = this.streamIdMngrUni.isMaxStreamSet() // we block if the maxsetting did not arrive
if (canopen || waitUntilAvailable || !maxset) {
this.orderUniStreams.push({
// @ts-ignore
sendGroupId: sendGroup?._sendGroupId || 0n,
sendOrder: sendOrder ?? 0
})
this.trySendingUnidirectionalStreams()
return true
}
return false
}
trySendingBidirectionalStreams() {
while (
this.orderBiStreams.length > 0 &&
this.streamIdMngrBi.canOpenNextOutgoingStream()
) {
const streamid = this.streamIdMngrBi.getNextOutgoingStreamId()
const priority = this.orderBiStreams.pop()
this.capsParser.writeCapsule({
type: ParserBase.WT_STREAM_WOFIN,
headerVints: [streamid],
payload: undefined
})
this.capsParser.newStream(
streamid,
priority || { sendGroupId: 0n, sendOrder: 0 }
)
}
}
/**
* @param {WebTransportSendStreamOptions} opts
*/
orderBidiStream({ sendGroup, sendOrder, waitUntilAvailable }) {
const canopen = this.streamIdMngrBi.canOpenNextOutgoingStream()
const maxset = this.streamIdMngrBi.isMaxStreamSet() // we block if the maxsetting did not arrive
if (canopen || waitUntilAvailable || !maxset) {
this.orderBiStreams.push({
// @ts-ignore
sendGroupId: sendGroup?._sendGroupId || 0n,
sendOrder: sendOrder ?? 0
})
this.trySendingBidirectionalStreams()
return true
}
return false
}
orderSessionStats() {
this.jsobj.onSessionStats({
timestamp: 0,
expiredOutgoing: 0n,
lostOutgoing: 0n,
// non Datagram
minRtt: 0,
smoothedRtt: 0,
rttVariation: 0,
estimatedSendRateBps: 0n
})
}
orderDatagramStats() {
this.jsobj.onDatagramStats({
timestamp: 0,
expiredOutgoing: 0n,
lostOutgoing: 0n
})
}
getMaxDatagramSize() {
return 16384 // this completly arbitry, we do not have a real restriction, but we choose more than quiche, to make things interesting
}
/*
* @returns {void}
*/
notifySessionDraining() {}
/**
* @param {{ code: number, reason: string }} arg
*/
close({ code, reason }) {
this.capsParser.sendClose({ code, reason }) // this includes for ws closing the session!
// should also close the stream
}
/**
* @param {bigint} windowOffset
*/
sendWindowUpdate(windowOffset) {
this.capsParser.writeCapsule({
type: ParserBase.WT_MAX_DATA,
headerVints: [windowOffset],
payload: undefined
})
}
/**
* @param {bigint} pos
*/
reportBlocked(pos) {
log('Session was blocked at:', pos)
}
/**
* @param {bigint} windowOffset
*/
sendBlocked(windowOffset) {
this.capsParser.writeCapsule({
type: ParserBase.WT_DATA_BLOCKED,
headerVints: [windowOffset],
payload: undefined
})
}
connected() {
return this.jsobj.state === 'connected'
}
/**
* @param {{ code: number, reason: string }} arg
*/
closeConnection({ code, reason }) {
// called in case of failure in parsing or flowcontrol
this.jsobj.onClose({
errorcode: code,
error: reason
})
this.close({ code, reason })
}
smoothedRtt() {
let toret
if (this.stream) {
// we are on node
// @ts-ignore
toret = this.stream.session?.WTrtt || 25
} else if (this.ws) {
// we are at the Browser, so we use the connection rtt?
// @ts-ignore
// eslint-disable-next-line no-undef
toret = navigator?.connection?.rtt || 25
}
toret = Math.ceil(toret / 25) * 25 // to do be to accurate!
return toret
}
/**
* @returns {boolean}
*/
canSendMaxStreams() {
return true
}
/**
* @param {bigint} maxStreams
* @param {boolean} unidirectional
*/
sendMaxStreams(maxStreams, unidirectional) {
this.capsParser.writeCapsule({
type: unidirectional
? ParserBase.WT_MAX_STREAMS_UNIDI
: ParserBase.WT_MAX_STREAMS_BIDI,
headerVints: [maxStreams],
payload: undefined
})
}
}