@fails-components/webtransport
Version:
A component to add webtransport support (server and client) to node.js using libquiche
460 lines (424 loc) • 12.4 kB
JavaScript
import { FlowController } from './flowcontroller.js'
import { ParserBase } from './parserbase.js'
import { logger } from '../utils.js'
import { WebTransportError } from '../error.js'
const pid = typeof process !== 'undefined' ? process.pid : 0
const log = logger(`webtransport:http2webtransportstream(${pid})`)
/**
* WebTransport stream events
* @typedef {import('../types').WebTransportStreamEventHandler} WebTransportStreamEventHandler
* @typedef {import('../types').StreamRecvSignalEvent} StreamRecvSignalEvent
* @typedef {import('../types').StreamReadEvent} StreamReadEvent
* @typedef {import('../types').StreamWriteEvent} StreamWriteEvent
* @typedef {import('../types').StreamNetworkFinishEvent} StreamNetworkFinishEvent
*
* @typedef {import('../types').ReadDataInt} ReadDataInt
* @typedef {import('./streamidmanager.js').StreamIdManager} StreamIdManager
*/
let processnextTick = (/** @type {{ (): void; }} */ func) =>
setTimeout(func, 0)
// @ts-ignore
if (typeof process !== 'undefined') processnextTick = process.nextTick
export class Http2WebTransportStream {
/**
* @param {{streamid: bigint,
* unidirectional: boolean,
* incoming: boolean,
* capsuleParser: ParserBase
* sendWindowOffset: Number,
* receiveWindowOffset: Number,
* shouldAutoTuneReceiveWindow: boolean
* receiveWindowSizeLimit: Number,
* sessionFlowController: FlowController,
* streamIdManager: StreamIdManager}} args
* */
constructor({
streamid,
unidirectional,
incoming,
capsuleParser,
sendWindowOffset,
receiveWindowOffset,
shouldAutoTuneReceiveWindow,
receiveWindowSizeLimit,
sessionFlowController,
streamIdManager
}) {
/** @type {import('../stream').HttpWTStream} */
// @ts-ignore
this.jsobj = undefined // the creator will set this
this.streamid = streamid
/** @type {Array<ReadDataInt>} */
this.incomdata = []
this.capsuleParser = capsuleParser
/** @type {Array<{buf?:Uint8Array,fin:boolean}>} */
this.outgochunks = []
this.flowController = new FlowController({
tocontrol: this,
sendWindowOffset,
receiveWindowOffset,
shouldAutoTuneReceiveWindow,
receiveWindowSizeLimit,
sessionFlowController
})
this.sessionFlowController = sessionFlowController
this.streamIdManager = streamIdManager
this.final = false
this.finalmessagesend = false
this.stopReading_ = true
this.drainReads_ = true
this.recvBytes = 0
this.outgoingClosed_ = false
this.incomingClosed_ = false
if (unidirectional) {
if (incoming) {
this.outgoingClosed_ = true
} else {
this.incomingClosed_ = true
}
}
}
sendInitialParameters() {
this.flowController.sendWindowUpdate()
}
/**
* @param {Object} obj
* @param {Uint8Array|undefined} obj.data
* @param {Boolean} obj.fin
*/
recvData({ data, fin }) {
this.incomdata.push({ data, fin })
if (data && data?.byteLength > 0) {
const checkstream = this.flowController.updateHighestReceivedOffset(
data?.byteLength
)
const checksession =
this.sessionFlowController.updateHighestReceivedOffset(data?.byteLength)
if (checksession && checkstream) {
// As the highest received offset has changed, check to see if this is a
// violation of flow control.
if (
this.flowController.flowControlViolation() ||
this.sessionFlowController.flowControlViolation()
) {
this.closeConnection({
code: 63 /* QUIC_FLOW_CONTROL_SENT_TOO_MUCH_DATA */,
reason: 'Flow control violation after increasing offset'
})
return
}
}
}
this.processRead()
if (this.incomdata.length > 0) {
if (!this.stopReading_) this.processRead()
}
}
processRead() {
if (!this.jsobj) return
let buffer
let bufferoffset = 0
while (
this.incomdata.length > 0 &&
(!this.stopReading_ || this.drainReads_)
) {
if (!buffer) {
const bytes = this.incomdata.reduce(
(prevVal, val) => prevVal + ((val && val.data?.byteLength) || 0),
0
)
if (bytes > 0) {
buffer = this.jsobj.getReadBuffer({
byteSize: this.incomdata.reduce(
(prevVal, val) => prevVal + ((val && val.data?.byteLength) || 0),
0
)
})
buffer.readBytes = 0
bufferoffset = 0
}
}
const cur = this.incomdata.shift()
if (cur?.data && cur.data.byteLength > 0 && buffer && buffer.buffer) {
const len = Math.min(
buffer.buffer.byteLength - bufferoffset,
cur.data.byteLength
)
const srcview = new Uint8Array(
cur.data.buffer,
cur.data.byteOffset,
len
)
const destview = new Uint8Array(buffer.buffer.buffer, bufferoffset, len)
destview.set(srcview)
bufferoffset += len
// @ts-ignore
buffer.readBytes += len
buffer.drained = true
if (cur.data.byteLength !== len) {
buffer.drained = false
this.incomdata.unshift({
data: new Uint8Array(
cur.data.buffer,
cur.data.byteOffset + len,
cur.data.byteLength - len
),
fin: cur.fin
})
buffer.fin = false // next round
} else {
buffer.fin ||= cur.fin
}
if (this.incomdata.length > 0) buffer.drained = false
if (
bufferoffset === buffer.buffer.byteLength ||
this.incomdata.length === 0
) {
this.flowController.addBytesConsumed(buffer.readBytes || 0)
this.sessionFlowController.addBytesConsumed(buffer.readBytes || 0)
const { stopReading } = this.jsobj.commitReadBuffer(buffer)
if (stopReading) this.stopReading_ = true
buffer = undefined
bufferoffset = 0
}
this.recvBytes += len
} else if (cur?.fin) {
this.jsobj.commitReadBuffer({ fin: true })
}
}
}
startReading() {
this.stopReading_ = false
this.processRead()
}
drainReads() {
this.drainReads_ = true
this.stopReading_ = false
this.processRead()
}
stopReading() {
this.stopReading_ = true
}
/**
* @param {'resetStream'|'stopSending'} type
*/
onStreamSignal(type) {
switch (type) {
case 'resetStream':
this.closeIncoming()
break
case 'stopSending':
this.closeOutgoing()
break
}
}
closeIncoming() {
this.incomingClosed_ = true
if (this.outgoingClosed_) this.onClose()
}
closeOutgoing() {
this.outgoingClosed_ = true
if (this.incomingClosed_) this.onClose()
}
onClose() {
// ok, inform our session id manager
this.streamIdManager.onStreamClosed(this.streamid)
}
/**
* @param {Number} code
*/
stopSending(code) {
this.closeIncoming()
this.capsuleParser.writeCapsule({
type: ParserBase.WT_STOP_SENDING,
headerVints: [this.streamid, code],
payload: undefined
})
processnextTick(() =>
this.jsobj.onStreamNetworkFinish({
nettask: 'stopSending'
})
)
}
onFin() {
this.closeIncoming()
}
/**
* @param {Number} code
*/
resetStream(code) {
this.closeOutgoing()
this.capsuleParser.writeCapsule({
type: ParserBase.WT_RESET_STREAM,
headerVints: [this.streamid, code],
payload: undefined
})
processnextTick(() =>
this.jsobj.onStreamNetworkFinish({
nettask: 'resetStream'
})
)
}
/**
* @param {Uint8Array} buf
*/
writeChunk(buf) {
this.outgochunks.push({ buf, fin: false })
this.capsuleParser.scheduler.Schedule(this.streamid)
this.capsuleParser.scheduleDrainWrites()
}
hasPendingData() {
return this.outgochunks.length > 0
}
drainWrites() {
let finsend = false
while (
this.outgochunks.length > 0 &&
(!this.capsuleParser.blocked ||
this.final ||
!this.capsuleParser.shouldYieldStream(this.streamid)) &&
this.flowController.sendWindowSize() > 0n &&
this.sessionFlowController.sendWindowSize() > 0n
) {
const cur = this.outgochunks.shift()
let outgoChunkSend = true
if (cur) {
let payload = cur.buf
if (payload) {
if (payload.byteLength === 0 && !cur.fin) {
throw new WebTransportError(
'Trying to send zero length capsule without a fin'
)
}
const sessWindow = this.sessionFlowController.sendWindowSize()
const streamWindow = this.flowController.sendWindowSize()
if (
payload?.byteLength > streamWindow ||
payload?.byteLength > sessWindow
) {
const len =
sessWindow > streamWindow
? Number(streamWindow)
: Number(sessWindow)
// ok we have to split
{
const src = new Uint8Array(
payload.buffer,
payload.byteOffset + len,
payload.byteLength - len
)
const dest = new Uint8Array(payload.byteLength - len)
dest.set(src)
this.outgochunks.unshift({ fin: cur.fin, buf: dest })
outgoChunkSend = false // remaing part is send later
}
cur.fin = false
payload = cur.buf = new Uint8Array(
payload.buffer,
payload.byteOffset,
len
)
}
}
this.capsuleParser.writeCapsule({
type: cur?.fin
? ParserBase.WT_STREAM_WFIN
: ParserBase.WT_STREAM_WOFIN,
headerVints: [this.streamid],
payload
})
finsend ||= !!cur?.fin
if (payload) {
this.flowController.addBytesSent(payload?.byteLength)
this.sessionFlowController.addBytesSent(payload?.byteLength)
}
}
if (outgoChunkSend) {
this.jsobj.onStreamWrite({
success: true
})
}
}
if (finsend) {
this.capsuleParser.removeStream(this.streamid)
return
}
if (
!(
!this.capsuleParser.blocked ||
this.final ||
!this.capsuleParser.shouldYieldStream(this.streamid)
) &&
this.outgochunks.length > 0
) {
this.capsuleParser.scheduleDrainWriteStream(this.streamid)
}
if (this.final && this.outgochunks.length === 0 && !this.finalmessagesend) {
processnextTick(() => {
this.jsobj.onStreamNetworkFinish({
nettask: 'streamFinal'
})
})
this.finalmessagesend = true
}
}
streamFinal() {
this.final = true
this.outgochunks.push({ fin: true })
this.capsuleParser.scheduleDrainWriteStream(this.streamid)
this.capsuleParser.scheduleDrainWrites()
}
/**
*
* @param {{sendOrder: number, sendGroupId: bigint}} args
*/
updateSendOrderAndGroup({ sendOrder, sendGroupId }) {
this.capsuleParser.streamUpdateSendOrderAndGroup(this.streamid, {
sendOrder,
sendGroupId
})
}
/**
* @param {bigint} windowOffset
*/
sendWindowUpdate(windowOffset) {
log('sendwindow offset stream:', windowOffset)
this.capsuleParser.writeCapsule({
type: ParserBase.WT_MAX_STREAM_DATA,
headerVints: [this.streamid, windowOffset],
payload: undefined
})
}
/**
* @param {bigint} windowOffset
*/
sendBlocked(windowOffset) {
this.capsuleParser.writeCapsule({
type: ParserBase.WT_STREAM_DATA_BLOCKED,
headerVints: [this.streamid, windowOffset],
payload: undefined
})
}
/**
* @param {bigint} pos
*/
reportBlocked(pos) {
log('Stream id: ', this.streamid, ' was blocked at:', pos)
}
connected() {
return this.jsobj.parentobj.state === 'connected'
}
/**
* @param {{ code: number, reason: string }} arg
*/
closeConnection({ code, reason }) {
if (this.jsobj.parentobj?.objint)
// @ts-ignore
this.jsobj.parentobj.objint.closeConnection({ code, reason })
}
smoothedRtt() {
if (this.jsobj.parentobj?.objint)
// @ts-ignore
return this.jsobj.parentobj.objint.smoothedRtt()
}
}