@fails-components/webtransport 1.1.4 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/lib/dom.d.ts +27 -10
  2. package/dist/lib/dom.d.ts.map +1 -1
  3. package/dist/lib/http2/browser/browser.d.ts +5 -2
  4. package/dist/lib/http2/browser/browser.d.ts.map +1 -1
  5. package/dist/lib/http2/browser/browserparser.d.ts.map +1 -1
  6. package/dist/lib/http2/node/capsuleparser.d.ts.map +1 -1
  7. package/dist/lib/http2/node/client.d.ts +3 -2
  8. package/dist/lib/http2/node/client.d.ts.map +1 -1
  9. package/dist/lib/http2/node/server.d.ts +10 -5
  10. package/dist/lib/http2/node/server.d.ts.map +1 -1
  11. package/dist/lib/http2/node/websocketparser.d.ts.map +1 -1
  12. package/dist/lib/http2/parserbase.d.ts +38 -1
  13. package/dist/lib/http2/parserbase.d.ts.map +1 -1
  14. package/dist/lib/http2/priorityscheduler.d.ts +167 -0
  15. package/dist/lib/http2/priorityscheduler.d.ts.map +1 -0
  16. package/dist/lib/http2/session.d.ts +25 -16
  17. package/dist/lib/http2/session.d.ts.map +1 -1
  18. package/dist/lib/http2/stream.d.ts +10 -0
  19. package/dist/lib/http2/stream.d.ts.map +1 -1
  20. package/dist/lib/http2/websocketcommon.d.ts.map +1 -1
  21. package/dist/lib/server.d.ts.map +1 -1
  22. package/dist/lib/session.d.ts +32 -17
  23. package/dist/lib/session.d.ts.map +1 -1
  24. package/dist/lib/stream.d.ts +13 -0
  25. package/dist/lib/stream.d.ts.map +1 -1
  26. package/dist/lib/types.d.ts +18 -15
  27. package/dist/lib/types.d.ts.map +1 -1
  28. package/dist/lib/webtransport.browser.d.ts +3 -2
  29. package/dist/lib/webtransport.browser.d.ts.map +1 -1
  30. package/dist/lib/webtransportbase.d.ts +1 -0
  31. package/dist/lib/webtransportbase.d.ts.map +1 -1
  32. package/eslint.config.js +20 -22
  33. package/lib/dom.ts +31 -13
  34. package/lib/http2/browser/browser.js +28 -13
  35. package/lib/http2/browser/browserparser.js +11 -2
  36. package/lib/http2/node/capsuleparser.js +14 -5
  37. package/lib/http2/node/client.js +38 -27
  38. package/lib/http2/node/server.js +56 -19
  39. package/lib/http2/node/websocketparser.js +10 -3
  40. package/lib/http2/parserbase.js +84 -7
  41. package/lib/http2/priorityscheduler.js +654 -0
  42. package/lib/http2/session.js +83 -23
  43. package/lib/http2/stream.js +60 -10
  44. package/lib/http2/websocketcommon.js +1 -1
  45. package/lib/server.js +0 -1
  46. package/lib/session.js +133 -64
  47. package/lib/stream.js +50 -8
  48. package/lib/types.ts +15 -8
  49. package/lib/webtransport.browser.js +84 -18
  50. package/lib/webtransportbase.js +7 -0
  51. package/old_test/testsuite.js +6 -2
  52. package/package.json +6 -6
  53. package/readme.md +3 -1
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @typedef {import('http2').Http2Stream} Http2Stream
3
3
  * @typedef {import('../dom.js').WebTransportSendGroup} WebTransportSendGroup
4
+ * @typedef {import('../dom.js').WebTransportSendStreamOptions} WebTransportSendStreamOptions
4
5
  */
5
6
  import { ParserBase } from './parserbase.js'
6
7
  import { FlowController } from './flowcontroller.js'
