@fails-components/webtransport 0.0.8 → 0.1.0-macarmbuild.5
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/CMakeLists.txt +109 -75
- package/build.js +32 -8
- package/lib/client.js +163 -0
- package/lib/dom.ts +95 -0
- package/lib/event-loop.js +79 -0
- package/lib/index.js +32 -0
- package/lib/native.js +25 -0
- package/lib/server.js +164 -0
- package/lib/session.js +422 -0
- package/lib/stream.js +325 -0
- package/lib/transport.js +42 -0
- package/lib/types.ts +173 -0
- package/lib/utils.js +23 -0
- package/lib/webtransport.js +114 -0
- package/package.json +15 -6
- package/readme.md +28 -5
- package/src/http3client.h +0 -1
- package/src/http3eventloop.cc +52 -0
- package/src/http3eventloop.h +22 -1
- package/src/http3server.cc +4 -1
- package/src/http3server.h +8 -0
- package/src/http3serversession.h +1 -1
- package/src/http3wtsessionvisitor.h +1 -1
- package/test/bidirectional-streams.spec.js +137 -0
- package/test/datagrams.spec.js +131 -0
- package/test/echoclient.js +10 -6
- package/test/echoserver.js +6 -8
- package/test/event-loop.specoff.js +23 -0
- package/test/{certificate.js → fixtures/certificate.js} +24 -8
- package/test/fixtures/read-stream.js +36 -0
- package/test/fixtures/reader-value.js +25 -0
- package/test/fixtures/server.js +58 -0
- package/test/fixtures/write-stream.js +18 -0
- package/test/suite.specoff.js +9 -0
- package/test/test.js +29 -3
- package/test/testsuite.js +22 -0
- package/test/unidirectional-streams.spec.js +127 -0
- package/tsconfig.json +40 -0
- package/src/webtransport.js +0 -870
package/lib/stream.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { ReadableStream, WritableStream } from 'node:stream/web'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebTransport stream events
|
|
5
|
+
* @typedef {import('./types').WebTransportStreamEventHandler} WebTransportStreamEventHandler
|
|
6
|
+
* @typedef {import('./types').StreamRecvSignalEvent} StreamRecvSignalEvent
|
|
7
|
+
* @typedef {import('./types').StreamReadEvent} StreamReadEvent
|
|
8
|
+
* @typedef {import('./types').StreamWriteEvent} StreamWriteEvent
|
|
9
|
+
* @typedef {import('./types').StreamResetEvent} StreamResetEvent
|
|
10
|
+
* @typedef {import('./types').StreamNetworkFinishEvent} StreamNetworkFinishEvent
|
|
11
|
+
*
|
|
12
|
+
* @typedef {import('./types').NativeHttp3WTStream} NativeHttp3WTStream
|
|
13
|
+
*
|
|
14
|
+
* @typedef {import('./session').Http3WTSession} Http3WTSession
|
|
15
|
+
*
|
|
16
|
+
* @typedef {import('stream/web').WritableStreamDefaultController} WritableStreamDefaultController
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export class Http3WTStream {
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} args
|
|
22
|
+
* @param {NativeHttp3WTStream} args.object
|
|
23
|
+
* @param {Http3WTSession} args.parentobj
|
|
24
|
+
* @param {object} args.transport
|
|
25
|
+
* @param {boolean} args.bidirectional
|
|
26
|
+
* @param {boolean} args.incoming
|
|
27
|
+
*/
|
|
28
|
+
constructor(args) {
|
|
29
|
+
this.objint = args.object
|
|
30
|
+
this.objint.jsobj = this
|
|
31
|
+
this.parentobj = args.parentobj
|
|
32
|
+
this.transport = args.transport
|
|
33
|
+
this.bidirectional = args.bidirectional
|
|
34
|
+
this.incoming = args.incoming
|
|
35
|
+
this.closed = false
|
|
36
|
+
|
|
37
|
+
/** @type {Promise<void> | null} */
|
|
38
|
+
this.pendingoperation = null
|
|
39
|
+
this.pendingres = null
|
|
40
|
+
|
|
41
|
+
if (this.bidirectional || this.incoming) {
|
|
42
|
+
this.readable = new ReadableStream(
|
|
43
|
+
{
|
|
44
|
+
start: (controller) => {
|
|
45
|
+
this.readableController = controller
|
|
46
|
+
// @ts-expect-error this.readable could be undefined
|
|
47
|
+
this.parentobj.addReceiveStream(this.readable, controller)
|
|
48
|
+
this.objint.startReading()
|
|
49
|
+
},
|
|
50
|
+
pull: (controller) => {
|
|
51
|
+
if (this.readableclosed) {
|
|
52
|
+
return Promise.resolve()
|
|
53
|
+
}
|
|
54
|
+
this.objint.startReading()
|
|
55
|
+
},
|
|
56
|
+
cancel: (reason) => {
|
|
57
|
+
/** @type {Promise<void>} */
|
|
58
|
+
const promise = new Promise((resolve, reject) => {
|
|
59
|
+
this.cancelres = resolve
|
|
60
|
+
})
|
|
61
|
+
let code = 0
|
|
62
|
+
if (reason && reason.code) {
|
|
63
|
+
if (reason.code < 0) code = 0
|
|
64
|
+
else if (reason.code > 255) code = 255
|
|
65
|
+
else code = reason.code
|
|
66
|
+
}
|
|
67
|
+
this.readableclosed = true
|
|
68
|
+
this.objint.stopSending(code)
|
|
69
|
+
return promise
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{ highWaterMark: 4 }
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
if (this.bidirectional || !this.incoming) {
|
|
76
|
+
this.writable = new WritableStream(
|
|
77
|
+
{
|
|
78
|
+
start: (controller) => {
|
|
79
|
+
this.writableController = controller
|
|
80
|
+
// @ts-expect-error this.writable could be undefined
|
|
81
|
+
this.parentobj.addSendStream(this.writable, controller)
|
|
82
|
+
},
|
|
83
|
+
write: (chunk, controller) => {
|
|
84
|
+
if (this.writableclosed) {
|
|
85
|
+
return Promise.resolve()
|
|
86
|
+
}
|
|
87
|
+
if (chunk instanceof Uint8Array) {
|
|
88
|
+
this.pendingoperation = new Promise((resolve, reject) => {
|
|
89
|
+
this.pendingres = resolve
|
|
90
|
+
})
|
|
91
|
+
const dataprom = this.parentobj.waitForDatagramsSend()
|
|
92
|
+
dataprom.finally(() => {
|
|
93
|
+
this.objint.writeChunk(chunk)
|
|
94
|
+
})
|
|
95
|
+
return this.pendingoperation
|
|
96
|
+
} else {
|
|
97
|
+
console.log('chunk info:', chunk)
|
|
98
|
+
throw new Error('chunk is not of instanceof Uint8Array ')
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
close: () => {
|
|
102
|
+
if (this.writableclosed) {
|
|
103
|
+
return Promise.resolve()
|
|
104
|
+
}
|
|
105
|
+
this.objint.streamFinal()
|
|
106
|
+
this.pendingoperation = new Promise((resolve, reject) => {
|
|
107
|
+
this.pendingres = resolve
|
|
108
|
+
})
|
|
109
|
+
return this.pendingoperation
|
|
110
|
+
},
|
|
111
|
+
abort: (reason) => {
|
|
112
|
+
if (this.writableclosed) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
resolve()
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
let code = 0
|
|
118
|
+
if (reason && reason.code) {
|
|
119
|
+
if (reason.code < 0) code = 0
|
|
120
|
+
else if (reason.code > 255) code = 255
|
|
121
|
+
else code = reason.code
|
|
122
|
+
}
|
|
123
|
+
/** @type {Promise<void>} */
|
|
124
|
+
const promise = new Promise((resolve, reject) => {
|
|
125
|
+
this.abortres = resolve
|
|
126
|
+
})
|
|
127
|
+
this.objint.resetStream(code)
|
|
128
|
+
return promise
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{ highWaterMark: 4 }
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @type {(() => void) | null} */
|
|
136
|
+
this.cancelres = null
|
|
137
|
+
/** @type {(() => void) | null} */
|
|
138
|
+
this.pendingres = null
|
|
139
|
+
/** @type {(() => void) | null} */
|
|
140
|
+
this.abortres = null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {import('./types').StreamRecvSignalEvent} args
|
|
145
|
+
* @returns {void}
|
|
146
|
+
*/
|
|
147
|
+
onStreamRecvSignal(args) {
|
|
148
|
+
// console.log('onStreamRecvSignal', args)
|
|
149
|
+
// check if transport is closed
|
|
150
|
+
const parentstate = this.parentobj.state
|
|
151
|
+
if (parentstate === 'closed' || parentstate === 'failed') return
|
|
152
|
+
switch (args.nettask) {
|
|
153
|
+
case 'resetStream':
|
|
154
|
+
if (this.readable) {
|
|
155
|
+
this.parentobj.removeReceiveStream(
|
|
156
|
+
this.readable,
|
|
157
|
+
this.readableController
|
|
158
|
+
)
|
|
159
|
+
this.readableclosed = true
|
|
160
|
+
this.readableController.error(args.code || 0)
|
|
161
|
+
} else console.log('stopSending wihtout readable')
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
case 'stopSending':
|
|
165
|
+
if (this.writable) {
|
|
166
|
+
this.parentobj.removeSendStream(
|
|
167
|
+
this.writable,
|
|
168
|
+
this.writableController
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
this.writableclosed = true
|
|
172
|
+
this.writableController.error(args.code || 0)
|
|
173
|
+
} else console.log('stopSending wihtout writable')
|
|
174
|
+
break
|
|
175
|
+
default:
|
|
176
|
+
console.log('unhandled onStreamRecvSignal')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this.pendingoperation) {
|
|
180
|
+
const res = this.pendingres
|
|
181
|
+
this.pendingoperation = null
|
|
182
|
+
this.pendingres = null
|
|
183
|
+
if (res != null) {
|
|
184
|
+
res()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {StreamReadEvent} args
|
|
191
|
+
* @returns {void}
|
|
192
|
+
*/
|
|
193
|
+
onStreamRead(args) {
|
|
194
|
+
if (args.data && args.data.length && !this.readableclosed) {
|
|
195
|
+
// console.log('stream read received', args.data, Date.now())
|
|
196
|
+
this.readableController.enqueue(args.data)
|
|
197
|
+
if (
|
|
198
|
+
this.readableController.desiredSize != null &&
|
|
199
|
+
this.readableController.desiredSize < 0
|
|
200
|
+
)
|
|
201
|
+
this.objint.stopReading()
|
|
202
|
+
}
|
|
203
|
+
if (args.fin) {
|
|
204
|
+
if (this.cancelres) {
|
|
205
|
+
const res = this.cancelres
|
|
206
|
+
this.cancelres = null
|
|
207
|
+
res()
|
|
208
|
+
}
|
|
209
|
+
if (!this.readableclosed) {
|
|
210
|
+
this.readableController.close()
|
|
211
|
+
this.readableclosed = true
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {StreamWriteEvent} args
|
|
218
|
+
*/
|
|
219
|
+
onStreamWrite(args) {
|
|
220
|
+
// we ignore success
|
|
221
|
+
if (this.pendingoperation) {
|
|
222
|
+
const res = this.pendingres
|
|
223
|
+
this.pendingoperation = null
|
|
224
|
+
this.pendingres = null
|
|
225
|
+
if (res != null) {
|
|
226
|
+
res()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {StreamResetEvent} args
|
|
233
|
+
*/
|
|
234
|
+
onStreamReset(args) {
|
|
235
|
+
if (this.abortres) {
|
|
236
|
+
this.abortres()
|
|
237
|
+
if (this.readable)
|
|
238
|
+
this.parentobj.removeReceiveStream(
|
|
239
|
+
this.readable,
|
|
240
|
+
this.readableController
|
|
241
|
+
)
|
|
242
|
+
if (this.writable)
|
|
243
|
+
this.parentobj.removeSendStream(this.writable, this.writableController)
|
|
244
|
+
this.readableclosed = true
|
|
245
|
+
this.parentobj.removeStreamObj(this)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {StreamNetworkFinishEvent} args
|
|
251
|
+
*/
|
|
252
|
+
onStreamNetworkFinish(args) {
|
|
253
|
+
// console.log('networkfinish args', args)
|
|
254
|
+
switch (args.nettask) {
|
|
255
|
+
case 'stopSending':
|
|
256
|
+
if (this.cancelres) {
|
|
257
|
+
const res = this.cancelres
|
|
258
|
+
this.cancelres = null
|
|
259
|
+
res()
|
|
260
|
+
}
|
|
261
|
+
this.stopSendingRecv = true
|
|
262
|
+
break
|
|
263
|
+
case 'resetStream':
|
|
264
|
+
if (this.abortres) {
|
|
265
|
+
const res = this.abortres
|
|
266
|
+
this.abortres = null
|
|
267
|
+
res()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
case 'streamFinal':
|
|
273
|
+
if (this.pendingoperation) {
|
|
274
|
+
const res = this.pendingres
|
|
275
|
+
this.pendingoperation = null
|
|
276
|
+
this.pendingres = null
|
|
277
|
+
if (res != null) {
|
|
278
|
+
res()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
break
|
|
282
|
+
default:
|
|
283
|
+
console.log('onStreamNetworkFinish unknown task')
|
|
284
|
+
}
|
|
285
|
+
// we could differentiate....
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {StreamRecvSignalEvent | StreamReadEvent | StreamWriteEvent | StreamResetEvent | StreamNetworkFinishEvent} args
|
|
290
|
+
*/
|
|
291
|
+
static callback(args) {
|
|
292
|
+
// console.log('Stream callback called', args)
|
|
293
|
+
if (!args || !args.object || !args.object.jsobj)
|
|
294
|
+
throw new Error('Stream callback without jsobj')
|
|
295
|
+
const visitor = args.object.jsobj
|
|
296
|
+
if (args.purpose) {
|
|
297
|
+
switch (args.purpose) {
|
|
298
|
+
case 'StreamRecvSignal':
|
|
299
|
+
visitor.onStreamRecvSignal(args)
|
|
300
|
+
break
|
|
301
|
+
case 'StreamRead':
|
|
302
|
+
if (visitor && Object.prototype.hasOwnProperty.call(args, 'data')) {
|
|
303
|
+
visitor.onStreamRead(args)
|
|
304
|
+
} else {
|
|
305
|
+
console.log('Stream callback called', visitor, args)
|
|
306
|
+
throw new Error('Malformed StreamRead')
|
|
307
|
+
}
|
|
308
|
+
break
|
|
309
|
+
case 'StreamWrite':
|
|
310
|
+
visitor.onStreamWrite(args)
|
|
311
|
+
break
|
|
312
|
+
case 'StreamReset':
|
|
313
|
+
visitor.onStreamReset(args)
|
|
314
|
+
break
|
|
315
|
+
case 'StreamNetworkFinish':
|
|
316
|
+
visitor.onStreamNetworkFinish(args)
|
|
317
|
+
|
|
318
|
+
break
|
|
319
|
+
default: {
|
|
320
|
+
throw new Error('unknown purpose Streamcb')
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} else throw new Error('no purpose Streamcb')
|
|
324
|
+
}
|
|
325
|
+
}
|
package/lib/transport.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Http3EventLoop } from './event-loop.js'
|
|
2
|
+
import { wtrouter } from './native.js'
|
|
3
|
+
|
|
4
|
+
export class Http3WebTransport {
|
|
5
|
+
/**
|
|
6
|
+
* @param {*} args
|
|
7
|
+
* @param {'server' | 'client'} purpose
|
|
8
|
+
*/
|
|
9
|
+
constructor(args, purpose) {
|
|
10
|
+
const eventloop = Http3EventLoop.getGlobalEventLoop(this).eventloopInt
|
|
11
|
+
|
|
12
|
+
if (purpose === 'server')
|
|
13
|
+
this.transportInt = new wtrouter.Http3WebTransportServer(args, eventloop)
|
|
14
|
+
else if (purpose === 'client')
|
|
15
|
+
this.transportInt = new wtrouter.Http3WebTransportClient(args, eventloop)
|
|
16
|
+
else throw new Error('unknown purpose')
|
|
17
|
+
this.transportInt.jsobj = this
|
|
18
|
+
|
|
19
|
+
this.sessions = {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} TransportCallbackEvent
|
|
24
|
+
* @property {{ jsobj: { customCallback: (args: any) => void }}} object
|
|
25
|
+
* @property {string} purpose
|
|
26
|
+
*
|
|
27
|
+
* @param {TransportCallbackEvent} args
|
|
28
|
+
*/
|
|
29
|
+
static transportCallback(args) {
|
|
30
|
+
// console.log('incoming callback transport', args)
|
|
31
|
+
if (!args || !args.object || !args.object.jsobj)
|
|
32
|
+
throw new Error('Transport callback without jsobj')
|
|
33
|
+
const visitor = args.object.jsobj
|
|
34
|
+
if (args.purpose) {
|
|
35
|
+
if (visitor.customCallback) {
|
|
36
|
+
visitor.customCallback(args)
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error('unknown purpose')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { WebTransport } from './dom'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native Http3WTSession counterpart
|
|
5
|
+
*/
|
|
6
|
+
export interface NativeHttp3WTSession {
|
|
7
|
+
jsobj: WebTransportSessionEventHandler
|
|
8
|
+
writeDatagram: (chunk: Uint8Array) => void
|
|
9
|
+
orderUnidiStream: () => void
|
|
10
|
+
orderBidiStream: () => void
|
|
11
|
+
close: (arg: { code: number, reason: string }) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Native Http3WTStream counterpart
|
|
16
|
+
*/
|
|
17
|
+
export interface NativeHttp3WTStream {
|
|
18
|
+
jsobj: WebTransportStreamEventHandler
|
|
19
|
+
startReading: () => void
|
|
20
|
+
stopReading: () => void
|
|
21
|
+
stopSending: (code: number) => void
|
|
22
|
+
resetStream: (code: number) => void
|
|
23
|
+
writeChunk: (buf: Uint8Array) => void
|
|
24
|
+
streamFinal: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Purpose = 'StreamRecvSignal' | 'StreamRead' | 'StreamWrite' | 'StreamReset' | 'StreamNetworkFinish'
|
|
28
|
+
export type NetTask = 'stopSending' | 'resetStream' | 'streamFinal'
|
|
29
|
+
|
|
30
|
+
export interface StreamRecvSignalEvent {
|
|
31
|
+
object: NativeHttp3WTStream
|
|
32
|
+
purpose: 'StreamRecvSignal'
|
|
33
|
+
code: number
|
|
34
|
+
nettask: NetTask
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StreamReadEvent {
|
|
38
|
+
object: NativeHttp3WTStream
|
|
39
|
+
purpose: 'StreamRead'
|
|
40
|
+
data?: Uint8Array
|
|
41
|
+
fin?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface StreamWriteEvent {
|
|
45
|
+
object: NativeHttp3WTStream
|
|
46
|
+
purpose: 'StreamWrite'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StreamResetEvent {
|
|
50
|
+
object: NativeHttp3WTStream
|
|
51
|
+
purpose: 'StreamReset'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface StreamNetworkFinishEvent {
|
|
55
|
+
object: NativeHttp3WTStream
|
|
56
|
+
purpose: 'StreamNetworkFinish'
|
|
57
|
+
nettask: NetTask
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface WebTransportStreamEventHandler {
|
|
61
|
+
onStreamRecvSignal: (evt: StreamRecvSignalEvent) => void
|
|
62
|
+
onStreamRead: (evt: StreamReadEvent) => void
|
|
63
|
+
onStreamWrite: (evt: StreamWriteEvent) => void
|
|
64
|
+
onStreamReset: (evt: StreamResetEvent) => void
|
|
65
|
+
onStreamNetworkFinish: (evt: StreamNetworkFinishEvent) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SessionReadyEvent {
|
|
69
|
+
object: NativeHttp3WTSession
|
|
70
|
+
purpose: 'SessionReady'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SessionCloseEvent {
|
|
74
|
+
object: NativeHttp3WTSession
|
|
75
|
+
purpose: 'SessionClose'
|
|
76
|
+
errorcode: number
|
|
77
|
+
error: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DatagramReceivedEvent {
|
|
81
|
+
object: NativeHttp3WTSession
|
|
82
|
+
purpose: 'DatagramReceived'
|
|
83
|
+
datagram: Uint8Array
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DatagramSendEvent {
|
|
87
|
+
object: NativeHttp3WTSession
|
|
88
|
+
purpose: 'DatagramSend'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface NewStreamEvent {
|
|
92
|
+
object: NativeHttp3WTSession
|
|
93
|
+
purpose: 'Http3WTStreamVisitor'
|
|
94
|
+
stream: NativeHttp3WTStream
|
|
95
|
+
transport: object
|
|
96
|
+
bidirectional: boolean
|
|
97
|
+
incoming: boolean
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface WebTransportSessionEventHandler {
|
|
101
|
+
onReady: (evt: SessionReadyEvent) => void
|
|
102
|
+
onClose: (evt: SessionCloseEvent) => void
|
|
103
|
+
onDatagramReceived: (evt: DatagramReceivedEvent) => void
|
|
104
|
+
onDatagramSend: (evt: DatagramSendEvent) => void
|
|
105
|
+
onStream: (evt: NewStreamEvent) => void
|
|
106
|
+
closeHook?: (() => void) | null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ClientConnectedEvent {
|
|
110
|
+
purpose: 'ClientConnected'
|
|
111
|
+
success: boolean
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ClientWebtransportSupportEvent {
|
|
115
|
+
purpose: 'ClientWebtransportSupport'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface Http3WTSessionVisitorEvent {
|
|
119
|
+
purpose: 'Http3WTSessionVisitor'
|
|
120
|
+
session: NativeHttp3WTSession
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface Http3ClientEventHandler {
|
|
124
|
+
onClientConnected: (evt: ClientConnectedEvent) => void
|
|
125
|
+
onClientWebTransportSupport: (evt: ClientWebtransportSupportEvent) => void
|
|
126
|
+
onHttp3WTSessionVisitor: (evt: Http3WTSessionVisitorEvent) => void
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface Http3WTServerSessionVisitorEvent extends Http3WTSessionVisitorEvent {
|
|
130
|
+
path: string
|
|
131
|
+
object: any
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The Http3 server is listening on the specified port
|
|
136
|
+
*/
|
|
137
|
+
export interface Http3ServerListeningEvent {
|
|
138
|
+
purpose: 'Http3ServerListening'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The Http3 server has stopped listening on the specified port
|
|
143
|
+
*/
|
|
144
|
+
export interface ServerStatusEvent {
|
|
145
|
+
port: number | null
|
|
146
|
+
host: string | null
|
|
147
|
+
purpose: 'ServerStatus',
|
|
148
|
+
status: 'error' | 'listening' | 'close'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
export interface Http3ServerEventHandler {
|
|
153
|
+
onHttp3WTSessionVisitor: (evt: Http3WTServerSessionVisitorEvent) => void
|
|
154
|
+
onServerError: () => void
|
|
155
|
+
onServerListening: () => void
|
|
156
|
+
onServerClose: () => void
|
|
157
|
+
onServerStatus: (evt: ServerStatusEvent) => void
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A defered promise with the value T
|
|
162
|
+
*/
|
|
163
|
+
export interface Deferred<T = unknown> {
|
|
164
|
+
promise: Promise<T>
|
|
165
|
+
resolve: (value?: T) => void
|
|
166
|
+
reject: (reason?: any) => void
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type WebTransportSessionState = 'connected' | 'closed' | 'failed'
|
|
170
|
+
|
|
171
|
+
export interface WebTransportSession extends WebTransport {
|
|
172
|
+
state: WebTransportSessionState
|
|
173
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @template {unknown} T
|
|
4
|
+
* @returns {import('./types').Deferred<T>}
|
|
5
|
+
*/
|
|
6
|
+
export function defer () {
|
|
7
|
+
/** @type {(value?: T) => void} */
|
|
8
|
+
let res = () => {}
|
|
9
|
+
|
|
10
|
+
/** @type {(reason?: Error) => void} */
|
|
11
|
+
let rej = () => {}
|
|
12
|
+
|
|
13
|
+
const promise = new Promise((resolve, reject) => {
|
|
14
|
+
res = resolve
|
|
15
|
+
rej = reject
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
promise,
|
|
20
|
+
resolve: res,
|
|
21
|
+
reject: rej
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Http3Client } from './client.js'
|
|
2
|
+
import { Http3WTSession } from './session.js'
|
|
3
|
+
|
|
4
|
+
/** @type {WeakMap<WebTransport, Http3WTSession>} */
|
|
5
|
+
const sessions = new WeakMap()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('./dom').WebTransportCloseInfo} WebTransportCloseInfo
|
|
9
|
+
* @typedef {import('./dom').WebTransportBidirectionalStream} WebTransportBidirectionalStream
|
|
10
|
+
* @typedef {import('./dom').WebTransportReceiveStream} WebTransportReceiveStream
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import('./dom').WebTransport} WebTransportInterface
|
|
15
|
+
*
|
|
16
|
+
* @implements {WebTransportInterface}
|
|
17
|
+
*/
|
|
18
|
+
export class WebTransport {
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} url
|
|
21
|
+
* @param {import('./dom').WebTransportOptions} [args]
|
|
22
|
+
*/
|
|
23
|
+
constructor(url, args) {
|
|
24
|
+
if (!url) throw new Error('no URL supplied')
|
|
25
|
+
|
|
26
|
+
const ourl = new URL(url)
|
|
27
|
+
|
|
28
|
+
if (ourl.protocol !== 'https:') {
|
|
29
|
+
throw new Error('URL is not supported for webtransport')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hostname = ourl.hostname
|
|
33
|
+
let port = ourl.port
|
|
34
|
+
if (port === '') port = '443'
|
|
35
|
+
|
|
36
|
+
const client = new Http3Client({ hostname, port, ...args })
|
|
37
|
+
|
|
38
|
+
const sessionint = new Http3WTSession({
|
|
39
|
+
/* object: args.session, */
|
|
40
|
+
parentobj: client
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
this.ready = sessionint.ready
|
|
44
|
+
this.closed = sessionint.closed
|
|
45
|
+
|
|
46
|
+
this.datagrams = sessionint.datagrams
|
|
47
|
+
|
|
48
|
+
/** @type {ReadableStream<WebTransportBidirectionalStream>} */
|
|
49
|
+
this.incomingBidirectionalStreams = sessionint.incomingBidirectionalStreams
|
|
50
|
+
|
|
51
|
+
/** @type {ReadableStream<WebTransportReceiveStream>} */
|
|
52
|
+
this.incomingUnidirectionalStreams =
|
|
53
|
+
sessionint.incomingUnidirectionalStreams
|
|
54
|
+
|
|
55
|
+
sessions.set(this, sessionint)
|
|
56
|
+
|
|
57
|
+
client
|
|
58
|
+
.handleConnection()
|
|
59
|
+
.then(() => {
|
|
60
|
+
client.quicconnected
|
|
61
|
+
?.then(() => {
|
|
62
|
+
return client.createWTSession(sessionint, ourl.pathname)
|
|
63
|
+
})
|
|
64
|
+
.catch((err) => {
|
|
65
|
+
sessionint.readyReject(
|
|
66
|
+
new Error('Establishing session failed ' + err)
|
|
67
|
+
)
|
|
68
|
+
console.log('Establishing session failed ' + err)
|
|
69
|
+
client.closeHookSession()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
.catch((error) => {
|
|
73
|
+
sessionint.readyReject(new Error('Quic connection failed ' + error))
|
|
74
|
+
console.log('Quic connection failed ' + error)
|
|
75
|
+
client.closeHookSession()
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {WebTransportCloseInfo} [closeinfo]
|
|
81
|
+
*/
|
|
82
|
+
close(closeinfo) {
|
|
83
|
+
const session = sessions.get(this)
|
|
84
|
+
|
|
85
|
+
if (session == null) {
|
|
86
|
+
// should never happen as session is only removed when this instance is garbage collected
|
|
87
|
+
throw new Error('Http3WTSession was undefined')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return session.close(closeinfo)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
createBidirectionalStream() {
|
|
94
|
+
const session = sessions.get(this)
|
|
95
|
+
|
|
96
|
+
if (session == null) {
|
|
97
|
+
// should never happen as session is only removed when this instance is garbage collected
|
|
98
|
+
throw new Error('Http3WTSession was undefined')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return session.createBidirectionalStream()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
createUnidirectionalStream() {
|
|
105
|
+
const session = sessions.get(this)
|
|
106
|
+
|
|
107
|
+
if (session == null) {
|
|
108
|
+
// should never happen as session is only removed when this instance is garbage collected
|
|
109
|
+
throw new Error('Http3WTSession was undefined')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return session.createUnidirectionalStream()
|
|
113
|
+
}
|
|
114
|
+
}
|