@fails-components/webtransport 1.0.0-rc.1 → 1.0.4

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.
package/test/testinone.js DELETED
@@ -1,2854 +0,0 @@
1
- const SERVER_URL = 'https://127.0.0.1:8080'
2
- const CERT_HASH =
3
- 'CA:02:33:76:6C:0D:9B:2F:3C:49:82:CF:DE:7F:ED:CE:39:2B:48:9A:FE:67:8D:DD:2B:09:AF:33:E8:99:FE:EB'
4
-
5
- /**
6
- * WebTransport stream events
7
- * @typedef {import('../types').WebTransportStreamEventHandler} WebTransportStreamEventHandler
8
- * @typedef {import('../types').StreamRecvSignalEvent} StreamRecvSignalEvent
9
- * @typedef {import('../types').StreamReadEvent} StreamReadEvent
10
- * @typedef {import('../types').StreamWriteEvent} StreamWriteEvent
11
- * @typedef {import('../types').StreamNetworkFinishEvent} StreamNetworkFinishEvent
12
- *
13
- * @typedef {import('../types').ReadDataInt} ReadDataInt
14
- */
15
-
16
- let processnextTick = (func) => setTimeout(func, 0)
17
- if (typeof process !== 'undefined') processnextTick = process.nextTick
18
-
19
- export class Http2WebTransportStream {
20
- /**
21
- * @param {{streamid: Number, capsuleParser: ParserBase}} args
22
- * */
23
- constructor({ streamid, capsuleParser }) {
24
- /** @type {import('../stream').HttpWTStream} */
25
- // @ts-ignore
26
- this.jsobj = undefined // the creator will set this
27
- this.readbuffer = new ArrayBuffer(64 * 1024)
28
- this.readpos_ = 0
29
- this.writepos_ = 0
30
- this.bufferlen_ = 0
31
- this.readbufsize_ = this.readbuffer.byteLength
32
- this.streamid = streamid
33
- /** @type {Array<ReadDataInt>} */
34
- this.incomdata = []
35
-
36
- this.capsuleParser = capsuleParser
37
- /** @type {Array<Uint8Array>} */
38
- this.outgochunks = []
39
-
40
- this.final = false
41
- }
42
-
43
- /**
44
- * @param {Object} obj
45
- * @param {Uint8Array} obj.data
46
- * @param {Boolean} obj.fin
47
- */
48
- recvData({ data, fin }) {
49
- this.incomdata.push({ data, fin })
50
- this.processRead()
51
- if (this.incomdata.length > 0) {
52
- // TODO tell the peer to stop sending by sending a capsule
53
- // TODO SEND WT_STREAM_DATA_BLOCKED:
54
- }
55
- }
56
-
57
- processRead() {
58
- let bytesRead = 0
59
- let fin = false
60
-
61
- while (this.incomdata.length > 0 && this.bufferlen_ < this.readbufsize_) {
62
- const cur = this.incomdata.shift()
63
- if (cur.data) {
64
- let len
65
-
66
- if (this.writepos_ >= this.readpos_) {
67
- len = Math.min(
68
- this.readbufsize_ - this.writepos_,
69
- cur.data.byteLength
70
- )
71
-
72
- const destview = new Uint8Array(
73
- this.readbuffer,
74
- 0 + this.writepos_,
75
- len
76
- )
77
- const srcview = new Uint8Array(
78
- cur.data.buffer,
79
- cur.data.byteOffset,
80
- len
81
- )
82
- destview.set(srcview)
83
-
84
- this.writepos_ = (this.writepos_ + len) % this.readbufsize_
85
- this.bufferlen_ = this.bufferlen_ + len
86
- bytesRead += len
87
- } else {
88
- // readpos_ > writepos_
89
- len = Math.min(this.readpos_ - this.writepos_, cur.data.byteLength)
90
- const destview = new Uint8Array(
91
- this.readbuffer,
92
- 0 + this.writepos_,
93
- len
94
- )
95
- const srcview = new Uint8Array(
96
- cur.data.buffer,
97
- cur.data.byteOffset,
98
- len
99
- )
100
- destview.set(srcview)
101
-
102
- this.writepos_ = (this.writepos_ + len) % this.readbufsize_
103
- this.bufferlen_ = this.bufferlen_ + len
104
- bytesRead += len
105
- }
106
- if (cur.data.byteLength !== len) {
107
- this.incomdata.unshift({
108
- data: new Uint8Array(
109
- cur.data.buffer,
110
- cur.data.byteOffset + len,
111
- cur.data.byteLength - len
112
- ),
113
- fin: cur.fin
114
- })
115
- fin = false // next round
116
- } else {
117
- fin = fin || cur.fin
118
- }
119
- } else {
120
- fin = fin || cur.fin
121
- }
122
- }
123
-
124
- if (bytesRead > 0 || fin) {
125
- this.jsobj.onStreamRead({
126
- buffergrow: bytesRead,
127
- fin,
128
- success: true
129
- })
130
- }
131
- }
132
-
133
- /**
134
- * @param {Number} bytesread
135
- * @param {Number} pos
136
- */
137
- updateReadPos(bytesread, pos) {
138
- this.readpos_ = pos
139
- this.bufferlen_ -= bytesread
140
- // well a good time to try to read again
141
- this.processRead()
142
- }
143
-
144
- startReading() {}
145
- stopReading() {}
146
- /**
147
- * @param {Number} code
148
- */
149
- stopSending(code) {
150
- this.capsuleParser.writeCapsule({
151
- type: ParserBase.WT_STOP_SENDING,
152
- headerVints: [this.streamid, code],
153
- payload: undefined
154
- })
155
- processnextTick(() =>
156
- this.jsobj.onStreamNetworkFinish({
157
- nettask: 'stopSending'
158
- })
159
- )
160
- }
161
-
162
- /**
163
- * @param {Number} code
164
- */
165
- resetStream(code) {
166
- this.capsuleParser.writeCapsule({
167
- type: ParserBase.WT_RESET_STREAM,
168
- headerVints: [this.streamid, code],
169
- payload: undefined
170
- })
171
- processnextTick(() =>
172
- this.jsobj.onStreamNetworkFinish({
173
- nettask: 'resetStream'
174
- })
175
- )
176
- }
177
-
178
- /**
179
- * @param {Uint8Array} buf
180
- */
181
- writeChunk(buf) {
182
- this.outgochunks.push({ buf, fin: false })
183
- this.drainWrites()
184
- }
185
-
186
- drainWrites() {
187
- while (
188
- this.outgochunks.length > 0 &&
189
- (!this.capsuleParser.blocked || this.final)
190
- ) {
191
- const cur = this.outgochunks.shift()
192
- const payload = cur.buf
193
- this.capsuleParser.writeCapsule({
194
- type: cur?.fin ? ParserBase.WT_STREAM_WFIN : ParserBase.WT_STREAM_WOFIN,
195
- headerVints: [this.streamid],
196
- payload
197
- })
198
- this.jsobj.onStreamWrite({
199
- success: true
200
- })
201
- }
202
- }
203
-
204
- streamFinal() {
205
- this.final = true
206
- this.outgochunks.push({ fin: true })
207
- this.drainWrites()
208
- processnextTick(() =>
209
- this.jsobj.onStreamNetworkFinish({
210
- nettask: 'streamFinal'
211
- })
212
- )
213
- }
214
- }
215
-
216
- /**
217
- * @param{Number} int
218
- * @returns {Number}
219
- */
220
- export function lengthVarInt(int) {
221
- if (int < 64) return 1
222
- if (int < 16384) return 2
223
- if (int < 1073741824) return 4
224
- /* if (int < 4611686018427387904 ) */
225
- return 8
226
- }
227
-
228
- export class ParserBase {
229
- static PADDING = 0x190b4d38
230
- static WT_RESET_STREAM = 0x190b4d39
231
- static WT_STOP_SENDING = 0x190b4d3a
232
- static WT_STREAM_WOFIN = 0x190b4d3b
233
- static WT_STREAM_WFIN = 0x190b4d3c
234
- static WT_MAX_DATA = 0x190b4d3d
235
- static WT_MAX_STREAM_DATA = 0x190b4d3e
236
- static WT_MAX_STREAMS_BIDI = 0x190b4d3f
237
- static WT_MAX_STREAMS_UNIDI = 0x190b4d40
238
- static WT_DATA_BLOCKED = 0x190b4d41
239
- static WT_STREAM_DATA_BLOCKED = 0x190b4d42
240
- static WT_STREAMS_BLOCKED_UNIDI = 0x190b4d43
241
- static WT_STREAMS_BLOCKED_BIDI = 0x190b4d44
242
- static DATAGRAM = 0x00
243
-
244
- /**
245
- * @param {import('../types').ParserInit} arg
246
- */
247
- constructor({ nativesession, isclient }) {
248
- this.session = nativesession
249
- this.isclient = isclient
250
- /** @type {boolean} */
251
- this.blocked = false
252
-
253
- this.wtstreams = new Map()
254
- }
255
-
256
- /**
257
- * @abstract
258
- * @param {Buffer|Uint8Array} data
259
- */
260
- parseData(data) {
261
- throw new Error('Implement parseData in derived Class')
262
- }
263
-
264
- /**
265
- * @abstract
266
- * @param{{type: Number, headerVints: Array<Number>, payload: Uint8Array|undefined}} bs
267
- */
268
- writeCapsule({ type, headerVints, payload }) {
269
- throw new Error('Implement writeCapsule in derived Class')
270
- }
271
-
272
- /**
273
- * @param{{code: Number, reason: string}}arg
274
- */
275
- sendClose({ code, reason }) {}
276
-
277
- /**
278
- * @param {Number} streamid
279
- */
280
- newStream(streamid) {
281
- const stream = new Http2WebTransportStream({
282
- streamid,
283
- capsuleParser: this
284
- })
285
- this.wtstreams.set(streamid, stream)
286
- this.session.jsobj.onStream({
287
- bidirectional: !(streamid & 0x2),
288
- incoming: this.isclient ? !(streamid & 0x1) : !!(streamid & 0x1),
289
- stream
290
- })
291
- return stream
292
- }
293
-
294
- drainWrites() {
295
- for (const stream of this.wtstreams.values()) {
296
- stream.drainWrites()
297
- }
298
- }
299
- }
300
-
301
- /**
302
- * @param{{offset: Number, buffer: Uint8Array, size: Number}} bs
303
- */
304
- function readVarInt(bs) {
305
- let val = bs.buffer[bs.offset]
306
- bs.offset++
307
- const prefix = val >>> 6
308
- const intlength = 1 << prefix
309
-
310
- if (bs.offset + intlength - 1 > bs.size) {
311
- return undefined
312
- }
313
- val = val & 0x3f
314
- for (let i = 0; i < intlength - 1; i++) {
315
- val = (val << 8) | bs.buffer[bs.offset]
316
- bs.offset++
317
- }
318
- return val
319
- }
320
-
321
- /**
322
- * @param{{offset: Number, buffer: Uint8Array, size: Number}} bs
323
- * @param{Number} int
324
- */
325
- export function writeVarInt(bs, int) {
326
- let numbytes = 8
327
- let msb = 0xc0
328
- if (int < 64) {
329
- numbytes = 1
330
- msb = 0x0
331
- } else if (int < 16384) {
332
- numbytes = 2
333
- msb = 0x40
334
- } else if (int < 1073741824) {
335
- numbytes = 4
336
- msb = 0x80
337
- }
338
- bs.buffer[bs.offset] = msb | ((int >>> ((numbytes - 1) * 8)) & 0xff)
339
- bs.offset++
340
-
341
- for (let i = numbytes - 2; i >= 0; i--) {
342
- bs.buffer[bs.offset] = (int >>> (i * 8)) & 0xff
343
- bs.offset++
344
- }
345
- }
346
-
347
- export class BrowserParser extends ParserBase {
348
- static WS_CONTINUE = 0x0
349
- static WS_TEXT = 0x1
350
- static WS_BINARY = 0x2
351
- static WS_CLOSE = 0x8
352
- static WS_PING = 0x9
353
- static WS_PONG = 0xa
354
- /**
355
- * @param {import('../../types.js').ParserWebsocketInit} stream
356
- */
357
- constructor({ ws, nativesession, isclient }) {
358
- super({ nativesession, isclient })
359
- this.ws = ws
360
- /** @type {Buffer|undefined} */
361
- this.saveddata = undefined
362
- /** @type {Number|undefined} */
363
- this.rtype = undefined
364
-
365
- this.closesend = false
366
-
367
- this.ws.addEventListener('message', (event) => {
368
- if (event.data instanceof ArrayBuffer) {
369
- // binary frame
370
- this.parseData(new Uint8Array(event.data, 0, event.data.byteLength))
371
- } else {
372
- // text frame
373
- console.log('Illegal text frame', event.data)
374
- }
375
- })
376
- }
377
-
378
- /**
379
- * @param {Uint8Array} data
380
- */
381
- parseData(data) {
382
- const bufferstate = { offset: 0, size: data.byteLength, buffer: data }
383
-
384
- const offsetend = bufferstate.size
385
-
386
- const type = readVarInt(bufferstate)
387
-
388
- // all safeguards passed now apply the mask
389
-
390
- switch (type) {
391
- case ParserBase.PADDING:
392
- // only padding do nothing
393
- break
394
- case ParserBase.WT_RESET_STREAM:
395
- case ParserBase.WT_STOP_SENDING:
396
- {
397
- const streamid = readVarInt(bufferstate)
398
- const stream = this.wtstreams.get(streamid)
399
- const code = readVarInt(bufferstate)
400
- if (stream && typeof code !== 'undefined')
401
- stream.jsobj.onStreamRecvSignal({
402
- code,
403
- nettask:
404
- type === ParserBase.WT_RESET_STREAM
405
- ? 'resetStream'
406
- : 'stopSending'
407
- })
408
- }
409
- break
410
- case ParserBase.WT_STREAM_WOFIN:
411
- case ParserBase.WT_STREAM_WFIN:
412
- {
413
- const streamid = readVarInt(bufferstate)
414
-
415
- if (typeof streamid !== 'undefined') {
416
- let object = this.wtstreams.get(streamid)
417
- if (!object) {
418
- object = this.newStream(streamid)
419
- }
420
- // TODO submit data
421
- if (offsetend - bufferstate.offset >= 0) {
422
- object.recvData({
423
- data: new Uint8Array(
424
- bufferstate.buffer.buffer,
425
- bufferstate.buffer.byteOffset + bufferstate.offset,
426
- offsetend - bufferstate.offset
427
- ),
428
- fin: type === ParserBase.WT_STREAM_WFIN
429
- })
430
- }
431
- }
432
- }
433
- break
434
- case ParserBase.WT_MAX_DATA:
435
- // this.recvSession({ maxdata: readVarInt(bufferstate), type })
436
- break
437
- case ParserBase.WT_MAX_STREAM_DATA:
438
- /* {
439
- const streamid = readVarInt(bufferstate)
440
- const object = this.wtstreams.get(streamid)
441
- if (object)
442
- this.recvStream({
443
- maxstreamdata: readVarInt(bufferstate),
444
- type,
445
- object
446
- })
447
- } */
448
- break
449
- case ParserBase.WT_MAX_STREAMS_BIDI:
450
- // this.recvSession({ maxstreams: readVarInt(bufferstate), type })
451
- break
452
- case ParserBase.WT_MAX_STREAMS_UNIDI:
453
- // this.recvSession({ maxstreams: readVarInt(bufferstate), type })
454
- break
455
- case ParserBase.WT_DATA_BLOCKED: // TODO
456
- // this.recvSession({ maxdata: readVarInt(bufferstate), type })
457
- break
458
- case ParserBase.WT_STREAM_DATA_BLOCKED: // TODO
459
- /* {
460
- const streamid = readVarInt(bufferstate)
461
- const object = this.wtstreams.get(streamid)
462
- if (object)
463
- this.recvStream({
464
- maxstreamdata: readVarInt(bufferstate),
465
- type,
466
- object
467
- })
468
- } */
469
- break
470
- case ParserBase.WT_STREAMS_BLOCKED_UNIDI:
471
- /* {
472
- const streamid = readVarInt(bufferstate)
473
- const object = this.wtstreams.get(streamid)
474
- if (object)
475
- this.recvStream({
476
- maxstreams: readVarInt(bufferstate),
477
- type,
478
- object
479
- })
480
- } */
481
- break
482
- case ParserBase.WT_STREAMS_BLOCKED_BIDI:
483
- /* {
484
- const streamid = readVarInt(bufferstate)
485
- const object = this.wtstreams.get(streamid)
486
- if (object)
487
- this.recvStream({
488
- maxstreams: readVarInt(bufferstate),
489
- type,
490
- streamid
491
- })
492
- } */
493
- break
494
- case ParserBase.DATAGRAM:
495
- this.session.jsobj.onDatagramReceived({
496
- datagram: new Uint8Array(
497
- bufferstate.buffer.buffer,
498
- bufferstate.buffer.byteOffset + bufferstate.offset,
499
- offsetend - bufferstate.offset
500
- )
501
- })
502
-
503
- break
504
- default:
505
- // do nothing
506
- }
507
-
508
- bufferstate.offset = offsetend
509
- }
510
-
511
- /**
512
- * @param{{type: Number, headerVints: Array<Number>, payload: Uint8Array|undefined}} bs
513
- */
514
- writeCapsule({ type, headerVints, payload }) {
515
- let plength = 0
516
- for (const ind in headerVints) plength += lengthVarInt(headerVints[ind])
517
- plength += lengthVarInt(type)
518
- const hlength = plength
519
- if (payload) plength += payload.byteLength
520
-
521
- const cdata = new Uint8Array(plength)
522
- const bufferstate = { offset: 0, size: cdata.length, buffer: cdata }
523
- writeVarInt(bufferstate, type)
524
- for (const ind in headerVints) writeVarInt(bufferstate, headerVints[ind])
525
- const dest = new Uint8Array(cdata.buffer, cdata.byteOffset + hlength)
526
- if (payload) dest.set(payload)
527
- this.ws.send(cdata)
528
-
529
- /* const blocked = this.ws.bufferedAmount > 1024 * 256
530
- // do something if blocked
531
- if (blocked) this.blocked = true
532
- return blocked */
533
- return false
534
- }
535
-
536
- /**
537
- * @param{{code: Number, reason: string}}arg
538
- */
539
- sendClose({ code, reason }) {
540
- this.ws.close(1000, code.toString() + ':' + reason)
541
- }
542
- }
543
-
544
- /**
545
- * @typedef {import('http2').Http2Stream} Http2Stream
546
- */
547
-
548
- export class Http2WebTransportSession {
549
- /**
550
- * @param {Object} obj
551
- * @param {Http2Stream} [obj.stream]
552
- * @param {WebSocket} [obj.ws]
553
- * @param {boolean} obj.isclient
554
- * @param {import('../types.js').CreateParserFunction} obj.createParser
555
- */
556
- constructor({ stream, ws, isclient, createParser }) {
557
- // @ts-ignore
558
- this.jsobj = undefined // the creator will set this
559
- if (stream) {
560
- this.stream = stream
561
- } else if (ws) {
562
- this.ws = ws
563
- } else throw new Error('Neither stream or websocket supplied')
564
- this.capsParser = createParser(this)
565
- this.unidiId = 0
566
- this.bidiId = 0
567
- this.isclient = isclient
568
- if (stream) {
569
- if (isclient) {
570
- stream.on('response', (headers) => {
571
- processnextTick(() => {
572
- if (headers[':status'] === 200) {
573
- // on ready
574
- this.jsobj.onReady({})
575
- } else {
576
- this.jsobj.onClose({
577
- errorcode: headers[':status'],
578
- error: 'Session stream errored'
579
- })
580
- }
581
- })
582
- })
583
- } else {
584
- processnextTick(() => {
585
- this.jsobj.onReady({})
586
- })
587
- }
588
- stream.on('close', () => {
589
- if (!(this.jsobj.state === 'failed' || this.jsobj.state === 'closed')) {
590
- this.jsobj.onClose({
591
- errorcode: 0,
592
- error: 'Session http/2 stream closed'
593
- })
594
- }
595
- })
596
- }
597
- }
598
-
599
- /**
600
- * @param {Uint8Array} chunk
601
- */
602
- writeDatagram(chunk) {
603
- this.capsParser.writeCapsule({
604
- type: ParserBase.DATAGRAM,
605
- headerVints: [],
606
- payload: chunk
607
- })
608
- processnextTick(() => {
609
- this.jsobj.onDatagramSend({})
610
- })
611
- }
612
-
613
- orderUnidiStream() {
614
- let streamid = 0x2 | (this.unidiId << 2)
615
- if (this.isclient) streamid = streamid | 0x1
616
- this.capsParser.writeCapsule({
617
- type: ParserBase.WT_STREAM_WOFIN,
618
- headerVints: [streamid],
619
- payload: undefined
620
- })
621
- this.capsParser.newStream(streamid)
622
- this.unidiId++
623
- }
624
-
625
- orderBidiStream() {
626
- let streamid = 0x0 | (this.bidiId << 2)
627
- if (this.isclient) streamid = streamid | 0x1
628
- this.capsParser.writeCapsule({
629
- type: ParserBase.WT_STREAM_WOFIN,
630
- headerVints: [streamid],
631
- payload: undefined
632
- })
633
- this.capsParser.newStream(streamid)
634
- this.bidiId++
635
- }
636
-
637
- orderSessionStats() {
638
- this.jsobj.onSessionStats({
639
- timestamp: 0,
640
- expiredOutgoing: 0n,
641
- lostOutgoing: 0n,
642
- // non Datagram
643
- minRtt: 0,
644
- smoothedRtt: 0,
645
- rttVariation: 0,
646
- estimatedSendRateBps: 0n
647
- })
648
- }
649
-
650
- orderDatagramStats() {
651
- this.jsobj.onDatagramStats({
652
- timestamp: 0,
653
- expiredOutgoing: 0n,
654
- lostOutgoing: 0n
655
- })
656
- }
657
-
658
- /*
659
- * @returns {void}
660
- */
661
- notifySessionDraining() {}
662
- /**
663
- * @param {{ code: number, reason: string }} arg
664
- */
665
- close({ code, reason }) {
666
- this.capsParser.sendClose({ code, reason }) // thid includes for ws closing the session!
667
- // what to do with the reason
668
- if (this.stream) {
669
- if (this.stream.close) this.stream.close(code)
670
- else if (this.stream.end) this.stream.end()
671
- else throw new Error('http2:session not close method')
672
- }
673
- }
674
- }
675
-
676
- export class Http2WebTransportBrowser {
677
- /**
678
- * @param {import('../../types.js').NativeClientOptions} args
679
- */
680
- constructor(args) {
681
- this.port = args?.port || 443
682
- this.hostname = args?.host || 'localhost'
683
- /** @type {import('../../session.js').HttpClient} */
684
- // @ts-ignore
685
- this.jsobj = undefined // the transport will set this
686
- /** @type {WebSocket} */
687
- // @ts-ignore
688
- this.clientInt = undefined
689
- }
690
-
691
- /**
692
- * @param {{path: string}} arg
693
- */
694
- createTransport({ path }) {
695
- try {
696
- let url = 'wss://' + this.hostname + ':' + this.port
697
- if (path) url = url + '/' + path
698
- // eslint-disable-next-line no-undef
699
- this.clientInt = new WebSocket(url, ['webtransport'])
700
- } catch (error) {
701
- this.jsobj.onClientConnected({
702
- success: false
703
- })
704
-
705
- return
706
- }
707
- this.clientInt.binaryType = 'arraybuffer'
708
-
709
- this.clientInt.addEventListener('open', (event) => {
710
- if (this.clientInt?.protocol === 'webtransport') {
711
- this.jsobj.onClientWebTransportSupport({})
712
- this.jsobj.onClientConnected({
713
- success: true
714
- })
715
- } else {
716
- if (this.clientInt) this.clientInt.close()
717
- this.jsobj.onClientConnected({
718
- success: false
719
- })
720
- }
721
- })
722
-
723
- this.clientInt.addEventListener('error', () => {
724
- this.jsobj.onClientConnected({
725
- success: false
726
- })
727
- })
728
- }
729
-
730
- /**
731
- * @param {string} path
732
- */
733
- openWTSession(path) {
734
- if (!this.clientInt) throw new Error('clientInt not present')
735
- let sessobj
736
-
737
- const retObj = {
738
- session: new Http2WebTransportSession({
739
- ws: this.clientInt,
740
- isclient: true,
741
- createParser: (
742
- /** @type {Http2WebTransportSession} */ nativesession
743
- ) => {
744
- sessobj = nativesession
745
- const session = new BrowserParser({
746
- ws: this.clientInt,
747
- nativesession,
748
- isclient: true
749
- })
750
- if (this.clientInt)
751
- this.clientInt.addEventListener('close', (event) => {
752
- let code = event.code
753
- let error = 'Session WebSocket closed'
754
- if (event.reason) {
755
- let tokens = event.reason.split(':')
756
- if (tokens.length > 1) {
757
- code = parseInt(tokens[0])
758
- tokens = tokens.slice(1)
759
- }
760
- error = tokens.join(':')
761
- }
762
- nativesession.jsobj.onClose({
763
- errorcode: code,
764
- error
765
- })
766
- })
767
- return session
768
- }
769
- }),
770
- reliable: true
771
- }
772
- this.jsobj.onHttpWTSessionVisitor(retObj)
773
-
774
- // @ts-ignore
775
- sessobj.jsobj.onReady({})
776
- }
777
-
778
- closeClient() {
779
- if (this.clientInt) this.clientInt.close()
780
- }
781
- }
782
-
783
- /** @type {WeakMap<WebTransportBase, import('./session.js').HttpWTSession>} */
784
-
785
- /**
786
- * @typedef {import('./dom.js').WebTransportCloseInfo} WebTransportCloseInfo
787
- * @typedef {import('./dom.js').WebTransportBidirectionalStream} WebTransportBidirectionalStream
788
- * @typedef {import('./dom.js').WebTransportReceiveStream} WebTransportReceiveStream
789
- * @typedef {import('./session.js').HttpWTSession} HttpWTSession
790
- * @typedef { import('./session.js').HttpClient} HttpClient
791
- */
792
-
793
- /**
794
- * @template T
795
- * @typedef {import('node:stream/web').ReadableStream<T>} ReadableStream<T>
796
- */
797
-
798
- /**
799
- * @typedef {import('./dom.js').WebTransport} WebTransportInterface
800
- *
801
- * @implements {WebTransportInterface}
802
- */
803
- export class WebTransportBase {
804
- /**
805
- * @param {string} url
806
- * @param {import('./dom.js').WebTransportOptions} [args]
807
- */
808
- constructor(url, args) {
809
- if (!url) throw new Error('no URL supplied')
810
-
811
- const ourl = new URL(url)
812
-
813
- if (ourl.protocol !== 'https:') {
814
- throw new Error('URL is not supported for webtransport')
815
- }
816
-
817
- const host = ourl.hostname
818
- let port = ourl.port
819
- if (port === '') port = '443'
820
- const { sessionint, client } = this.createClient({ host, port, ...args })
821
- this.ready = sessionint.ready
822
- this.closed = sessionint.closed
823
- this.draining = sessionint.draining
824
-
825
- this.datagrams = sessionint.datagrams
826
-
827
- this.incomingBidirectionalStreams = sessionint.incomingBidirectionalStreams
828
-
829
- this.incomingUnidirectionalStreams =
830
- sessionint.incomingUnidirectionalStreams
831
-
832
- this.sessionint = sessionint
833
-
834
- this.startUpConnection({ client, sessionint, ourl })
835
- }
836
-
837
- /**
838
- * @param{import('./types.js').HttpWebTransportInit} args
839
- * @return {{sessionint: HttpWTSession, client: HttpClient}}
840
- * @abstract
841
- */
842
- createClient(args) {
843
- throw new Error('Implement createClient')
844
- }
845
-
846
- /**
847
- * @param{{client: HttpClient, sessionint: HttpWTSession, ourl: URL}} args
848
- * @abstract
849
- */
850
- startUpConnection({ client, sessionint, ourl }) {
851
- throw new Error('Implement createClient')
852
- }
853
-
854
- get reliability() {
855
- const session = this.sessionint
856
-
857
- if (!session) {
858
- // should never happen as session is only removed when this instance is garbage collected
859
- throw new Error('Http3WTSession was undefined')
860
- }
861
- return session.reliability
862
- }
863
-
864
- get congestionControl() {
865
- const session = this.sessionint
866
-
867
- if (!session) {
868
- // should never happen as session is only removed when this instance is garbage collected
869
- throw new Error('Http3WTSession was undefined')
870
- }
871
- return session.congestionControl
872
- }
873
-
874
- getStats() {
875
- const session = this.sessionint
876
-
877
- if (!session) {
878
- // should never happen as session is only removed when this instance is garbage collected
879
- throw new Error('Http3WTSession was undefined')
880
- }
881
- return session.getStats()
882
- }
883
-
884
- /**
885
- * @param {WebTransportCloseInfo} [closeinfo]
886
- */
887
- close(closeinfo) {
888
- const session = this.sessionint
889
-
890
- if (!session) {
891
- // should never happen as session is only removed when this instance is garbage collected
892
- throw new Error('Http3WTSession was undefined')
893
- }
894
-
895
- return session.close(closeinfo)
896
- }
897
-
898
- createBidirectionalStream() {
899
- const session = this.sessionint
900
-
901
- if (!session) {
902
- // should never happen as session is only removed when this instance is garbage collected
903
- throw new Error('Http3WTSession was undefined')
904
- }
905
-
906
- return session.createBidirectionalStream()
907
- }
908
-
909
- createUnidirectionalStream() {
910
- const session = this.sessionint
911
-
912
- if (!session) {
913
- // should never happen as session is only removed when this instance is garbage collected
914
- throw new Error('Http3WTSession was undefined')
915
- }
916
-
917
- return session.createUnidirectionalStream()
918
- }
919
- }
920
-
921
- export class WebTransportError extends Error {
922
- /**
923
- * @param {string} message
924
- */
925
- constructor(message) {
926
- super(message)
927
-
928
- this.name = this[Symbol.toStringTag] = 'WebTransportError'
929
- }
930
- }
931
-
932
- /**
933
- * WebTransport stream events
934
- * @typedef {import('./types').WebTransportStreamEventHandler} WebTransportStreamEventHandler
935
- * @typedef {import('./types').StreamRecvSignalEvent} StreamRecvSignalEvent
936
- * @typedef {import('./types').StreamReadEvent} StreamReadEvent
937
- * @typedef {import('./types').StreamWriteEvent} StreamWriteEvent
938
- * @typedef {import('./types').StreamNetworkFinishEvent} StreamNetworkFinishEvent
939
- *
940
- * @typedef {import('./types').NativeHttpWTStream} NativeHttpWTStream
941
- *
942
- * @typedef {import('./dom').WebTransportReceiveStream} WebTransportReceiveStream
943
- * @typedef {import('./dom').WebTransportSendStream} WebTransportSendStream
944
- *
945
- * @typedef {import('./session').HttpWTSession} HttpWTSession
946
- *
947
- * @typedef {import('stream/web').WritableStreamDefaultController} WritableStreamDefaultController
948
- */
949
-
950
- export class HttpWTStream {
951
- /**
952
- * @param {object} args
953
- * @param {NativeHttpWTStream} args.object
954
- * @param {HttpWTSession} args.parentobj
955
- * @param {object} args.transport
956
- * @param {boolean} args.bidirectional
957
- * @param {boolean} args.incoming
958
- */
959
- constructor(args) {
960
- this.objint = args.object
961
- this.objint.jsobj = this
962
- this.parentobj = args.parentobj
963
- this.transport = args.transport
964
- this.bidirectional = args.bidirectional
965
- this.incoming = args.incoming
966
- this.closed = false
967
-
968
- /** @type {Promise<void> | null} */
969
- this.pendingoperation = null
970
- this.pendingres = null
971
-
972
- /** @type {WebTransportReceiveStream} */
973
- this.readable // eslint-disable-line no-unused-expressions
974
- /** @type {WebTransportSendStream} */
975
- this.writable // eslint-disable-line no-unused-expressions
976
-
977
- /** @type {Promise<void> | null} */
978
- this.pendingoperationRead = null
979
- this.pendingresRead = null
980
-
981
- if (this.bidirectional || this.incoming) {
982
- /** @type {Number} */
983
- this.incomingbufferfilled = 0
984
- /** @type {Number} */
985
- this.incomingbufferreadpos = 0
986
- if (!this.objint.readbuffer)
987
- throw new Error('No readbuffer for read stream')
988
- /** @type {WebTransportReceiveStream} */
989
- // @ts-expect-error `getStats` property is missing from ReadableStream
990
- this.readable = new ReadableStream(
991
- {
992
- start: (
993
- /** @type {import("stream/web").ReadableByteStreamController} */ controller
994
- ) => {
995
- this.readableController = controller
996
- this.objint.startReading()
997
- },
998
- pull: async (
999
- /** @type {import("stream/web").ReadableByteStreamController} */ controller
1000
- ) => {
1001
- if (this.readableclosed) {
1002
- return Promise.resolve()
1003
- }
1004
-
1005
- /** @type {Uint8Array} */
1006
- if (this.incomingbufferfilled === 0) {
1007
- this.pendingoperationRead = new Promise((resolve, reject) => {
1008
- this.pendingresRead = resolve
1009
- })
1010
- await this.pendingoperationRead
1011
- }
1012
- if (this.incomingbufferfilled === 0) return Promise.resolve()
1013
-
1014
- this.drainBuffer()
1015
- },
1016
- cancel: (/** @type {{ code: number; }} */ reason) => {
1017
- /** @type {Promise<void>} */
1018
- const promise = new Promise((resolve, reject) => {
1019
- this.cancelres = resolve
1020
- })
1021
- let code = 0
1022
- if (reason && reason.code) {
1023
- if (reason.code < 0) code = 0
1024
- else if (reason.code > 255) code = 255
1025
- else code = reason.code
1026
- }
1027
- this.readableclosed = true
1028
- this.objint.stopSending(code)
1029
- return promise
1030
- },
1031
- type: 'bytes',
1032
- autoAllocateChunkSize: 4096 // lets take this as buffer size
1033
- }
1034
- // TODO fix stretegy
1035
- )
1036
- this.readable.getStats = () => {
1037
- return Promise.resolve({
1038
- timestamp: 0,
1039
- bytesReceived: 0n,
1040
- bytesRead: 0n
1041
- })
1042
- }
1043
- // @ts-ignore
1044
- this.parentobj.addReceiveStream(this.readable, this.readableController)
1045
- }
1046
- if (this.bidirectional || !this.incoming) {
1047
- /** @type {WebTransportSendStream} */
1048
- // @ts-expect-error `getStats` property is missing from WritableStream
1049
- this.writable = new WritableStream(
1050
- {
1051
- start: (controller) => {
1052
- this.writableController = controller
1053
- },
1054
- write: (chunk, controller) => {
1055
- if (this.writableclosed) {
1056
- return Promise.resolve()
1057
- }
1058
- let wchunk = chunk
1059
- if (wchunk instanceof ArrayBuffer) {
1060
- wchunk = new Uint8Array(wchunk)
1061
- }
1062
- if (wchunk instanceof Uint8Array) {
1063
- this.pendingoperation = new Promise((resolve, reject) => {
1064
- this.pendingres = resolve
1065
- })
1066
- this.parentobj
1067
- .waitForDatagramsSend()
1068
- .finally(() => {
1069
- this.objint.writeChunk(wchunk)
1070
- })
1071
- .catch((/** @type {any} */ err) => {
1072
- log.error(err)
1073
- })
1074
- return this.pendingoperation
1075
- } else {
1076
- log.trace('chunk info:', chunk)
1077
- throw new Error(
1078
- 'chunk is not of instanceof Uint8Array or Arraybuffer'
1079
- )
1080
- }
1081
- },
1082
- close: () => {
1083
- if (this.writableclosed) {
1084
- return Promise.resolve()
1085
- }
1086
- this.objint.streamFinal()
1087
- this.pendingoperation = new Promise((resolve, reject) => {
1088
- this.pendingres = resolve
1089
- })
1090
- return this.pendingoperation
1091
- },
1092
- abort: (reason) => {
1093
- if (this.writableclosed) {
1094
- return new Promise((resolve, reject) => {
1095
- resolve()
1096
- })
1097
- }
1098
- let code = 0
1099
- if (reason && reason.code) {
1100
- if (reason.code < 0) code = 0
1101
- else if (reason.code > 255) code = 255
1102
- else code = reason.code
1103
- }
1104
- /** @type {Promise<void>} */
1105
- const promise = new Promise((resolve, reject) => {
1106
- this.abortres = resolve
1107
- })
1108
- this.objint.resetStream(code)
1109
- return promise
1110
- }
1111
- },
1112
- { highWaterMark: 4 }
1113
- )
1114
- this.writable.getStats = () => {
1115
- return Promise.resolve({
1116
- timestamp: 0,
1117
- bytesWritten: 0n,
1118
- bytesSent: 0n,
1119
- bytesAcknowledged: 0n
1120
- })
1121
- }
1122
- // @ts-ignore
1123
- this.parentobj.addSendStream(this.writable, this.writableController)
1124
- }
1125
-
1126
- /** @type {(() => void) | null} */
1127
- this.cancelres = null
1128
- /** @type {(() => void) | null} */
1129
- this.pendingres = null
1130
- /** @type {(() => void) | null} */
1131
- this.abortres = null
1132
- }
1133
-
1134
- drainBuffer() {
1135
- const byob = this.readableController.byobRequest
1136
- if (byob) {
1137
- // @ts-ignore
1138
- const view = byob?.view
1139
- // @ts-ignore
1140
- if (!(view instanceof Uint8Array)) {
1141
- throw new Error('byob view is not a Uint8Array')
1142
- }
1143
- let toread = Math.min(view.byteLength, this.incomingbufferfilled)
1144
- let read = 0
1145
- if (!this.objint.readbuffer)
1146
- throw new Error('No readbuffer in read for read stream')
1147
- if (
1148
- this.incomingbufferreadpos + toread >
1149
- this.objint.readbuffer.byteLength
1150
- ) {
1151
- /** @type {Number} */
1152
- const firstread =
1153
- this.objint.readbuffer.byteLength - this.incomingbufferreadpos
1154
- read += firstread
1155
- toread -= firstread
1156
- const destview = new Uint8Array(
1157
- view.buffer,
1158
- 0 + view.byteOffset,
1159
- firstread
1160
- )
1161
- const srcview = new Uint8Array(
1162
- this.objint.readbuffer,
1163
- this.incomingbufferreadpos,
1164
- firstread
1165
- )
1166
- destview.set(srcview)
1167
- this.incomingbufferreadpos = 0
1168
- }
1169
- {
1170
- const destview = new Uint8Array(
1171
- view.buffer,
1172
- read + view.byteOffset,
1173
- toread
1174
- )
1175
- const srcview = new Uint8Array(
1176
- this.objint.readbuffer,
1177
- this.incomingbufferreadpos,
1178
- toread
1179
- )
1180
- destview.set(srcview)
1181
- read += toread
1182
- this.incomingbufferreadpos =
1183
- (this.incomingbufferreadpos + toread) %
1184
- this.objint.readbuffer.byteLength
1185
- }
1186
- // @ts-ignore
1187
- byob.respond(read)
1188
- this.incomingbufferfilled -= read
1189
- this.objint.updateReadPos(read, this.incomingbufferreadpos)
1190
- } else {
1191
- let toread = this.incomingbufferfilled
1192
- const toqueue = new Uint8Array(toread)
1193
- let read = 0
1194
- if (!this.objint.readbuffer)
1195
- throw new Error('No readbuffer in read for read stream')
1196
- if (
1197
- this.incomingbufferreadpos + toread >
1198
- this.objint.readbuffer.byteLength
1199
- ) {
1200
- /** @type {Number} */
1201
- const firstread =
1202
- this.objint.readbuffer.byteLength - this.incomingbufferreadpos
1203
- read += firstread
1204
- toread -= firstread
1205
- const destview = new Uint8Array(
1206
- toqueue.buffer,
1207
- 0 + toqueue.byteOffset,
1208
- firstread
1209
- )
1210
- const srcview = new Uint8Array(
1211
- this.objint.readbuffer,
1212
- this.incomingbufferreadpos,
1213
- firstread
1214
- )
1215
- destview.set(srcview)
1216
- this.incomingbufferreadpos = 0
1217
- }
1218
- {
1219
- const destview = new Uint8Array(
1220
- toqueue.buffer,
1221
- read + toqueue.byteOffset,
1222
- toread
1223
- )
1224
- const srcview = new Uint8Array(
1225
- this.objint.readbuffer,
1226
- this.incomingbufferreadpos,
1227
- toread
1228
- )
1229
- destview.set(srcview)
1230
- read += toread
1231
- this.incomingbufferreadpos =
1232
- (this.incomingbufferreadpos + toread) %
1233
- this.objint.readbuffer.byteLength
1234
- }
1235
- this.readableController.enqueue(toqueue)
1236
-
1237
- this.incomingbufferfilled -= read
1238
- this.pulledbytes += read
1239
- this.objint.updateReadPos(read, this.incomingbufferreadpos)
1240
- }
1241
- }
1242
-
1243
- /**
1244
- * @param {import('./types').StreamRecvSignalEvent} args
1245
- * @returns {void}
1246
- */
1247
- onStreamRecvSignal(args) {
1248
- log('callback', args?.nettask)
1249
- log.trace('onStreamRecvSignal', args)
1250
- // check if transport is closed
1251
- let parentcleanup = true
1252
- const parentstate = this.parentobj.state
1253
- if (parentstate === 'closed' || parentstate === 'failed') {
1254
- log('no parent cleanup as parent was closed or failed')
1255
- parentcleanup = false
1256
- }
1257
- switch (args.nettask) {
1258
- case 'resetStream':
1259
- if (this.readable) {
1260
- if (parentcleanup)
1261
- this.parentobj.removeReceiveStream(
1262
- this.readable,
1263
- this.readableController
1264
- )
1265
- this.readableclosed = true
1266
- this.readableController.error(
1267
- new WebTransportError('Resetstream with code:' + (args.code || 0))
1268
- )
1269
- } else {
1270
- log.error('resetStream without readable')
1271
- }
1272
- break
1273
-
1274
- case 'stopSending':
1275
- if (this.writable) {
1276
- if (parentcleanup)
1277
- this.parentobj.removeSendStream(
1278
- this.writable,
1279
- this.writableController
1280
- )
1281
-
1282
- this.writableclosed = true
1283
- this.writableController.error(
1284
- new WebTransportError('StopSending with code:' + (args.code || 0))
1285
- )
1286
- } else {
1287
- log.error('stopSending without writable')
1288
- }
1289
- break
1290
- default:
1291
- log.error('unhandled onStreamRecvSignal')
1292
- }
1293
-
1294
- if (this.pendingoperation) {
1295
- const res = this.pendingres
1296
- this.pendingoperation = null
1297
- this.pendingres = null
1298
- if (res != null) {
1299
- res()
1300
- }
1301
- }
1302
- if (this.pendingoperationRead) {
1303
- const res = this.pendingresRead
1304
- this.pendingoperationRead = null
1305
- this.pendingresRead = null
1306
- if (res != null) {
1307
- res()
1308
- }
1309
- }
1310
- }
1311
-
1312
- /**
1313
- * @param {StreamReadEvent} args
1314
- * @returns {void}
1315
- */
1316
- onStreamRead(args) {
1317
- if (args.buffergrow && !this.readableclosed) {
1318
- log.trace('stream read received', args.buffergrow)
1319
- this.incomingbufferfilled += args.buffergrow
1320
- // console.log('stream read received', args.data, Date.now())
1321
- if (this.pendingoperationRead) {
1322
- // this.readableController.enqueue(data)
1323
- const res = this.pendingresRead
1324
- this.pendingoperationRead = null
1325
- this.pendingresRead = null
1326
- if (res) res()
1327
- }
1328
- if (
1329
- this.readableController.desiredSize != null &&
1330
- this.readableController.desiredSize < 0
1331
- )
1332
- this.objint.stopReading()
1333
- }
1334
- if (args.fin) {
1335
- if (this.incomingbufferfilled > 0) {
1336
- log.trace('Warning buffer filled and we got a fin')
1337
- if (this.pendingoperationRead || this.pendingresRead)
1338
- throw new Error('We have pendingoperationRead and a filled buffer?')
1339
-
1340
- this.finalDrain()
1341
- }
1342
- if (this.cancelres) {
1343
- const res = this.cancelres
1344
- this.cancelres = null
1345
- res()
1346
- }
1347
- if (!this.readableclosed) {
1348
- this.readableController.close()
1349
- this.readableclosed = true
1350
- }
1351
- }
1352
- }
1353
-
1354
- finalDrain() {
1355
- while (this.incomingbufferfilled > 0) this.drainBuffer()
1356
- }
1357
-
1358
- /**
1359
- * @param {StreamWriteEvent} args
1360
- */
1361
- onStreamWrite(args) {
1362
- // we ignore success
1363
- if (this.pendingoperation) {
1364
- const res = this.pendingres
1365
- this.pendingoperation = null
1366
- this.pendingres = null
1367
- if (res != null) {
1368
- res()
1369
- }
1370
- }
1371
- }
1372
-
1373
- /**
1374
- * @param {StreamNetworkFinishEvent} args
1375
- */
1376
- onStreamNetworkFinish(args) {
1377
- log('callback', args?.nettask)
1378
- log.trace('networkfinish args', args)
1379
- switch (args.nettask) {
1380
- case 'stopSending':
1381
- if (this.cancelres) {
1382
- const res = this.cancelres
1383
- this.cancelres = null
1384
- res()
1385
- }
1386
- this.stopSendingRecv = true
1387
- break
1388
- case 'resetStream':
1389
- if (this.abortres) {
1390
- const res = this.abortres
1391
- this.abortres = null
1392
- res()
1393
- if (this.readable)
1394
- this.parentobj.removeReceiveStream(
1395
- this.readable,
1396
- this.readableController
1397
- )
1398
- if (this.writable)
1399
- this.parentobj.removeSendStream(
1400
- this.writable,
1401
- this.writableController
1402
- )
1403
- this.readableclosed = true
1404
- this.parentobj.removeStreamObj(this)
1405
- }
1406
-
1407
- break
1408
-
1409
- case 'streamFinal':
1410
- if (this.pendingoperation) {
1411
- const res = this.pendingres
1412
- this.pendingoperation = null
1413
- this.pendingres = null
1414
- if (res != null) {
1415
- res()
1416
- }
1417
- }
1418
- break
1419
- default:
1420
- log.error('onStreamNetworkFinish unknown task', args.nettask)
1421
- }
1422
- // we could differentiate....
1423
- }
1424
- }
1425
-
1426
- /**
1427
- * WebTransport session events
1428
- * @typedef {import('./types').WebTransportSessionEventHandler} WebTransportSessionEventHandler
1429
- * @typedef {import('./types').SessionReadyEvent} SessionReadyEvent
1430
- * @typedef {import('./types').SessionCloseEvent} SessionCloseEvent
1431
- * @typedef {import('./types').DatagramReceivedEvent} DatagramReceivedEvent
1432
- * @typedef {import('./types').DatagramSendEvent} DatagramSendEvent
1433
- * @typedef {import('./types').GoawayReceivedEvent} GoawayReceivedEvent
1434
- * @typedef {import('./types').DatagramStatsEvent} DatagramStatsEvent
1435
- * @typedef {import('./types').SessionStatsEvent} SessionStatsEvent
1436
- * @typedef {import('./types').NewStreamEvent} NewStreamEvent
1437
- *
1438
- * @typedef {import('./dom').WebTransportCloseInfo} WebTransportCloseInfo
1439
- * @typedef {import('./dom').WebTransportBidirectionalStream} WebTransportBidirectionalStream
1440
- * @typedef {import('./dom').WebTransportSendStream} WebTransportSendStream
1441
- * @typedef {import('./dom').WebTransportReceiveStream} WebTransportReceiveStream
1442
- * @typedef {import('./dom').WebTransportDatagramDuplexStream} WebTransportDatagramDuplexStream
1443
- * @typedef {import('./dom').WebTransportReliabilityMode} WebTransportReliabilityMode
1444
- * @typedef {import('./dom').WebTransportCongestionControl} WebTransportCongestionControl
1445
- * @typedef {import('./dom').WebTransportStats} WebTransportStats
1446
- * @typedef {import('./dom').WebTransportDatagramStats} WebTransportDatagramStats
1447
- *
1448
- * @typedef {import('./types').NativeHttpWTSession} NativeHttpWTSession
1449
- *
1450
- * Public API
1451
- * @typedef {import('./types').WebTransportSession} WebTransportSession
1452
- *
1453
- * @typedef {import('./server').HttpServer} HttpServer
1454
- * @typedef {import('./client').HttpClient} HttpClient
1455
- *
1456
- * @typedef {import('stream/web').WritableStreamDefaultController} WritableStreamDefaultController
1457
- */
1458
-
1459
- /**
1460
- * @implements {WebTransportSessionEventHandler}
1461
- * @implements {WebTransportSession}
1462
- */
1463
- export class HttpWTSession {
1464
- /**
1465
- * @param {object} args
1466
- * @param {import('./types').NativeHttpWTSession} [args.object]
1467
- * @param {HttpServer | HttpClient} args.parentobj
1468
- * @param {any | undefined} [args.header= undefined]
1469
- */
1470
- constructor(args) {
1471
- if (args.object) {
1472
- this.objint = args.object
1473
- this.objint.jsobj = this
1474
- }
1475
- this.parentobj = args.parentobj
1476
- /** @type {import('./types').WebTransportSessionState} */
1477
- this.state = 'connecting'
1478
-
1479
- /** @type {((value?: any) => void) | null | undefined} */
1480
- this.readyResolve = null
1481
- /** @type {(() => void) | null | undefined} */
1482
- this.closeHook = null
1483
- /** @type {(any | null | undefined)} */
1484
- this.header = args.header
1485
-
1486
- /** @type {Promise<void>} */
1487
- this.ready = new Promise((resolve, reject) => {
1488
- this.readyResolve = resolve
1489
- this.readyReject = reject
1490
- })
1491
- /** @type {WebTransportReliabilityMode} */
1492
- this.reliability = 'pending'
1493
- /** @type {WebTransportCongestionControl} */
1494
- this.congestionControl = 'default'
1495
- /** @type {Promise<WebTransportCloseInfo>} */
1496
- this.closed = new Promise((resolve, reject) => {
1497
- this.closedResolve = resolve
1498
- this.closedReject = reject
1499
- })
1500
-
1501
- /** @type {Promise<undefined>} */
1502
- this.draining = new Promise((resolve, reject) => {
1503
- this.drainingResolve = resolve
1504
- this.drainingReject = reject
1505
- })
1506
-
1507
- /** @type {ReadableStream<WebTransportBidirectionalStream>} */
1508
- this.incomingBidirectionalStreams = new ReadableStream({
1509
- /** @param {ReadableStreamDefaultController<WebTransportBidirectionalStream>} controller */
1510
- start: (controller) => {
1511
- this.incomBiDiController = controller
1512
- }
1513
- })
1514
- /** @type {ReadableStream<WebTransportReceiveStream>} */
1515
- this.incomingUnidirectionalStreams = new ReadableStream({
1516
- /** @param {ReadableStreamDefaultController<WebTransportReceiveStream>} controller */
1517
- start: (controller) => {
1518
- this.incomUniDiController = controller
1519
- }
1520
- })
1521
-
1522
- /** @type {Array<() => void>} */
1523
- this.writeDatagramRes = []
1524
- /** @type {Array<(err?: Error) => void>} */
1525
- this.writeDatagramRej = []
1526
- /** @type {Array<Promise<void>>} */
1527
- this.writeDatagramProm = []
1528
-
1529
- /** @type {WebTransportDatagramDuplexStream} */
1530
- this.datagrams = {
1531
- /** @type {ReadableStream<Uint8Array>} */
1532
- readable: new ReadableStream({
1533
- start: (
1534
- /** @type {import("stream/web").ReadableByteStreamController} */ controller
1535
- ) => {
1536
- this.incomDatagramController = controller
1537
- },
1538
- type: 'bytes'
1539
- }),
1540
- writable: new WritableStream({
1541
- start: (controller) => {
1542
- this.outgoDatagramController = controller
1543
- },
1544
- write: (chunk, controller) => {
1545
- if (this.state === 'closed') throw new Error('Session is closed')
1546
- if (chunk instanceof Uint8Array) {
1547
- /** @type {Promise<void>} */
1548
- const ret = new Promise((resolve, reject) => {
1549
- this.writeDatagramRes.push(resolve)
1550
- this.writeDatagramRej.push(reject)
1551
- })
1552
- this.writeDatagramProm.push(ret)
1553
- log.trace('b4 datagram write', chunk)
1554
- if (this.objint == null) {
1555
- throw new Error('this.objint is not set')
1556
- }
1557
- this.objint.writeDatagram(chunk)
1558
- return ret
1559
- } else throw new Error('chunk is not of type Uint8Array')
1560
- },
1561
- close: () => {
1562
- // do nothing
1563
- }
1564
- })
1565
- }
1566
-
1567
- /** @type {Array<(stream: WebTransportBidirectionalStream) => void>} */
1568
- this.resolveBiDi = []
1569
- /** @type {Array<(stream: WebTransportSendStream) => void>} */
1570
- this.resolveUniDi = []
1571
- /** @type {Array<(err?: Error) => void>} */
1572
- this.rejectBiDi = []
1573
- /** @type {Array<(err?: Error) => void>} */
1574
- this.rejectUniDi = []
1575
-
1576
- /** @type {Array<(stats: WebTransportStats) => void>} */
1577
- this.resolveSessionStats = []
1578
- /** @type {Array<(err?: Error) => void>} */
1579
- this.rejectSessionStats = []
1580
-
1581
- /** @type {Array<(stats: WebTransportDatagramStats) => void>} */
1582
- this.resolveDatagramStats = []
1583
- /** @type {Array<(err?: Error) => void>} */
1584
- this.rejectDatagramStats = []
1585
-
1586
- /** @type {Set<WebTransportSendStream>} */
1587
- this.sendStreams = new Set()
1588
- /** @type {Set<WebTransportReceiveStream>} */
1589
- this.receiveStreams = new Set()
1590
- /** @type {Set<HttpWTStream>} */
1591
- this.streamObjs = new Set()
1592
-
1593
- /** @type {Set<WritableStreamDefaultController>} */
1594
- this.sendStreamsController = new Set()
1595
- /** @type {Set<ReadableStreamDefaultController>} */
1596
- this.receiveStreamsController = new Set()
1597
- }
1598
-
1599
- /**
1600
- * @param {NativeHttpWTSession} object
1601
- * @param {boolean} reliable
1602
- */
1603
- setSessionObj(object, reliable) {
1604
- if (object) {
1605
- this.objint = object
1606
- this.objint.jsobj = this
1607
- this.reliable = !!reliable
1608
- }
1609
- }
1610
-
1611
- getStats() {
1612
- if (this.objint == null) {
1613
- throw new Error('this.objint not set')
1614
- }
1615
- const prom = new Promise((resolve, reject) => {
1616
- this.resolveSessionStats.push(resolve)
1617
- this.rejectSessionStats.push(reject)
1618
- })
1619
- this.objint.orderSessionStats()
1620
- return prom
1621
- }
1622
-
1623
- /**
1624
- * @param {SessionStatsEvent} evt
1625
- */
1626
- onSessionStats({
1627
- timestamp,
1628
- expiredOutgoing = BigInt(0),
1629
- lostOutgoing = BigInt(0),
1630
- // non Datagram
1631
- minRtt = 0,
1632
- smoothedRtt = 0,
1633
- rttVariation = 0,
1634
- estimatedSendRateBps
1635
- }) {
1636
- const res = this.resolveSessionStats.pop()
1637
- this.rejectSessionStats.pop()
1638
- if (res)
1639
- res({
1640
- timestamp,
1641
- bytesSent: BigInt(0),
1642
- packetsSent: BigInt(0),
1643
- packetsLost: BigInt(0),
1644
- numOutgoingStreamsCreated: 0,
1645
- numIncomingStreamsCreated: 0,
1646
- bytesReceived: BigInt(0),
1647
- packetsReceived: BigInt(0),
1648
- smoothedRtt,
1649
- rttVariation,
1650
- minRtt,
1651
- estimatedSendRate: estimatedSendRateBps,
1652
- datagrams: {
1653
- timestamp,
1654
- expiredOutgoing,
1655
- droppedIncoming: BigInt(0),
1656
- lostOutgoing
1657
- }
1658
- })
1659
- }
1660
-
1661
- /**
1662
- * @param {DatagramStatsEvent} evt
1663
- */
1664
- onDatagramStats({
1665
- timestamp,
1666
- expiredOutgoing = BigInt(0),
1667
- lostOutgoing = BigInt(0)
1668
- }) {
1669
- const res = this.resolveDatagramStats.pop()
1670
- this.rejectDatagramStats.pop()
1671
- if (res)
1672
- res({
1673
- timestamp,
1674
- expiredOutgoing,
1675
- droppedIncoming: BigInt(0),
1676
- lostOutgoing
1677
- })
1678
- }
1679
-
1680
- async waitForDatagramsSend() {
1681
- while (this.writeDatagramProm.length > 0) {
1682
- try {
1683
- await Promise.allSettled(this.writeDatagramProm)
1684
- } catch (error) {
1685
- log.error('datagram promise failed ', error)
1686
- }
1687
- }
1688
- }
1689
-
1690
- notifySessionDraining() {
1691
- if (this.objint == null) {
1692
- throw new Error('this.objint not set')
1693
- }
1694
- this.objint.notifySessionDraining()
1695
- }
1696
-
1697
- /**
1698
- * @param {HttpWTStream} stream
1699
- */
1700
- addStreamObj(stream) {
1701
- this.streamObjs.add(stream)
1702
- }
1703
-
1704
- /**
1705
- * @param {HttpWTStream} stream
1706
- */
1707
- removeStreamObj(stream) {
1708
- this.streamObjs.delete(stream)
1709
- }
1710
-
1711
- /**
1712
- * @param {WebTransportSendStream} stream
1713
- * @param {WritableStreamDefaultController} controller
1714
- */
1715
- addSendStream(stream, controller) {
1716
- this.sendStreams.add(stream)
1717
- this.sendStreamsController.add(controller)
1718
- }
1719
-
1720
- /**
1721
- * @param {WebTransportSendStream} stream
1722
- * @param {WritableStreamDefaultController} controller
1723
- */
1724
- removeSendStream(stream, controller) {
1725
- this.sendStreams.delete(stream)
1726
- this.sendStreamsController.delete(controller)
1727
- }
1728
-
1729
- /**
1730
- * @param {WebTransportReceiveStream } stream
1731
- * @param {ReadableStreamDefaultController} controller
1732
- */
1733
- addReceiveStream(stream, controller) {
1734
- this.receiveStreams.add(stream)
1735
- this.receiveStreamsController.add(controller)
1736
- }
1737
-
1738
- /**
1739
- * @param {WebTransportReceiveStream } stream
1740
- * @param {ReadableStreamDefaultController} controller
1741
- */
1742
- removeReceiveStream(stream, controller) {
1743
- this.receiveStreams.delete(stream)
1744
- this.receiveStreamsController.delete(controller)
1745
- }
1746
-
1747
- /**
1748
- * @returns {Promise<WebTransportBidirectionalStream>}
1749
- */
1750
- createBidirectionalStream() {
1751
- if (this.objint == null) {
1752
- throw new Error('this.objint not set')
1753
- }
1754
- /** @type {Promise<WebTransportBidirectionalStream>} */
1755
- const prom = new Promise((resolve, reject) => {
1756
- this.resolveBiDi.push(resolve)
1757
- this.rejectBiDi.push(reject)
1758
- })
1759
- this.objint.orderBidiStream()
1760
- return prom
1761
- }
1762
-
1763
- /**
1764
- *@returns {Promise<WebTransportSendStream>}
1765
- */
1766
- createUnidirectionalStream() {
1767
- if (this.objint == null) {
1768
- throw new Error('this.objint not set')
1769
- }
1770
- /** @type {Promise<WebTransportSendStream>} */
1771
- const prom = new Promise((resolve, reject) => {
1772
- this.resolveUniDi.push(resolve)
1773
- this.rejectUniDi.push(reject)
1774
- })
1775
- this.objint.orderUnidiStream()
1776
- return prom
1777
- }
1778
-
1779
- /**
1780
- * @param {object} [closeInfo]
1781
- * @param {number} closeInfo.closeCode
1782
- * @param {string} closeInfo.reason
1783
- * @returns {void}
1784
- */
1785
- close(closeInfo) {
1786
- log('closeinfo', closeInfo)
1787
- if (this.state === 'closed' || this.state === 'failed') return
1788
- if (this.objint) {
1789
- this.objint.close({
1790
- code: closeInfo?.closeCode ?? 0,
1791
- reason: closeInfo?.reason.substring(0, 1023) ?? ''
1792
- })
1793
- }
1794
- }
1795
-
1796
- onReady(/* error */) {
1797
- this.state = 'connected'
1798
- if (!this.reliable) this.reliability = 'supports-unreliable'
1799
- else this.reliability = 'reliable-only'
1800
- if (this.readyResolve) this.readyResolve()
1801
- delete this.readyResolve
1802
- }
1803
-
1804
- /**
1805
- * @param {SessionCloseEvent} args
1806
- */
1807
- onClose(args) {
1808
- delete this.objint // not valid any more
1809
-
1810
- if (this.state !== 'connected') {
1811
- log.error(
1812
- 'session was closed before state was "connected" - it was "%s"',
1813
- this.state
1814
- )
1815
- this.state = 'failed'
1816
-
1817
- // make sure the event loop can still exit
1818
- if (this.closeHook) {
1819
- this.closeHook()
1820
- delete this.closeHook
1821
- }
1822
-
1823
- // closed before connected
1824
- const error = new WebTransportError('Opening handshake failed.')
1825
- this.readyReject(error)
1826
- this.closedReject(error)
1827
- return
1828
- }
1829
-
1830
- log('onClose')
1831
- this.streamObjs.forEach((ele) => ele.finalDrain())
1832
-
1833
- const error = new WebTransportError('Session closed')
1834
-
1835
- for (const rej of this.rejectBiDi) rej(error)
1836
- for (const rej of this.rejectUniDi) rej(error)
1837
- for (const rej of this.writeDatagramRej) rej(error)
1838
- for (const rej of this.rejectSessionStats) rej(error)
1839
- for (const rej of this.rejectDatagramStats) rej(error)
1840
-
1841
- this.writeDatagramRej = []
1842
- this.writeDatagramRes = []
1843
- this.writeDatagramProm = []
1844
- this.resolveBiDi = []
1845
- this.resolveUniDi = []
1846
- this.rejectBiDi = []
1847
- this.rejectUniDi = []
1848
-
1849
- this.resolveSessionStats = []
1850
- this.rejectSessionStats = []
1851
- this.resolveDatagramStats = []
1852
- this.rejectDatagramStats = []
1853
-
1854
- this.incomBiDiController.close()
1855
- this.incomUniDiController.close()
1856
- this.incomDatagramController.close()
1857
- // this.outgoDatagramController.error(errorcode)
1858
- this.state = 'closed'
1859
-
1860
- const wtError = new WebTransportError(
1861
- `Session closed (on process ${pid}) with code ` +
1862
- args.errorcode +
1863
- ' and reason' +
1864
- args.error
1865
- )
1866
-
1867
- this.sendStreamsController.forEach((ele) => ele.error(wtError))
1868
- this.receiveStreamsController.forEach((ele) => ele.error(wtError))
1869
-
1870
- this.streamObjs.forEach((ele) => (ele.readableclosed = true))
1871
-
1872
- this.sendStreams.clear()
1873
- this.receiveStreams.clear()
1874
- this.sendStreamsController.clear()
1875
- this.receiveStreamsController.clear()
1876
- this.streamObjs.clear()
1877
-
1878
- if (this.closedResolve)
1879
- this.closedResolve({
1880
- closeCode: args.errorcode,
1881
- reason: args.error ? args.error : ''
1882
- })
1883
- if (this.closeHook) {
1884
- this.closeHook()
1885
- delete this.closeHook
1886
- }
1887
- }
1888
-
1889
- /**
1890
- * @param {NewStreamEvent} args
1891
- */
1892
- onStream(args) {
1893
- const strobj = new HttpWTStream({
1894
- object: args.stream,
1895
- parentobj: this,
1896
- transport: this.parentobj,
1897
- bidirectional: args.bidirectional,
1898
- incoming: args.incoming
1899
- })
1900
- this.addStreamObj(strobj)
1901
- if (args.incoming) {
1902
- if (args.bidirectional) {
1903
- this.incomBiDiController.enqueue(strobj)
1904
- } else {
1905
- this.incomUniDiController.enqueue(strobj.readable)
1906
- }
1907
- } else {
1908
- if (args.bidirectional) {
1909
- if (this.resolveBiDi.length === 0)
1910
- throw new Error('Got bidirectional stream without asking for it')
1911
- this.rejectBiDi.shift()
1912
- const curres = this.resolveBiDi.shift()
1913
-
1914
- if (
1915
- curres != null &&
1916
- strobj.readable != null &&
1917
- strobj.writable != null
1918
- ) {
1919
- curres({
1920
- readable: strobj.readable,
1921
- writable: strobj.writable
1922
- })
1923
- }
1924
- } else {
1925
- if (this.resolveUniDi.length === 0)
1926
- throw new Error('Got unidirectional stream without asking for it')
1927
- this.rejectUniDi.shift()
1928
- const curres = this.resolveUniDi.shift()
1929
-
1930
- if (curres != null && strobj.writable != null) {
1931
- curres(strobj.writable)
1932
- }
1933
- }
1934
- }
1935
- }
1936
-
1937
- /**
1938
- * @param {DatagramReceivedEvent} args
1939
- */
1940
- onDatagramReceived(args) {
1941
- log.trace('datagram received', args.datagram)
1942
- // console.log('datagram received', args.datagram, Date.now())
1943
- if (this.incomDatagramController.byobRequest) {
1944
- /** @type {ReadableStreamBYOBRequest} */
1945
- const byob = this.incomDatagramController.byobRequest
1946
- /** @type {Uint8Array} */
1947
- // @ts-ignore
1948
- const view = byob?.view
1949
- // @ts-ignore
1950
- if (!(view instanceof Uint8Array))
1951
- throw new Error('byob view is not a Uint8Array')
1952
- if (view.byteLength < args.datagram.byteLength) {
1953
- throw new Error('supplied view is not large enough.')
1954
- }
1955
- const destview = new Uint8Array(
1956
- view.buffer,
1957
- 0 + view.byteOffset,
1958
- args.datagram.byteLength
1959
- )
1960
- destview.set(args.datagram)
1961
- byob.respond(args.datagram.byteLength)
1962
- }
1963
- this.incomDatagramController.enqueue(new Uint8Array(args.datagram))
1964
- }
1965
-
1966
- /**
1967
- * @param {DatagramSendEvent} args
1968
- */
1969
- onDatagramSend(args) {
1970
- if (this.state === 'closed') return
1971
- this.writeDatagramRej.shift()
1972
- this.writeDatagramProm.shift()
1973
- const res = this.writeDatagramRes.shift()
1974
-
1975
- if (res != null) {
1976
- res()
1977
- }
1978
- }
1979
-
1980
- /**
1981
- * @param {GoawayReceivedEvent} args
1982
- */
1983
- onGoAwayReceived(args) {
1984
- if (this.drainingResolve) this.drainingResolve(undefined)
1985
- this.state = 'draining'
1986
- }
1987
- }
1988
-
1989
- const pid = typeof process !== 'undefined' ? process.pid : 0
1990
- const log = console.log
1991
-
1992
- /**
1993
- * @typedef {import('./session').HttpWTSession} HttpWTSession
1994
- * @typedef {import('./types').HttpClientInit} HttpClientInit
1995
- *
1996
- * Http3Client events
1997
- * @typedef {import('./types').HttpClientEventHandler} HttpClientEventHandler
1998
- * @typedef {import('./types').ClientConnectedEvent} ClientConnectedEvent
1999
- * @typedef {import('./types').ClientWebtransportSupportEvent} ClientWebtransportSupportEvent
2000
- * @typedef {import('./types').HttpWTSessionVisitorEvent} HttpWTSessionVisitorEvent
2001
- */
2002
-
2003
- /**
2004
- * @implements {HttpClientEventHandler}
2005
- */
2006
- export class HttpClient {
2007
- /**
2008
- * @param {HttpClientInit} args
2009
- */
2010
- constructor(args) {
2011
- /** @type {HttpClientInit| undefined} */
2012
- this.args = args
2013
-
2014
- /** @type {{ resolve: (value?: any) => void, reject: (err?: Error) => void} | null | undefined} */
2015
- this.sessionProm = null
2016
-
2017
- /** @type {Promise<void> | undefined} */
2018
- this.sessionobj = new Promise((resolve, reject) => {
2019
- this.sessionProm = { resolve, reject }
2020
- }).catch(() => {}) // add default handler if no one cares
2021
- /** @type {HttpWTSession | null | undefined} */
2022
- this.sessionobjint = null
2023
- this.closeHookSession = this.closeHookSession.bind(this)
2024
-
2025
- /** @type {{ resolve: (value?: any) => void, reject: (err?: Error) => void} | null | undefined} */
2026
- this.webtransportProm = null
2027
- /** @type {{ resolve: (value?: any) => void, reject: (err?: Error) => void} | null | undefined} */
2028
- this.quicconnectedProm = null
2029
-
2030
- this._quicConnectTimeout = args.quicConnectTimeout ?? 8000
2031
- this._webTransportConnectTimeout = args.webTransportConnectTimeout ?? 2000
2032
- }
2033
-
2034
- /**
2035
- * @param {Object} args
2036
- * @param {boolean} args.createTransport
2037
- * @param {string} args.path
2038
- */
2039
- async handleConnection({ createTransport, path }) {
2040
- if (createTransport) this.createTransportInt({ path })
2041
-
2042
- this.quicconnected = new Promise((resolve, reject) => {
2043
- this.quicconnectedProm = { resolve, reject }
2044
- })
2045
- this.webtransport = new Promise((resolve, reject) => {
2046
- this.webtransportProm = { resolve, reject }
2047
- })
2048
-
2049
- const timeout = setTimeout(() => {
2050
- if (this.quicconnectedProm) {
2051
- log.error('quic connection timeout')
2052
- this.quicconnectedProm.reject(
2053
- new WebTransportError('Opening handshake failed.')
2054
- )
2055
- delete this.quicconnectedProm
2056
- }
2057
- }, this._quicConnectTimeout)
2058
- try {
2059
- await this.quicconnected
2060
- } finally {
2061
- clearTimeout(timeout)
2062
- }
2063
- }
2064
-
2065
- /**
2066
- * @param {HttpWTSession} sessionobj
2067
- * @param {string} path
2068
- * @returns
2069
- */
2070
- async createWTSession(sessionobj, path) {
2071
- // now create Webtransport session
2072
- const timeout = setTimeout(() => {
2073
- if (this.webtransportProm) {
2074
- log.error('webtransport connection timeout')
2075
- this.webtransportProm.reject(
2076
- new WebTransportError('Opening handshake failed.')
2077
- )
2078
- delete this.webtransportProm
2079
- }
2080
- }, this._webTransportConnectTimeout)
2081
- await this.webtransport // wait for webtransport support
2082
- clearTimeout(timeout)
2083
-
2084
- // ok now we open the session
2085
- this.sessionobjint = sessionobj
2086
- this.transportInt.openWTSession(path)
2087
-
2088
- // we wait for a new session
2089
- const sessobj = await this.sessionobj
2090
-
2091
- delete this.sessionobj
2092
-
2093
- return sessobj
2094
- }
2095
-
2096
- closeHookSession() {
2097
- if (this.transportInt != null) {
2098
- this.transportInt.closeClient()
2099
- }
2100
-
2101
- this.stopped = true
2102
- }
2103
-
2104
- /**
2105
- * @param {import('./types').SessionCloseEvent} args
2106
- */
2107
- onClientError(args) {
2108
- if (this.sessionobjint != null) {
2109
- this.sessionobjint.onClose(args)
2110
- }
2111
- }
2112
-
2113
- /**
2114
- * @param {ClientConnectedEvent} args
2115
- */
2116
- onClientConnected(args) {
2117
- this.transportIntSwitchToReliable = undefined
2118
- if (this.quicconnectedProm) {
2119
- if (args.success) this.quicconnectedProm.resolve()
2120
- else
2121
- this.quicconnectedProm.reject(
2122
- new WebTransportError('Opening handshake failed.')
2123
- )
2124
- delete this.quicconnectedProm
2125
- } else
2126
- throw new WebTransportError('Client connected with no pending promise')
2127
- }
2128
-
2129
- /**
2130
- * @param {ClientWebtransportSupportEvent} args
2131
- */
2132
- onClientWebTransportSupport(args) {
2133
- if (this.webtransportProm) {
2134
- this.webtransportProm.resolve()
2135
- delete this.webtransportProm
2136
- }
2137
- }
2138
-
2139
- /**
2140
- * @param {HttpWTSessionVisitorEvent} args
2141
- */
2142
- onHttpWTSessionVisitor(args) {
2143
- // create Http Visitor
2144
- if (args.session && this.sessionProm && this.sessionobjint) {
2145
- this.sessionobjint.setSessionObj(args.session, !!args.reliable)
2146
- args.session.jsobj.closeHook = this.closeHookSession
2147
- delete this.sessionobjint
2148
- this.sessionProm.resolve(args.session)
2149
- delete this.sessionProm
2150
- } else {
2151
- throw new WebTransportError(
2152
- 'Http3WTSessionVisitor no object session or nor sessionprom'
2153
- )
2154
- }
2155
- }
2156
-
2157
- /**
2158
- * @param{{path: string}} [args]
2159
- **/
2160
- createTransportInt(args) {
2161
- const path = args?.path
2162
- if (this.transportInt != null) {
2163
- return
2164
- }
2165
-
2166
- try {
2167
- // @ts-ignore
2168
- if (this.args?.forceReliable || !this.args.createUnreliableClient) {
2169
- // internal option for unit tests only
2170
- // @ts-ignore
2171
- this.transportInt = this.args.createReliableClient(this)
2172
- } else {
2173
- // @ts-ignore
2174
- this.transportInt = this.args.createUnreliableClient(this)
2175
- if (!this.args?.requireUnreliable) {
2176
- const args = this.args
2177
- this.transportIntSwitchToReliable = () => {
2178
- if (this.transportInt != null) {
2179
- this.transportInt.closeClient()
2180
- }
2181
- // @ts-ignore
2182
- this.transportInt = args.createReliableClient(this)
2183
- this.transportInt.jsobj = this
2184
- if (this.transportInt.createTransport) {
2185
- this.transportInt.createTransport({ path })
2186
- }
2187
- this.transportIntSwitchToReliable = undefined
2188
- }
2189
- }
2190
- }
2191
- } catch (/** @type {any} */ err) {
2192
- const error = new WebTransportError('Opening handshake failed.')
2193
- error.stack = err.stack
2194
-
2195
- throw error
2196
- }
2197
- delete this.args
2198
- this.transportInt.jsobj = this
2199
- if (this.transportInt.createTransport) {
2200
- this.transportInt.createTransport({ path })
2201
- }
2202
- }
2203
- }
2204
-
2205
- /**
2206
- * @typedef {import('./dom').WebTransport} WebTransport
2207
- * @typedef {import('./dom').WebTransportCloseInfo} WebTransportCloseInfo
2208
- * @typedef {import('./dom').WebTransportBidirectionalStream} WebTransportBidirectionalStream
2209
- * @typedef {import('./dom').WebTransportReceiveStream} WebTransportReceiveStream
2210
- * @typedef {import('./error').WebTransportError} WebTransportError
2211
- */
2212
-
2213
- /**
2214
- * @template T
2215
- * @typedef {import('node:stream/web').ReadableStream<T>} ReadableStream<T>
2216
- */
2217
-
2218
- /**
2219
- * @typedef {import('./dom').WebTransport} WebTransportInterface
2220
- *
2221
- * @implements {WebTransportInterface}
2222
- */
2223
- export class WebTransportPonyfill extends WebTransportBase {
2224
- /**
2225
- * @param{{client: HttpClient, sessionint: HttpWTSession, ourl: URL}} args
2226
- */
2227
- startUpConnection({ client, sessionint, ourl }) {
2228
- client
2229
- .handleConnection({ createTransport: true, path: ourl.pathname })
2230
- .then(() => client.createWTSession(sessionint, ourl.pathname))
2231
- .catch((error) => {
2232
- client.closeHookSession()
2233
- sessionint.readyReject(error)
2234
- sessionint.closedReject(error)
2235
- })
2236
- }
2237
-
2238
- /**
2239
- * @param{import('./types.js').HttpWebTransportInit} args
2240
- * @return {{sessionint: HttpWTSession, client: HttpClient}}
2241
- */
2242
- createClient(args) {
2243
- const client = new HttpClient({
2244
- createReliableClient: (client) => {
2245
- // @ts-ignore
2246
- return new Http2WebTransportBrowser({ ...args })
2247
- },
2248
- ...args
2249
- })
2250
- const sessionint = new HttpWTSession({
2251
- /* object: args.session, */
2252
- parentobj: client
2253
- })
2254
- return { client, sessionint }
2255
- }
2256
- }
2257
-
2258
- export class WebTransportPolyfill {
2259
- /**
2260
- * @param {string} url
2261
- * @param {import('./dom.js').WebTransportOptions} [args]
2262
- */
2263
- constructor(url, args) {
2264
- this.curtype = 'native'
2265
- this.closeset = false
2266
- this.allowFallback = true
2267
- this.initiatedFallback = false
2268
- this.args = args
2269
-
2270
- this.closed = new Promise((resolve, reject) => {
2271
- this.closeRes = resolve
2272
- this.closeRej = reject
2273
- })
2274
-
2275
- this.ready = new Promise((resolve, reject) => {
2276
- this.readyRes = resolve
2277
- this.readyRej = reject
2278
- })
2279
-
2280
- this.draining = new Promise((resolve, reject) => {
2281
- this.drainingRes = resolve
2282
- this.drainingRej = reject
2283
- })
2284
-
2285
- /** @type {WebTransport|WebTransportPonyfill} */
2286
- // @ts-ignore
2287
- // eslint-disable-next-line no-undef
2288
- this.curtransport = new WebTransport(url, args)
2289
-
2290
- const initiateFallback = () => {
2291
- this.initiatedFallback = true
2292
- this.curtype = 'websocket'
2293
- this.curtransport = new WebTransportPonyfill(url, args)
2294
- this.curtransport.ready
2295
- .then((val) => this.readyRes(val))
2296
- .catch((error) => this.readyRej(error))
2297
- this.curtransport.closed
2298
- .then((val) => this.closeRes(val))
2299
- .catch((error) => this.closeRej(error))
2300
- this.curtransport.draining
2301
- .then((val) => this.drainingRes(val))
2302
- .catch((error) => this.drainingRej(error))
2303
- }
2304
-
2305
- this.curtransport.ready
2306
- .then((val) => {
2307
- this.allowFallback = false
2308
- this.readyRes(val)
2309
- })
2310
- .catch((error) => {
2311
- if (this.allowFallback && !this.closeset) {
2312
- if (
2313
- !this.initiatedFallback &&
2314
- !this.curtransport?.reliability.supportsReliableOnly // way how browser signals support for http/2, no polyfill needed in this cases
2315
- ) {
2316
- initiateFallback()
2317
- }
2318
- } else {
2319
- this.readyRej(error)
2320
- }
2321
- })
2322
- this.curtransport.closed
2323
- .then((val) => {
2324
- if (this.curtype === 'native') this.closeRes(val)
2325
- })
2326
- .catch((error) => {
2327
- if (this.allowFallback && !this.closeset) {
2328
- if (
2329
- !this.initiatedFallback &&
2330
- !this.curtransport?.reliability?.supportsReliableOnly // way how browser signals support for http/2, no polyfill needed in this cases
2331
- ) {
2332
- initiateFallback()
2333
- }
2334
- } else {
2335
- this.closeRej(error)
2336
- }
2337
- })
2338
- if (this.curtransport.draining) {
2339
- // @ts-ignore
2340
- this.curtransport.draining
2341
- .then((/** @type {any} */ val) => {
2342
- if (this.curtype === 'native') this.drainingRes(val)
2343
- })
2344
- .catch((/** @type {WebTransportError} */ error) => {
2345
- if (this.curtype === 'native') this.drainingRej(error)
2346
- })
2347
- }
2348
- /** @type {import('./dom').WebTransportDatagramDuplexStream} */
2349
- // @ts-ignore
2350
- this.datagrams = {}
2351
- // @ts-ignore
2352
- // eslint-disable-next-line no-undef
2353
- this.datagrams.readable = new ReadableStream({
2354
- start: async (controller) => {
2355
- await this.ready
2356
- this.datagramsReader = this.curtransport.datagrams.readable.getReader()
2357
- },
2358
- pull: async (controller) => {
2359
- const { value, done } = await this.datagramsReader.read()
2360
- if (value) controller.enqueue(value)
2361
- if (done) controller.close()
2362
- },
2363
- cancel: async (reason) => {
2364
- await this.datagramsReader.cancel(reason)
2365
- }
2366
- })
2367
- // @ts-ignore
2368
- // eslint-disable-next-line no-undef
2369
- this.datagrams.writable = new WritableStream({
2370
- start: async (controller) => {
2371
- await this.ready
2372
- this.datagramsWriter = this.curtransport.datagrams.writable.getWriter()
2373
- },
2374
- write: async (chunk, controller) => {
2375
- await this.datagramsWriter.write(chunk)
2376
- },
2377
- abort: async (reason) => {
2378
- await this.datagramsWriter.abort(reason)
2379
- },
2380
- close: async () => {
2381
- await this.datagramsWriter.close()
2382
- }
2383
- })
2384
- // eslint-disable-next-line no-undef
2385
- this.incomingBidirectionalStreams = new ReadableStream({
2386
- start: async (controller) => {
2387
- await this.ready
2388
- this.incomingBidirectionalStreamsReader =
2389
- this.curtransport.incomingBidirectionalStreams.getReader()
2390
- },
2391
- pull: async (controller) => {
2392
- const { value, done } =
2393
- await this.incomingBidirectionalStreamsReader.read()
2394
- if (value) controller.enqueue(value)
2395
- if (done) controller.close()
2396
- },
2397
- cancel: async (reason) => {
2398
- await this.incomingBidirectionalStreamsReader.cancel(reason)
2399
- }
2400
- })
2401
- // eslint-disable-next-line no-undef
2402
- this.incomingUnidirectionalStreams = new ReadableStream({
2403
- start: async (controller) => {
2404
- await this.ready
2405
- this.incomingUnidirectionalStreamsReader =
2406
- this.curtransport.incomingUnidirectionalStreams.getReader()
2407
- },
2408
- pull: async (controller) => {
2409
- const { value, done } =
2410
- await this.incomingUnidirectionalStreamsReader.read()
2411
- if (value) controller.enqueue(value)
2412
- if (done) controller.close()
2413
- },
2414
- cancel: async (reason) => {
2415
- await this.incomingUnidirectionalStreamsReader.cancel(reason)
2416
- }
2417
- })
2418
- }
2419
-
2420
- get congestionControl() {
2421
- // @ts-ignore
2422
- return this.curtransport?.congestionControl || undefined
2423
- }
2424
-
2425
- get reliability() {
2426
- // @ts-ignore
2427
- return this.curtransport?.reliability || undefined
2428
- }
2429
-
2430
- get supportsReliableOnly() {
2431
- return true
2432
- }
2433
-
2434
- getStats() {
2435
- // @ts-ignore
2436
- return this.curtransport.getStats()
2437
- }
2438
-
2439
- /**
2440
- * @param {WebTransportCloseInfo} [closeinfo]
2441
- */
2442
- close(closeinfo) {
2443
- this.closeset = true
2444
- this.curtransport.close(closeinfo)
2445
- }
2446
-
2447
- async createBidirectionalStream() {
2448
- await this.ready
2449
- return await this.curtransport.createBidirectionalStream()
2450
- }
2451
-
2452
- async createUnidirectionalStream() {
2453
- await this.ready
2454
- return await this.curtransport.createUnidirectionalStream()
2455
- }
2456
- }
2457
-
2458
- // import { WebTransportPolyfill } from '../lib/index.browser'
2459
- // @ts-nocheck
2460
- let client
2461
-
2462
- const afterEach = async () => {
2463
- if (client != null) {
2464
- client.close()
2465
- client = undefined
2466
- }
2467
- }
2468
-
2469
- class TimeoutError extends Error {
2470
- /**
2471
- * @param {string} message
2472
- */
2473
- constructor(message) {
2474
- super(message)
2475
-
2476
- this.name = this[Symbol.toStringTag] = 'TimeoutError'
2477
- }
2478
- }
2479
-
2480
- async function pTimeout(promise, timeout) {
2481
- let ref
2482
-
2483
- const value = await Promise.race([
2484
- promise,
2485
- new Promise((resolve, reject) => {
2486
- ref = setTimeout(() => {
2487
- reject(new TimeoutError('timeout'))
2488
- }, timeout)
2489
- })
2490
- ])
2491
-
2492
- clearTimeout(ref)
2493
-
2494
- return value
2495
- }
2496
-
2497
- async function getReaderValue(readableStream) {
2498
- const reader = readableStream.getReader()
2499
-
2500
- try {
2501
- const { done, value } = await reader.read()
2502
-
2503
- if (done) {
2504
- throw new Error('Stream ended')
2505
- }
2506
-
2507
- if (!value) {
2508
- throw new Error('Stream value was undefined')
2509
- }
2510
-
2511
- return value
2512
- } finally {
2513
- reader.releaseLock()
2514
- }
2515
- }
2516
-
2517
- async function readStream(readable, expected) {
2518
- const reader = readable.getReader()
2519
-
2520
- try {
2521
- /** @type {T[]} */
2522
- const output = []
2523
-
2524
- while (true) {
2525
- const { done, value } = await reader.read()
2526
-
2527
- if (done) {
2528
- break
2529
- }
2530
-
2531
- if (value != null) {
2532
- output.push(value)
2533
- }
2534
-
2535
- if (expected != null && output.length === expected) {
2536
- break
2537
- }
2538
- }
2539
-
2540
- return output
2541
- } finally {
2542
- reader.releaseLock()
2543
- }
2544
- }
2545
-
2546
- const KNOWN_BYTES = [
2547
- Uint8Array.from([0, 1, 2, 3, 4]),
2548
- Uint8Array.from([5, 6, 7, 8, 9]),
2549
- Uint8Array.from([10, 11, 12, 13, 14]),
2550
- Uint8Array.from([15, 16, 17, 18, 19]),
2551
- Uint8Array.from([20, 21, 22, 23, 24])
2552
- ]
2553
-
2554
- function readCertHash(certHash) {
2555
- return Uint8Array.from(`${certHash}`.split(':').map((i) => parseInt(i, 16)))
2556
- }
2557
-
2558
- async function writeStream(writable, input) {
2559
- const writer = writable.getWriter()
2560
-
2561
- for (const buf of input) {
2562
- await writer.ready
2563
- await writer.write(buf)
2564
- }
2565
-
2566
- await writer.ready
2567
- await writer.releaseLock()
2568
- await writable.close()
2569
- }
2570
-
2571
- async function main() {
2572
- {
2573
- client = new WebTransportPolyfill(
2574
- `${SERVER_URL}/bidirectional_client_initiated_echo`,
2575
- {
2576
- serverCertificateHashes: [
2577
- {
2578
- algorithm: 'sha-256',
2579
- value: readCertHash(CERT_HASH)
2580
- }
2581
- ]
2582
- }
2583
- )
2584
- await client.ready
2585
-
2586
- const stream = await client.createBidirectionalStream()
2587
- await writeStream(stream.writable, KNOWN_BYTES)
2588
-
2589
- await readStream(stream.readable, KNOWN_BYTES.length)
2590
- }
2591
- console.log('test 1 complete')
2592
- await afterEach()
2593
- {
2594
- // client context - waits for the server to open a bidi stream then pipes it back to them
2595
- client = new WebTransportPolyfill(
2596
- `${SERVER_URL}/bidirectional_server_initiated_echo`,
2597
- {
2598
- serverCertificateHashes: [
2599
- {
2600
- algorithm: 'sha-256',
2601
- value: readCertHash(CERT_HASH)
2602
- }
2603
- ]
2604
- }
2605
- )
2606
- await client.ready
2607
-
2608
- const bidiStream = await getReaderValue(client.incomingBidirectionalStreams)
2609
-
2610
- // redirect input to output
2611
- await bidiStream.readable.pipeTo(bidiStream.writable)
2612
-
2613
- // the remote will close the session
2614
- await client.closed
2615
- }
2616
- console.log('test 2 complete')
2617
- await afterEach()
2618
- {
2619
- // client context - connects to the server, sends some datagrams and reads the response
2620
- client = new WebTransportPolyfill(`${SERVER_URL}/datagrams_client_send`, {
2621
- serverCertificateHashes: [
2622
- {
2623
- algorithm: 'sha-256',
2624
- value: readCertHash(CERT_HASH)
2625
- }
2626
- ]
2627
- })
2628
- await client.ready
2629
-
2630
- const writer = client.datagrams.writable.getWriter()
2631
- let closed = false
2632
-
2633
- // write datagrams until the server receives one and closes the connection
2634
- // eslint-disable-next-line promise/catch-or-return
2635
- Promise.resolve().then(async () => {
2636
- // eslint-disable-next-line no-unmodified-loop-condition
2637
- while (!closed) {
2638
- try {
2639
- await writer.ready
2640
- await writer.write(Uint8Array.from([0, 1, 2, 3, 4]))
2641
- await new Promise((resolve) => setTimeout(resolve, 100))
2642
- } catch {
2643
- // the session can be closed while we are writing
2644
- }
2645
- }
2646
- })
2647
-
2648
- await client.closed
2649
- closed = true
2650
- }
2651
- console.log('test 3 complete')
2652
- await afterEach()
2653
- {
2654
- // client context - pipes the server's datagrams back to them
2655
- client = new WebTransportPolyfill(`${SERVER_URL}/datagrams_server_send`, {
2656
- serverCertificateHashes: [
2657
- {
2658
- algorithm: 'sha-256',
2659
- value: readCertHash(CERT_HASH)
2660
- }
2661
- ]
2662
- })
2663
- await client.ready
2664
-
2665
- // datagram transport is unreliable, at least one message should make it through
2666
- const expected = 1
2667
-
2668
- await pTimeout(readStream(client.datagrams.readable, expected), 1000)
2669
- }
2670
- console.log('test 4 complete')
2671
- await afterEach()
2672
-
2673
- client = new WebTransportPolyfill(`${SERVER_URL}/session_close`, {
2674
- serverCertificateHashes: [
2675
- {
2676
- algorithm: 'sha-256',
2677
- value: readCertHash(CERT_HASH)
2678
- }
2679
- ]
2680
- })
2681
- await client.ready
2682
-
2683
- await client.closed
2684
- console.log('test 5 complete')
2685
-
2686
- await afterEach()
2687
-
2688
- client = new WebTransportPolyfill(`${SERVER_URL}/session_close_with_reason`, {
2689
- serverCertificateHashes: [
2690
- {
2691
- algorithm: 'sha-256',
2692
- value: readCertHash(CERT_HASH)
2693
- }
2694
- ]
2695
- })
2696
- await client.ready
2697
-
2698
- await client.closed
2699
- console.log('test 6 complete')
2700
- await afterEach()
2701
-
2702
- client = new WebTransportPolyfill(`https://127.0.0.1:39821`, {
2703
- serverCertificateHashes: [
2704
- {
2705
- algorithm: 'sha-256',
2706
- value: readCertHash(CERT_HASH)
2707
- }
2708
- ],
2709
- quicConnectTimeout: 100,
2710
- webTransportConnectTimeout: 100
2711
- })
2712
-
2713
- await Promise.all([
2714
- client.closed.catch((err) => err),
2715
- client.ready.catch((err) => err)
2716
- ])
2717
- console.log('test 7 complete')
2718
- await afterEach()
2719
-
2720
- client = new WebTransportPolyfill(`${SERVER_URL}/non_existant`, {
2721
- serverCertificateHashes: [
2722
- {
2723
- algorithm: 'sha-256',
2724
- value: readCertHash(CERT_HASH)
2725
- }
2726
- ]
2727
- })
2728
-
2729
- await Promise.all([
2730
- client.closed.catch((err) => err),
2731
- client.ready.catch((err) => err)
2732
- ])
2733
- console.log('test 8 complete')
2734
- await afterEach()
2735
-
2736
- client = new WebTransportPolyfill(`${SERVER_URL}/session_close`, {
2737
- serverCertificateHashes: [
2738
- {
2739
- algorithm: 'sha-256',
2740
- value: readCertHash(CERT_HASH + ':DE:AD:BE:EF')
2741
- }
2742
- ]
2743
- })
2744
-
2745
- await Promise.all([
2746
- client.closed.catch((err) => err),
2747
- client.ready.catch((err) => err)
2748
- ])
2749
- console.log('test 10 complete')
2750
- await afterEach()
2751
- client = new WebTransportPolyfill(`${SERVER_URL}/session_close`, {
2752
- serverCertificateHashes: [
2753
- {
2754
- algorithm: 'sha-256',
2755
- value: readCertHash('DE:AD:BE:EF:' + CERT_HASH?.substring(12))
2756
- }
2757
- ]
2758
- })
2759
-
2760
- await Promise.all([
2761
- client.closed.catch((err) => err),
2762
- client.ready.catch((err) => err)
2763
- ])
2764
- console.log('test 11 complete')
2765
- await afterEach()
2766
- {
2767
- // client context - connects to the server, opens a bidi stream, sends some data and reads the response
2768
- try {
2769
- console.log('unidirectional mark1')
2770
- client = new WebTransportPolyfill(
2771
- `${SERVER_URL}/unidirectional_client_send`,
2772
- {
2773
- serverCertificateHashes: [
2774
- {
2775
- algorithm: 'sha-256',
2776
- value: readCertHash(CERT_HASH)
2777
- }
2778
- ]
2779
- }
2780
- )
2781
- console.log('unidirectional mark2')
2782
- await client.ready
2783
- console.log('unidirectional mark3')
2784
- } catch (error) {
2785
- console.log('Peak unidirectional error:', error)
2786
- throw error
2787
- }
2788
-
2789
- const stream = await client.createUnidirectionalStream()
2790
- await writeStream(stream, KNOWN_BYTES)
2791
-
2792
- // the remote will close the session
2793
- await client.closed
2794
- }
2795
- console.log('test 12 complete')
2796
- await afterEach()
2797
- {
2798
- client = new WebTransportPolyfill(
2799
- `${SERVER_URL}/unidirectional_server_send`,
2800
- {
2801
- serverCertificateHashes: [
2802
- {
2803
- algorithm: 'sha-256',
2804
- value: readCertHash(CERT_HASH)
2805
- }
2806
- ]
2807
- }
2808
- )
2809
- await client.ready
2810
-
2811
- const stream = await getReaderValue(client.incomingUnidirectionalStreams)
2812
- await readStream(stream, KNOWN_BYTES.length)
2813
- }
2814
- console.log('test 13 complete')
2815
- await afterEach()
2816
- {
2817
- client = new WebTransportPolyfill(
2818
- `${SERVER_URL}/unidirectional_server_delay_before_read`,
2819
- {
2820
- serverCertificateHashes: [
2821
- {
2822
- algorithm: 'sha-256',
2823
- value: readCertHash(CERT_HASH)
2824
- }
2825
- ]
2826
- }
2827
- )
2828
- await client.ready
2829
-
2830
- const clientStream = await client.createUnidirectionalStream()
2831
-
2832
- const writer = clientStream.getWriter()
2833
-
2834
- for (const buf of KNOWN_BYTES) {
2835
- await writer.ready
2836
- await writer.write(buf)
2837
- }
2838
-
2839
- await new Promise((resolve) => setTimeout(resolve, 2000))
2840
-
2841
- await writer.ready
2842
- await writer.close()
2843
-
2844
- // the remote will close the session cleanly if everything was ok
2845
- await client.closed
2846
- }
2847
- console.log('test 14 complete')
2848
- await afterEach()
2849
- }
2850
-
2851
- main().catch((error) => {
2852
- console.log('main error: ', error.toString())
2853
- console.dir(error)
2854
- })