@@ -72,15 +73,25 @@ export class Http2WebTransportSession {
72
73
  maxAllowedIncomingStreams: initialBidirectionalReceiveStreams,
73
74
  maxAllowedOutgoingStreams: initialBidirectionalSendStreams
74
75
  })
75
- this.orderUniStreams = 0
76
- this.orderBiStreams = 0
76
+ /** @type {Array<Uint8Array>} */
77
+ this.datagramsWaiting_ = []
78
+ /** @type {Array<{sendOrder: bigint, sendGroupId: bigint}>} */
79
+ this.orderUniStreams = []
80
+ /** @type {Array<{sendOrder: bigint, sendGroupId: bigint}>} */
81
+ this.orderBiStreams = []
77
82
  if (stream) {
78
83
  if (isclient) {
79
84
  stream.on('response', (headers) => {
80
85
  processnextTick(() => {
81
86
  if (headers[':status'] === 200) {
87
+ const beReady = {}
88
+ if (stream && headers['wt-protocol']) {
89
+ // http/2 case
90
+ // @ts-ignore
91
+ beReady.protocol = headers['wt-protocol']
92
+ }
82
93
  // on ready
83
- this.jsobj.onReady({})
94
+ this.jsobj.onReady(beReady)
84
95
  } else {
85
96
  this.jsobj.onClose({
86
97
  errorcode: headers[':status'],
@@ -98,51 +109,87 @@ export class Http2WebTransportSession {
98
109
  }
99
110
 
100
111
  sendInitialParameters() {
101
- this.flowController.sendWindowUpdate()
102
- this.streamIdMngrBi.sendMaxStreamsFrameInitial()
103
- this.streamIdMngrUni.sendMaxStreamsFrameInitial()
112
+ let skip = false //skips the initial parameters at environment with settings
113
+ if (process) {
114
+ if (process.version) {
115
+ const majorVersion = parseInt(
116
+ process.version.split('.')[0].substring(1)
117
+ )
118
+ if (majorVersion >= 20) skip = true
119
+ }
120
+ }
121
+ if (!skip || this.capsParser.initialParametersMandatory()) {
122
+ this.flowController.sendWindowUpdate()
123
+ this.streamIdMngrBi.sendMaxStreamsFrameInitial()
124
+ this.streamIdMngrUni.sendMaxStreamsFrameInitial()
125
+ }
126
+ }
127
+
128
+ drainWrites() {
129
+ while (!this.capsParser.blocked && this.datagramsWaiting_.length > 0) {
130
+ const outChunk = this.datagramsWaiting_.shift()
131
+ this.capsParser.writeCapsule({
132
+ type: ParserBase.DATAGRAM,
133
+ headerVints: [],
134
+ payload: outChunk
135
+ })
136
+ }
137
+ if (this.datagramsWaiting_.length > 0) {
138
+ this.capsParser.scheduleDrainWrites()
139
+ }
104
140
  }
105
141
 
106
142
  /**
107
143
  * @param {Uint8Array} chunk
144
+ * @return {{ code: "success" | "blocked" | "internalError" | "tooBig", message?: string | undefined; }}
108
145
  */
109
146
  writeDatagram(chunk) {
147
+ if (chunk.byteLength > this.getMaxDatagramSize()) return { code: 'tooBig' }
148
+ if (this.capsParser.blocked) {
149
+ this.datagramsWaiting_.push(chunk)
150
+ this.capsParser.scheduleDrainWrites()
151
+ return { code: 'blocked' }
152
+ }
110
153
  this.capsParser.writeCapsule({
111
154
  type: ParserBase.DATAGRAM,
112
155
  headerVints: [],
113
156
  payload: chunk
114
157
  })
115
- processnextTick(() => {
116
- this.jsobj.onDatagramSend({})
117
- })
158
+ return { code: 'success' }
118
159
  }
119
160
 
120
161
  trySendingUnidirectionalStreams() {
121
162
  while (
122
- this.orderUniStreams > 0 &&
163
+ this.orderUniStreams.length > 0 &&
123
164
  this.streamIdMngrUni.canOpenNextOutgoingStream()
124
165
  ) {
125
166
  const streamid = this.streamIdMngrUni.getNextOutgoingStreamId()
167
+ const priority = this.orderUniStreams.pop()
126
168
  this.capsParser.writeCapsule({
127
169
  type: ParserBase.WT_STREAM_WOFIN,
128
170
  headerVints: [streamid],
129
171
  payload: undefined
130
172
  })
131
- this.capsParser.newStream(streamid)
132
- this.orderUniStreams--
173
+ this.capsParser.newStream(
174
+ streamid,
175
+ priority || { sendGroupId: 0n, sendOrder: 0n }
176
+ )
133
177
  }
134
178
  }
135
179
 
136
180
  /**
137
- * @param {{sendGroup: WebTransportSendGroup|null, sendOrder: number, waitUntilAvailable: boolean}} opts
181
+ * @param {WebTransportSendStreamOptions} opts
138
182
  */
139
- // eslint-disable-next-line no-unused-vars
140
183
  orderUnidiStream({ sendGroup, sendOrder, waitUntilAvailable }) {
141
184
  const canopen = this.streamIdMngrUni.canOpenNextOutgoingStream()
142
185
  const maxset = this.streamIdMngrUni.isMaxStreamSet() // we block if the maxsetting did not arrive
143
186
 
144
187
  if (canopen || waitUntilAvailable || !maxset) {
145
- this.orderUniStreams++
188
+ this.orderUniStreams.push({
189
+ // @ts-ignore
190
+ sendGroupId: sendGroup?._sendGroupId || 0n,
191
+ sendOrder: sendOrder ?? 0n
192
+ })
146
193
  this.trySendingUnidirectionalStreams()
147
194
  return true
148
195
  }
@@ -151,30 +198,36 @@ export class Http2WebTransportSession {
151
198
 
152
199
  trySendingBidirectionalStreams() {
153
200
  while (
154
- this.orderBiStreams > 0 &&
201
+ this.orderBiStreams.length > 0 &&
155
202
  this.streamIdMngrBi.canOpenNextOutgoingStream()
156
203
  ) {
157
204
  const streamid = this.streamIdMngrBi.getNextOutgoingStreamId()
205
+ const priority = this.orderBiStreams.pop()
158
206
  this.capsParser.writeCapsule({
159
207
  type: ParserBase.WT_STREAM_WOFIN,
160
208
  headerVints: [streamid],
161
209
  payload: undefined
162
210
  })
163
- this.capsParser.newStream(streamid)
164
- this.orderBiStreams--
211
+ this.capsParser.newStream(
212
+ streamid,
213
+ priority || { sendGroupId: 0n, sendOrder: 0n }
214
+ )
165
215
  }
166
216
  }
167
217
 
168
218
  /**
169
- * @param {{sendGroup: WebTransportSendGroup|null, sendOrder: number, waitUntilAvailable: boolean}} opts
219
+ * @param {WebTransportSendStreamOptions} opts
170
220
  */
171
- // eslint-disable-next-line no-unused-vars
172
221
  orderBidiStream({ sendGroup, sendOrder, waitUntilAvailable }) {
173
222
  const canopen = this.streamIdMngrBi.canOpenNextOutgoingStream()
174
223
  const maxset = this.streamIdMngrBi.isMaxStreamSet() // we block if the maxsetting did not arrive
175
224
 
176
225
  if (canopen || waitUntilAvailable || !maxset) {
177
- this.orderBiStreams++
226
+ this.orderBiStreams.push({
227
+ // @ts-ignore
228
+ sendGroupId: sendGroup?._sendGroupId || 0n,
229
+ sendOrder: sendOrder ?? 0n
230
+ })
178
231
  this.trySendingBidirectionalStreams()
179
232
  return true
180
233
  }
@@ -202,6 +255,10 @@ export class Http2WebTransportSession {
202
255
  })
203
256
  }
204
257
 
258
+ getMaxDatagramSize() {
259
+ return 16384 // this completly arbitry, we do not have a real restriction, but we choose more than quiche, to make things interesting
260
+ }
261
+
205
262
  /*
206
263
  * @returns {void}
207
264
  */
@@ -260,16 +317,19 @@ export class Http2WebTransportSession {
260
317
  }
261
318
 
262
319
  smoothedRtt() {
320
+ let toret
263
321
  if (this.stream) {
264
322
  // we are on node
265
323
  // @ts-ignore
266
- return this.stream.session?.WTrtt || 26
324
+ toret = this.stream.session?.WTrtt || 25
267
325
  } else if (this.ws) {
268
326
  // we are at the Browser, so we use the connection rtt?
269
327
  // @ts-ignore
270
328
  // eslint-disable-next-line no-undef
271
- return navigator?.connection?.rtt || 26
329
+ toret = navigator?.connection?.rtt || 25
272
330
  }
331
+ toret = Math.ceil(toret / 25) * 25 // to do be to accurate!
332
+ return toret
273
333
  }
274
334
 
275
335
  /**
@@ -1,6 +1,7 @@
1
1
  import { FlowController } from './flowcontroller.js'
2
2
  import { ParserBase } from './parserbase.js'
3
3
  import { logger } from '../utils.js'
4
+ import { WebTransportError } from '../error.js'
4
5
 
5
6
  const pid = typeof process !== 'undefined' ? process.pid : 0
6
7
  const log = logger(`webtransport:http2webtransportstream(${pid})`)
@@ -69,6 +70,7 @@ export class Http2WebTransportStream {
69
70
  this.streamIdManager = streamIdManager
70
71
 
71
72
  this.final = false
73
+ this.finalmessagesend = false
72
74
  this.stopReading_ = true
73
75
  this.drainReads_ = true
74
76
  this.recvBytes = 0
@@ -285,20 +287,34 @@ export class Http2WebTransportStream {
285
287
  */
286
288
  writeChunk(buf) {
287
289
  this.outgochunks.push({ buf, fin: false })
288
- this.drainWrites()
290
+ this.capsuleParser.scheduler.Schedule(this.streamid)
291
+ this.capsuleParser.scheduleDrainWrites()
292
+ }
293
+
294
+ hasPendingData() {
295
+ return this.outgochunks.length > 0
289
296
  }
290
297
 
291
298
  drainWrites() {
299
+ let finsend = false
292
300
  while (
293
301
  this.outgochunks.length > 0 &&
294
- (!this.capsuleParser.blocked || this.final) &&
302
+ (!this.capsuleParser.blocked ||
303
+ this.final ||
304
+ !this.capsuleParser.shouldYieldStream(this.streamid)) &&
295
305
  this.flowController.sendWindowSize() > 0n &&
296
306
  this.sessionFlowController.sendWindowSize() > 0n
297
307
  ) {
298
308
  const cur = this.outgochunks.shift()
309
+ let outgoChunkSend = true
299
310
  if (cur) {
300
311
  let payload = cur.buf
301
312
  if (payload) {
313
+ if (payload.byteLength === 0 && !cur.fin) {
314
+ throw new WebTransportError(
315
+ 'Trying to send zero length capsule without a fin'
316
+ )
317
+ }
302
318
  const sessWindow = this.sessionFlowController.sendWindowSize()
303
319
  const streamWindow = this.flowController.sendWindowSize()
304
320
  if (
@@ -319,6 +335,7 @@ export class Http2WebTransportStream {
319
335
  const dest = new Uint8Array(payload.byteLength - len)
320
336
  dest.set(src)
321
337
  this.outgochunks.unshift({ fin: cur.fin, buf: dest })
338
+ outgoChunkSend = false // remaing part is send later
322
339
  }
323
340
  cur.fin = false
324
341
  payload = cur.buf = new Uint8Array(
@@ -335,27 +352,60 @@ export class Http2WebTransportStream {
335
352
  headerVints: [this.streamid],
336
353
  payload
337
354
  })
355
+ finsend ||= !!cur?.fin
338
356
 
339
357
  if (payload) {
340
358
  this.flowController.addBytesSent(payload?.byteLength)
341
359
  this.sessionFlowController.addBytesSent(payload?.byteLength)
342
360
  }
343
361
  }
344
- this.jsobj.onStreamWrite({
345
- success: true
362
+ if (outgoChunkSend) {
363
+ this.jsobj.onStreamWrite({
364
+ success: true
365
+ })
366
+ }
367
+ }
368
+ if (finsend) {
369
+ this.capsuleParser.removeStream(this.streamid)
370
+ return
371
+ }
372
+
373
+ if (
374
+ !(
375
+ !this.capsuleParser.blocked ||
376
+ this.final ||
377
+ !this.capsuleParser.shouldYieldStream(this.streamid)
378
+ ) &&
379
+ this.outgochunks.length > 0
380
+ ) {
381
+ this.capsuleParser.scheduleDrainWriteStream(this.streamid)
382
+ }
383
+ if (this.final && this.outgochunks.length === 0 && !this.finalmessagesend) {
384
+ processnextTick(() => {
385
+ this.jsobj.onStreamNetworkFinish({
386
+ nettask: 'streamFinal'
387
+ })
346
388
  })
389
+ this.finalmessagesend = true
347
390
  }
348
391
  }
349
392
 
350
393
  streamFinal() {
351
394
  this.final = true
352
395
  this.outgochunks.push({ fin: true })
353
- this.drainWrites()
354
- processnextTick(() =>
355
- this.jsobj.onStreamNetworkFinish({
356
- nettask: 'streamFinal'
357
- })
358
- )
396
+ this.capsuleParser.scheduleDrainWriteStream(this.streamid)
397
+ this.capsuleParser.scheduleDrainWrites()
398
+ }
399
+
400
+ /**
401
+ *
402
+ * @param {{sendOrder: bigint, sendGroupId: bigint}} args
403
+ */
404
+ updateSendOrderAndGroup({ sendOrder, sendGroupId }) {
405
+ this.capsuleParser.streamUpdateSendOrderAndGroup(this.streamid, {
406
+ sendOrder,
407
+ sendGroupId
408
+ })
359
409
  }
360
410
 
361
411
  /**
@@ -1 +1 @@
1
- export const supportedVersions = ['kDraft1']
1
+ export const supportedVersions = ['kDraft1', 'kDraft2']
package/lib/server.js CHANGED
@@ -214,7 +214,6 @@ export class HttpServer {
214
214
  setRequestCallback(callback) {
215
215
  this.requestHandler = callback
216
216
 
217
- this.transportInt.setJSRequestHandler(!!callback)
218
217
  if (this.transportInt) this.transportInt.setJSRequestHandler(!!callback)
219
218
  else this._pendingRequestCallback = !!callback
220
219
  }
package/lib/session.js CHANGED
@@ -13,7 +13,6 @@ const log = logger(`webtransport:httpwtsession(${pid})`)
13
13
  * @typedef {import('./types').SessionReadyEvent} SessionReadyEvent
14
14
  * @typedef {import('./types').SessionCloseEvent} SessionCloseEvent
15
15
  * @typedef {import('./types').DatagramReceivedEvent} DatagramReceivedEvent
16
- * @typedef {import('./types').DatagramSendEvent} DatagramSendEvent
17
16
  * @typedef {import('./types').GoawayReceivedEvent} GoawayReceivedEvent
18
17
  * @typedef {import('./types').DatagramStatsEvent} DatagramStatsEvent
19
18
  * @typedef {import('./types').SessionStatsEvent} SessionStatsEvent
@@ -27,8 +26,11 @@ const log = logger(`webtransport:httpwtsession(${pid})`)
27
26
  * @typedef {import('./dom').WebTransportDatagramDuplexStream} WebTransportDatagramDuplexStream
28
27
  * @typedef {import('./dom').WebTransportReliabilityMode} WebTransportReliabilityMode
29
28
  * @typedef {import('./dom').WebTransportCongestionControl} WebTransportCongestionControl
29
+ * @typedef {import('./dom').WebTransportSendGroup} WebTransportSendGroup
30
30
  * @typedef {import('./dom').WebTransportStats} WebTransportStats
31
31
  * @typedef {import('./dom').WebTransportDatagramStats} WebTransportDatagramStats
32
+ * @typedef {import('./dom').WebTransportSendOptions} WebTransportSendOptions
33
+ * @typedef {import('./dom').WebTransportDatagramsWritable} WebTransportDatagramsWritable
32
34
  *
33
35
  * @typedef {import('./types').NativeHttpWTSession} NativeHttpWTSession
34
36
  *
@@ -112,12 +114,6 @@ export class HttpWTSession {
112
114
  }
113
115
  })
114
116
 
115
- /** @type {Array<() => void>} */
116
- this.writeDatagramRes = []
117
- /** @type {Array<(err?: Error) => void>} */
118
- this.writeDatagramRej = []
119
- /** @type {Array<Promise<void>>} */
120
- this.writeDatagramProm = []
121
117
  const readableopts = {
122
118
  start: (
123
119
  /** @type {import("stream/web").ReadableByteStreamController} */ controller
@@ -130,37 +126,95 @@ export class HttpWTSession {
130
126
  // @ts-ignore
131
127
  delete readableopts.type
132
128
  }
129
+ this._lastGetMaxDatagramSize = 0
133
130
  /** @type {WebTransportDatagramDuplexStream} */
134
131
  this.datagrams = {
135
132
  /** @type {ReadableStream<Uint8Array>} */
136
133
  // @ts-ignore
137
134
  readable: new ReadableStream(readableopts),
138
- writable: new WritableStream({
139
- start: (controller) => {
140
- this.outgoDatagramController = controller
141
- },
142
- // eslint-disable-next-line no-unused-vars
143
- write: (chunk, controller) => {
144
- if (this.state === 'closed') throw new Error('Session is closed')
145
- if (chunk instanceof Uint8Array) {
146
- /** @type {Promise<void>} */
147
- const ret = new Promise((resolve, reject) => {
148
- this.writeDatagramRes.push(resolve)
149
- this.writeDatagramRej.push(reject)
150
- })
151
- this.writeDatagramProm.push(ret)
152
- log.trace('b4 datagram write', chunk)
153
- if (this.objint == null) {
154
- throw new Error('this.objint is not set')
135
+ /**
136
+ * @param {WebTransportSendOptions|undefined} options
137
+ * @return {import('./dom').WebTransportDatagramsWritable}
138
+ */
139
+ createWritable: (options) => {
140
+ let sendOrder = options?.sendOrder ?? 0n
141
+ let sendGroup = options?.sendGroup
142
+ /** @type {WebTransportSendStream} */
143
+ // @ts-expect-error some props are initially missing
144
+ const retWritable = new WritableStream({
145
+ start: (controller) => {
146
+ this.outgoDatagramController = controller
147
+ },
148
+ // eslint-disable-next-line no-unused-vars
149
+ write: (chunk, controller) => {
150
+ if (this.state === 'closed') throw new Error('Session is closed')
151
+ if (chunk instanceof Uint8Array) {
152
+ /** @type {Promise<void>} */
153
+ if (this.objint == null) {
154
+ throw new Error('this.objint is not set')
155
+ }
156
+ const { code, message } = this.objint.writeDatagram(chunk)
157
+ if (
158
+ code !== 'success' &&
159
+ code !== 'blocked' &&
160
+ code !== 'tooBig'
161
+ ) {
162
+ throw new WebTransportError(code + ':' + message)
163
+ }
164
+ } else throw new Error('chunk is not of type Uint8Array')
165
+ },
166
+ close: () => {
167
+ // do nothing
168
+ }
169
+ })
170
+ Object.defineProperties(retWritable, {
171
+ sendOrder: {
172
+ get: () => {
173
+ return sendOrder
174
+ },
175
+ /**
176
+ * @param {bigint} value
177
+ */
178
+ set: (value) => {
179
+ sendOrder = value
180
+ }
181
+ },
182
+ sendGroup: {
183
+ get: () => {
184
+ return sendGroup
185
+ },
186
+ /**
187
+ * @param {WebTransportSendGroup} value
188
+ */
189
+ set: (value) => {
190
+ if (value !== sendGroup) {
191
+ sendGroup = value
192
+ }
155
193
  }
156
- this.objint.writeDatagram(chunk)
157
- return ret
158
- } else throw new Error('chunk is not of type Uint8Array')
159
- },
160
- close: () => {
161
- // do nothing
194
+ }
195
+ })
196
+ return retWritable
197
+ },
198
+ // @ts-ignore
199
+ get writable() {
200
+ // @ts-ignore
201
+ if (!this.datagramwritablepolyfilled_) {
202
+ console.warn('datagrams.writable is deprecated')
162
203
  }
163
- })
204
+ // @ts-ignore
205
+ return (this.datagramwritablepolyfilled_ ||= this.createWritable())
206
+ },
207
+ get maxDatagramSize() {
208
+ // @ts-ignore
209
+ return this._getMaxDatagramSize()
210
+ },
211
+ // @ts-ignore
212
+ _getMaxDatagramSize: () => {
213
+ if (this.objint) {
214
+ this._lastGetMaxDatagramSize = this.objint.getMaxDatagramSize()
215
+ }
216
+ return this._lastGetMaxDatagramSize
217
+ }
164
218
  }
165
219
 
166
220
  /** @type {Array<(stream: WebTransportBidirectionalStream) => void>} */
@@ -193,6 +247,13 @@ export class HttpWTSession {
193
247
  this.sendStreamsController = new Set()
194
248
  /** @type {Set<ReadableStreamDefaultController>} */
195
249
  this.receiveStreamsController = new Set()
250
+
251
+ this._sendGroupNum = 1n // 0n is reserved for no sendgroup
252
+ /** @type {Map<bigint,WebTransportSendGroup>} */
253
+ this._sendGroupIndex = new Map()
254
+
255
+ /** @type {undefined|string} */
256
+ this._selectedProtocol = undefined
196
257
  }
197
258
 
198
259
  /**
@@ -210,6 +271,10 @@ export class HttpWTSession {
210
271
  }
211
272
  }
212
273
 
274
+ get protocol() {
275
+ return this._selectedProtocol
276
+ }
277
+
213
278
  getStats() {
214
279
  if (this.objint == null) {
215
280
  throw new Error('this.objint not set')
@@ -279,16 +344,6 @@ export class HttpWTSession {
279
344
  })
280
345
  }
281
346
 
282
- async waitForDatagramsSend() {
283
- while (this.writeDatagramProm.length > 0) {
284
- try {
285
- await Promise.allSettled(this.writeDatagramProm)
286
- } catch (error) {
287
- log.error('datagram promise failed ', error)
288
- }
289
- }
290
- }
291
-
292
347
  notifySessionDraining() {
293
348
  if (this.objint == null) {
294
349
  throw new Error('this.objint not set')
@@ -361,7 +416,7 @@ export class HttpWTSession {
361
416
  })
362
417
  const notblocked = this.objint.orderBidiStream({
363
418
  sendGroup: opts?.sendGroup || null, // maybe replace, when implemented
364
- sendOrder: opts?.sendOrder || 0,
419
+ sendOrder: BigInt(opts?.sendOrder || 0n),
365
420
  waitUntilAvailable: opts?.waitUntilAvailable || false
366
421
  })
367
422
  if (!notblocked) {
@@ -388,7 +443,7 @@ export class HttpWTSession {
388
443
  })
389
444
  const notblocked = this.objint.orderUnidiStream({
390
445
  sendGroup: opts?.sendGroup || null, // maybe replace, when implemented
391
- sendOrder: opts?.sendOrder || 0,
446
+ sendOrder: opts?.sendOrder || 0n,
392
447
  waitUntilAvailable: opts?.waitUntilAvailable || false
393
448
  })
394
449
  if (!notblocked) {
@@ -417,7 +472,33 @@ export class HttpWTSession {
417
472
  }
418
473
  }
419
474
 
420
- onReady(/* error */) {
475
+ /**
476
+ * @returns {WebTransportSendGroup}
477
+ */
478
+ createSendGroup() {
479
+ if (this.state === 'closed' || this.state === 'failed')
480
+ throw new Error('InvalidState')
481
+ const _sendGroupId = this._sendGroupNum++
482
+ const sendGroup = {
483
+ // @ts-ignore
484
+ _sendGroupId,
485
+ getStats: async () => {
486
+ // TODO implement
487
+ return {
488
+ bytesWritten: 0n,
489
+ bytesSent: 0n,
490
+ bytesAcknowledged: 0n
491
+ }
492
+ }
493
+ }
494
+ this._sendGroupIndex.set(_sendGroupId, sendGroup)
495
+ return sendGroup
496
+ }
497
+ /**
498
+ * @param {{protocol?: string}} arg
499
+ **/
500
+ onReady({ protocol }) {
501
+ if (protocol) this._selectedProtocol = protocol
421
502
  this.state = 'connected'
422
503
  if (!this.reliable) this.reliability = 'supports-unreliable'
423
504
  else this.reliability = 'reliable-only'
@@ -458,13 +539,9 @@ export class HttpWTSession {
458
539
 
459
540
  for (const rej of this.rejectBiDi) rej(error)
460
541
  for (const rej of this.rejectUniDi) rej(error)
461
- for (const rej of this.writeDatagramRej) rej(error)
462
542
  for (const rej of this.rejectSessionStats) rej(error)
463
543
  for (const rej of this.rejectDatagramStats) rej(error)
464
544
 
465
- this.writeDatagramRej = []
466
- this.writeDatagramRes = []
467
- this.writeDatagramProm = []
468
545
  this.resolveBiDi = []
469
546
  this.resolveUniDi = []
470
547
  this.rejectBiDi = []
@@ -519,7 +596,9 @@ export class HttpWTSession {
519
596
  parentobj: this,
520
597
  transport: this.parentobj,
521
598
  bidirectional: args.bidirectional,
522
- incoming: args.incoming
599
+ incoming: args.incoming,
600
+ sendGroup: this._sendGroupIndex.get(args.sendGroupId || 0n),
601
+ sendOrder: args.sendOrder
523
602
  })
524
603
  this.addStreamObj(strobj)
525
604
  if (args.incoming) {
@@ -563,6 +642,11 @@ export class HttpWTSession {
563
642
  */
564
643
  onDatagramReceived(args) {
565
644
  log.trace('datagram received', args.datagram)
645
+ // streams spec says zero length chunk on byob stream is illegal
646
+ if (args.datagram.byteLength === 0) {
647
+ log.trace('zerolength datagram dropped')
648
+ return
649
+ }
566
650
  // console.log('datagram received', args.datagram, Date.now())
567
651
  if (this.incomDatagramController.byobRequest) {
568
652
  /** @type {ReadableStreamBYOBRequest} */
@@ -588,21 +672,6 @@ export class HttpWTSession {
588
672
  }
589
673
  }
590
674
 
591
- /**
592
- * @param {DatagramSendEvent} args
593
- */
594
- // eslint-disable-next-line no-unused-vars
595
- onDatagramSend(args) {
596
- if (this.state === 'closed') return
597
- this.writeDatagramRej.shift()
598
- this.writeDatagramProm.shift()
599
- const res = this.writeDatagramRes.shift()
600
-
601
- if (res != null) {
602
- res()
603
- }
604
- }
605
-
606
675
  /**
607
676
  * @param {GoawayReceivedEvent} args
608
677
  */