@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/src/webtransport.js
DELETED
|
@@ -1,870 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-prototype-builtins */
|
|
2
|
-
// Copyright (c) 2022 Marten Richter or other contributers (see commit). All rights reserved.
|
|
3
|
-
// Use of this source code is governed by a BSD-style license that can be
|
|
4
|
-
// found in the LICENSE file.
|
|
5
|
-
|
|
6
|
-
import { existsSync } from 'fs'
|
|
7
|
-
import { createRequire } from 'module'
|
|
8
|
-
import { ReadableStream, WritableStream } from 'node:stream/web'
|
|
9
|
-
import * as path from 'path'
|
|
10
|
-
import * as url from 'url'
|
|
11
|
-
import { arch, platform } from 'node:process'
|
|
12
|
-
|
|
13
|
-
const binplatform = platform + '_' + arch
|
|
14
|
-
|
|
15
|
-
const require = createRequire(import.meta.url)
|
|
16
|
-
const dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
|
17
|
-
let buildpath = '../build_' + binplatform
|
|
18
|
-
if (!existsSync(path.join(dirname, buildpath))) buildpath = '../build' // use precompiled only if own compilation does not exis
|
|
19
|
-
|
|
20
|
-
let wtpath = buildpath + '/Release/webtransport.node'
|
|
21
|
-
if (
|
|
22
|
-
process.env.NODE_ENV !== 'production' &&
|
|
23
|
-
existsSync(path.join(dirname, buildpath + '/Debug/webtransport.node'))
|
|
24
|
-
) {
|
|
25
|
-
wtpath = buildpath + '/Debug/webtransport.node'
|
|
26
|
-
}
|
|
27
|
-
console.log('load webtransport binary:', wtpath)
|
|
28
|
-
|
|
29
|
-
const wtrouter = require(wtpath)
|
|
30
|
-
|
|
31
|
-
class Http3WTStream {
|
|
32
|
-
constructor(args) {
|
|
33
|
-
this.objint = args.object
|
|
34
|
-
this.objint.jsobj = this
|
|
35
|
-
this.parentobj = args.parentobj
|
|
36
|
-
this.transport = args.transport
|
|
37
|
-
this.bidirectional = args.bidirectional
|
|
38
|
-
this.incoming = args.incoming
|
|
39
|
-
this.closed = false
|
|
40
|
-
|
|
41
|
-
this.pendingoperation = null
|
|
42
|
-
this.pendingres = null
|
|
43
|
-
|
|
44
|
-
if (this.bidirectional || this.incoming) {
|
|
45
|
-
this.readable = new ReadableStream(
|
|
46
|
-
{
|
|
47
|
-
start: (controller) => {
|
|
48
|
-
this.readableController = controller
|
|
49
|
-
this.parentobj.addReceiveStream(this.readable, controller)
|
|
50
|
-
this.objint.startReading()
|
|
51
|
-
},
|
|
52
|
-
pull: (controller) => {
|
|
53
|
-
if (this.readableclosed) {
|
|
54
|
-
return Promise.resolve()
|
|
55
|
-
}
|
|
56
|
-
this.objint.startReading()
|
|
57
|
-
},
|
|
58
|
-
cancel: (reason) => {
|
|
59
|
-
const promise = new Promise((resolve, reject) => {
|
|
60
|
-
this.cancelres = resolve
|
|
61
|
-
})
|
|
62
|
-
let code = 0
|
|
63
|
-
if (reason && reason.code) {
|
|
64
|
-
if (reason.code < 0) code = 0
|
|
65
|
-
else if (reason.code > 255) code = 255
|
|
66
|
-
else code = reason.code
|
|
67
|
-
}
|
|
68
|
-
this.readableclosed = true
|
|
69
|
-
this.objint.stopSending(code)
|
|
70
|
-
return promise
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
{ highWaterMark: 4 }
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
if (this.bidirectional || !this.incoming) {
|
|
77
|
-
this.writable = new WritableStream(
|
|
78
|
-
{
|
|
79
|
-
start: (controller) => {
|
|
80
|
-
this.writableController = controller
|
|
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: (controller) => {
|
|
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
|
-
const promise = new Promise((resolve, reject) => {
|
|
124
|
-
this.abortres = resolve
|
|
125
|
-
})
|
|
126
|
-
this.objint.resetStream(code)
|
|
127
|
-
return promise
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
{ highWaterMark: 4 }
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
onStreamRecvSignal(args) {
|
|
136
|
-
// console.log('onStreamRecvSignal', args)
|
|
137
|
-
// check if transport is closed
|
|
138
|
-
const parentstate = this.parentobj.state
|
|
139
|
-
if (parentstate === 'closed' || parentstate === 'failed') return
|
|
140
|
-
switch (args.nettask) {
|
|
141
|
-
case 'resetStream':
|
|
142
|
-
if (this.readable) {
|
|
143
|
-
this.parentobj.removeReceiveStream(
|
|
144
|
-
this.readable,
|
|
145
|
-
this.readableController
|
|
146
|
-
)
|
|
147
|
-
this.readableclosed = true
|
|
148
|
-
this.readableController.error(args.code || 0)
|
|
149
|
-
} else console.log('stopSending wihtout readable')
|
|
150
|
-
break
|
|
151
|
-
|
|
152
|
-
case 'stopSending':
|
|
153
|
-
if (this.writable) {
|
|
154
|
-
this.parentobj.removeSendStream(
|
|
155
|
-
this.writable,
|
|
156
|
-
this.writableController
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
this.writableclosed = true
|
|
160
|
-
this.writableController.error(args.code || 0)
|
|
161
|
-
} else console.log('stopSending wihtout writable')
|
|
162
|
-
break
|
|
163
|
-
default:
|
|
164
|
-
console.log('unhandled onStreamRecvSignal')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (this.pendingoperation) {
|
|
168
|
-
const res = this.pendingres
|
|
169
|
-
this.pendingoperation = null
|
|
170
|
-
this.pendingres = null
|
|
171
|
-
res()
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
onStreamRead(args) {
|
|
176
|
-
if (args.data && args.data.length && !this.readableclosed) {
|
|
177
|
-
// console.log('stream read received', args.data, Date.now())
|
|
178
|
-
this.readableController.enqueue(args.data)
|
|
179
|
-
if (this.readableController.desiredSize < 0) this.objint.stopReading()
|
|
180
|
-
}
|
|
181
|
-
if (args.fin) {
|
|
182
|
-
if (this.cancelres) {
|
|
183
|
-
const res = this.cancelres
|
|
184
|
-
this.cancelres = null
|
|
185
|
-
res()
|
|
186
|
-
}
|
|
187
|
-
if (!this.readableclosed) {
|
|
188
|
-
this.readableController.close()
|
|
189
|
-
this.readableclosed = true
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
onStreamWrite(args) {
|
|
195
|
-
// we ignore success
|
|
196
|
-
if (this.pendingoperation) {
|
|
197
|
-
const res = this.pendingres
|
|
198
|
-
this.pendingoperation = null
|
|
199
|
-
this.pendingres = null
|
|
200
|
-
res()
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
onStreamReset(args) {
|
|
205
|
-
if (this.abortres) {
|
|
206
|
-
this.abortres()
|
|
207
|
-
if (this.readable)
|
|
208
|
-
this.parentobj.removeReceiveStream(
|
|
209
|
-
this.readable,
|
|
210
|
-
this.readableController
|
|
211
|
-
)
|
|
212
|
-
if (this.writable)
|
|
213
|
-
this.parentobj.removeSendStream(this.writable, this.writableController)
|
|
214
|
-
this.readableclosed = true
|
|
215
|
-
this.parentobj.removeStreamObj(this)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
onStreamNetworkFinish(args) {
|
|
220
|
-
// console.log('networkfinish args', args)
|
|
221
|
-
switch (args.nettask) {
|
|
222
|
-
case 'stopSending':
|
|
223
|
-
if (this.cancelres) {
|
|
224
|
-
const res = this.cancelres
|
|
225
|
-
this.cancelres = null
|
|
226
|
-
res()
|
|
227
|
-
}
|
|
228
|
-
this.stopSendingRecv = true
|
|
229
|
-
break
|
|
230
|
-
case 'resetStream':
|
|
231
|
-
if (this.abortres) {
|
|
232
|
-
const res = this.abortres
|
|
233
|
-
this.abortres = null
|
|
234
|
-
res()
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
break
|
|
238
|
-
|
|
239
|
-
case 'streamFinal':
|
|
240
|
-
if (this.pendingoperation) {
|
|
241
|
-
const res = this.pendingres
|
|
242
|
-
this.pendingoperation = null
|
|
243
|
-
this.pendingres = null
|
|
244
|
-
res()
|
|
245
|
-
}
|
|
246
|
-
break
|
|
247
|
-
default:
|
|
248
|
-
console.log('onStreamNetworkFinish unknown task')
|
|
249
|
-
}
|
|
250
|
-
// we could differentiate....
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
static callback(args) {
|
|
254
|
-
// console.log('Stream callback called', args)
|
|
255
|
-
if (!args || !args.object || !args.object.jsobj)
|
|
256
|
-
throw new Error('Stream callback without jsobj')
|
|
257
|
-
const visitor = args.object.jsobj
|
|
258
|
-
if (args.purpose) {
|
|
259
|
-
switch (args.purpose) {
|
|
260
|
-
case 'StreamRecvSignal':
|
|
261
|
-
visitor.onStreamRecvSignal(args)
|
|
262
|
-
break
|
|
263
|
-
case 'StreamRead':
|
|
264
|
-
if (visitor && args.hasOwnProperty('data')) {
|
|
265
|
-
visitor.onStreamRead(args)
|
|
266
|
-
} else {
|
|
267
|
-
console.log('Stream callback called', visitor, args)
|
|
268
|
-
throw new Error('Malformed StreamRead')
|
|
269
|
-
}
|
|
270
|
-
break
|
|
271
|
-
case 'StreamWrite':
|
|
272
|
-
visitor.onStreamWrite(args)
|
|
273
|
-
break
|
|
274
|
-
case 'StreamReset':
|
|
275
|
-
visitor.onStreamReset(args)
|
|
276
|
-
break
|
|
277
|
-
case 'StreamNetworkFinish':
|
|
278
|
-
visitor.onStreamNetworkFinish(args)
|
|
279
|
-
|
|
280
|
-
break
|
|
281
|
-
default: {
|
|
282
|
-
throw new Error('unknown purpose Streamcb')
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
} else throw new Error('no purpose Streamcb')
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
class Http3WTSession {
|
|
290
|
-
constructor(args) {
|
|
291
|
-
if (args.object) {
|
|
292
|
-
this.objint = args.object
|
|
293
|
-
this.objint.jsobj = this
|
|
294
|
-
}
|
|
295
|
-
this.parentobj = args.parentobj
|
|
296
|
-
this.state = 'connected'
|
|
297
|
-
|
|
298
|
-
this.ready = new Promise((resolve, reject) => {
|
|
299
|
-
this.readyResolve = resolve
|
|
300
|
-
this.readyReject = reject
|
|
301
|
-
}).catch(() => {}) // add default handler if no one cares
|
|
302
|
-
this.closed = new Promise((resolve, reject) => {
|
|
303
|
-
this.closedResolve = resolve
|
|
304
|
-
this.closedReject = reject
|
|
305
|
-
}).catch(() => {}) // add default handler if no one cares
|
|
306
|
-
|
|
307
|
-
this.incomingBidirectionalStreams = new ReadableStream({
|
|
308
|
-
start: (controller) => {
|
|
309
|
-
this.incomBiDiController = controller
|
|
310
|
-
}
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
this.incomingUnidirectionalStreams = new ReadableStream({
|
|
314
|
-
start: (controller) => {
|
|
315
|
-
this.incomUniDiController = controller
|
|
316
|
-
}
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
this.datagrams = {}
|
|
320
|
-
this.datagrams.readable = new ReadableStream({
|
|
321
|
-
start: (controller) => {
|
|
322
|
-
this.incomDatagramController = controller
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
this.writeDatagramRes = []
|
|
326
|
-
this.writeDatagramRej = []
|
|
327
|
-
this.writeDatagramProm = []
|
|
328
|
-
this.datagrams.writable = new WritableStream({
|
|
329
|
-
start: (controller) => {
|
|
330
|
-
this.outgoDatagramController = controller
|
|
331
|
-
},
|
|
332
|
-
write: (chunk, controller) => {
|
|
333
|
-
if (this.state === 'closed') throw new Error('Session is closed')
|
|
334
|
-
if (chunk instanceof Uint8Array) {
|
|
335
|
-
const ret = new Promise((resolve, reject) => {
|
|
336
|
-
this.writeDatagramRes.push(resolve)
|
|
337
|
-
this.writeDatagramRej.push(reject)
|
|
338
|
-
})
|
|
339
|
-
this.writeDatagramProm.push(ret)
|
|
340
|
-
// console.log('b4 datagram write', chunk, Date.now())
|
|
341
|
-
this.objint.writeDatagram(chunk)
|
|
342
|
-
return ret
|
|
343
|
-
} else throw new Error('chunk is not of type Uint8Array')
|
|
344
|
-
},
|
|
345
|
-
close: (controller) => {
|
|
346
|
-
// do nothing
|
|
347
|
-
}
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
this.resolveBiDi = []
|
|
351
|
-
this.resolveUniDi = []
|
|
352
|
-
this.rejectBiDi = []
|
|
353
|
-
this.rejectUniDi = []
|
|
354
|
-
|
|
355
|
-
this.sendStreams = new Set()
|
|
356
|
-
this.receiveStreams = new Set()
|
|
357
|
-
this.streamObjs = new Set()
|
|
358
|
-
|
|
359
|
-
this.sendStreamsController = new Set()
|
|
360
|
-
this.receiveStreamsController = new Set()
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
setSessionObj(object) {
|
|
364
|
-
if (object) {
|
|
365
|
-
this.objint = object
|
|
366
|
-
this.objint.jsobj = this
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
async waitForDatagramsSend() {
|
|
371
|
-
while (this.writeDatagramProm.length > 0) {
|
|
372
|
-
try {
|
|
373
|
-
await Promise.allSettled(this.writeDatagramProm)
|
|
374
|
-
} catch (error) {
|
|
375
|
-
console.log('datagram promise failed ', error)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
addStreamObj(stream) {
|
|
381
|
-
this.streamObjs.add(stream)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
removeStreamObj(stream) {
|
|
385
|
-
this.streamObjs.delete(stream)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
addSendStream(stream, controller) {
|
|
389
|
-
this.sendStreams.add(stream)
|
|
390
|
-
this.sendStreamsController.add(controller)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
removeSendStream(stream, controller) {
|
|
394
|
-
this.sendStreams.delete(stream)
|
|
395
|
-
this.sendStreamsController.delete(controller)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
addReceiveStream(stream, controller) {
|
|
399
|
-
this.receiveStreams.add(stream)
|
|
400
|
-
this.receiveStreamsController.add(controller)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
removeReceiveStream(stream, controller) {
|
|
404
|
-
this.receiveStreams.delete(stream)
|
|
405
|
-
this.receiveStreamsController.delete(controller)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
createBidirectionalStream() {
|
|
409
|
-
const prom = new Promise((resolve, reject) => {
|
|
410
|
-
this.resolveBiDi.push(resolve)
|
|
411
|
-
this.rejectBiDi.push(reject)
|
|
412
|
-
})
|
|
413
|
-
this.objint.orderBidiStream()
|
|
414
|
-
return prom
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
createUnidirectionalStream() {
|
|
418
|
-
const prom = new Promise((resolve, reject) => {
|
|
419
|
-
this.resolveUniDi.push(resolve)
|
|
420
|
-
this.rejectUniDi.push(reject)
|
|
421
|
-
})
|
|
422
|
-
this.objint.orderUnidiStream()
|
|
423
|
-
return prom
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
close(closeInfo) {
|
|
427
|
-
// console.log('closeinfo', closeInfo)
|
|
428
|
-
if (this.state === 'closed' || this.state === 'failed') return
|
|
429
|
-
if (this.objint) {
|
|
430
|
-
this.objint.close({
|
|
431
|
-
code: closeInfo.closeCode,
|
|
432
|
-
reason: closeInfo.reason.substring(0, 1023)
|
|
433
|
-
})
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
onReady(/* error */) {
|
|
438
|
-
if (this.readyResolve) this.readyResolve()
|
|
439
|
-
delete this.readyResolve
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
onClose(errorcode, error) {
|
|
443
|
-
delete this.objint // not valid any more
|
|
444
|
-
// console.log('onClose')
|
|
445
|
-
for (const rej of this.rejectBiDi) rej()
|
|
446
|
-
for (const rej of this.rejectUniDi) rej()
|
|
447
|
-
for (const rej of this.writeDatagramRej) rej()
|
|
448
|
-
this.writeDatagramRej = []
|
|
449
|
-
this.writeDatagramRes = []
|
|
450
|
-
this.writeDatagramProm = []
|
|
451
|
-
this.resolveBiDi = []
|
|
452
|
-
this.resolveUniDi = []
|
|
453
|
-
this.rejectBiDi = []
|
|
454
|
-
this.rejectUniDi = []
|
|
455
|
-
|
|
456
|
-
this.incomBiDiController.close()
|
|
457
|
-
this.incomUniDiController.close()
|
|
458
|
-
this.incomDatagramController.close()
|
|
459
|
-
// this.outgoDatagramController.error(errorcode)
|
|
460
|
-
this.state = 'closed'
|
|
461
|
-
|
|
462
|
-
this.sendStreamsController.forEach((ele) => ele.error(errorcode))
|
|
463
|
-
this.receiveStreamsController.forEach((ele) => ele.error(errorcode))
|
|
464
|
-
this.streamObjs.forEach((ele) => (ele.readableclosed = true))
|
|
465
|
-
|
|
466
|
-
this.sendStreams.clear()
|
|
467
|
-
this.receiveStreams.clear()
|
|
468
|
-
this.sendStreamsController.clear()
|
|
469
|
-
this.receiveStreamsController.clear()
|
|
470
|
-
this.streamObjs.clear()
|
|
471
|
-
|
|
472
|
-
if (this.closedResolve) this.closedResolve(errorcode)
|
|
473
|
-
if (this.closeHook) {
|
|
474
|
-
this.closeHook()
|
|
475
|
-
delete this.closeHook
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
onStream(args) {
|
|
480
|
-
const strobj = new Http3WTStream({
|
|
481
|
-
object: args.stream,
|
|
482
|
-
parentobj: this,
|
|
483
|
-
transport: this.parentobj,
|
|
484
|
-
bidirectional: args.bidirectional,
|
|
485
|
-
incoming: args.incoming
|
|
486
|
-
})
|
|
487
|
-
this.addStreamObj(strobj)
|
|
488
|
-
if (args.incoming) {
|
|
489
|
-
if (args.bidirectional) {
|
|
490
|
-
this.incomBiDiController.enqueue(strobj)
|
|
491
|
-
} else {
|
|
492
|
-
this.incomUniDiController.enqueue(strobj.readable)
|
|
493
|
-
}
|
|
494
|
-
} else {
|
|
495
|
-
if (args.bidirectional) {
|
|
496
|
-
if (this.resolveBiDi.length === 0)
|
|
497
|
-
throw new Error('Got bidirectional stream without asking for it')
|
|
498
|
-
this.rejectBiDi.shift()
|
|
499
|
-
const curres = this.resolveBiDi.shift()
|
|
500
|
-
curres(strobj)
|
|
501
|
-
} else {
|
|
502
|
-
if (this.resolveUniDi.length === 0)
|
|
503
|
-
throw new Error('Got unidirectional stream without asking for it')
|
|
504
|
-
this.rejectUniDi.shift()
|
|
505
|
-
const curres = this.resolveUniDi.shift()
|
|
506
|
-
curres(strobj.writable)
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
onDatagramReceived(args) {
|
|
512
|
-
// console.log('datagram received', args.datagram, Date.now())
|
|
513
|
-
this.incomDatagramController.enqueue(args.datagram)
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
onDatagramSend(args) {
|
|
517
|
-
if (this.state === 'closed') return
|
|
518
|
-
this.writeDatagramRej.shift()
|
|
519
|
-
this.writeDatagramProm.shift()
|
|
520
|
-
const res = this.writeDatagramRes.shift()
|
|
521
|
-
res()
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
static callback(args) {
|
|
525
|
-
// console.log('Session callback called', args)
|
|
526
|
-
if (!args || !args.object || !args.object.jsobj)
|
|
527
|
-
throw new Error('Session callback without jsobj')
|
|
528
|
-
const visitor = args.object.jsobj
|
|
529
|
-
if (args.purpose) {
|
|
530
|
-
switch (args.purpose) {
|
|
531
|
-
case 'SessionReady':
|
|
532
|
-
visitor.onReady()
|
|
533
|
-
break
|
|
534
|
-
case 'SessionClose':
|
|
535
|
-
visitor.onClose(args.errorcode, args.error)
|
|
536
|
-
break
|
|
537
|
-
case 'DatagramReceived':
|
|
538
|
-
if (visitor && args.hasOwnProperty('datagram'))
|
|
539
|
-
visitor.onDatagramReceived(args)
|
|
540
|
-
break
|
|
541
|
-
case 'DatagramSend':
|
|
542
|
-
if (visitor) visitor.onDatagramSend(args)
|
|
543
|
-
break
|
|
544
|
-
case 'Http3WTStreamVisitor':
|
|
545
|
-
if (
|
|
546
|
-
visitor &&
|
|
547
|
-
args.hasOwnProperty('bidirectional') &&
|
|
548
|
-
args.hasOwnProperty('incoming')
|
|
549
|
-
) {
|
|
550
|
-
visitor.onStream(args)
|
|
551
|
-
} else throw new Error('Malformed Http3WTStreamVisitor')
|
|
552
|
-
break
|
|
553
|
-
default:
|
|
554
|
-
throw new Error('unknown purpose Sessioncb')
|
|
555
|
-
}
|
|
556
|
-
} else throw new Error('no purpose Sessioncb')
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
class Http3WebTransport {
|
|
561
|
-
constructor(args, purpose) {
|
|
562
|
-
const eventloop = Http3EventLoop.getGlobalEventLoop(this).eventloopInt
|
|
563
|
-
|
|
564
|
-
if (purpose === 'server')
|
|
565
|
-
this.transportInt = new wtrouter.Http3WebTransportServer(args, eventloop)
|
|
566
|
-
else if (purpose === 'client')
|
|
567
|
-
this.transportInt = new wtrouter.Http3WebTransportClient(args, eventloop)
|
|
568
|
-
else throw new Error('unknown purpose')
|
|
569
|
-
this.transportInt.jsobj = this
|
|
570
|
-
|
|
571
|
-
this.sessions = {}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
static transportCallback(args) {
|
|
575
|
-
// console.log('incoming callback transport', args)
|
|
576
|
-
if (!args || !args.object || !args.object.jsobj)
|
|
577
|
-
throw new Error('Transport callback without jsobj')
|
|
578
|
-
const visitor = args.object.jsobj
|
|
579
|
-
if (args.purpose) {
|
|
580
|
-
if (visitor.customCallback) {
|
|
581
|
-
visitor.customCallback(args)
|
|
582
|
-
} else {
|
|
583
|
-
throw new Error('unknown purpose')
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
export class Http3Server extends Http3WebTransport {
|
|
590
|
-
constructor(args) {
|
|
591
|
-
super(args, 'server')
|
|
592
|
-
this.sessionStreams = {}
|
|
593
|
-
this.sessionController = {}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
startServer() {
|
|
597
|
-
this.transportInt.startServer()
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
stopServer() {
|
|
601
|
-
this.transportInt.stopServer()
|
|
602
|
-
for (const i in this.sessionController) {
|
|
603
|
-
this.sessionController[i].close() // inform the controller, that we are closing
|
|
604
|
-
delete this.sessionController[i]
|
|
605
|
-
}
|
|
606
|
-
this.stopped = true
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
sessionStream(path) {
|
|
610
|
-
if (path in this.sessionStreams) {
|
|
611
|
-
return this.sessionsStreams[path]
|
|
612
|
-
}
|
|
613
|
-
this.sessionStreams[path] = new ReadableStream({
|
|
614
|
-
start: async (controller) => {
|
|
615
|
-
this.sessionController[path] = controller
|
|
616
|
-
}
|
|
617
|
-
})
|
|
618
|
-
this.transportInt.addPath(path)
|
|
619
|
-
return this.sessionStreams[path]
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
customCallback(args) {
|
|
623
|
-
// console.log('incoming callback server', args)
|
|
624
|
-
if (args.purpose) {
|
|
625
|
-
switch (args.purpose) {
|
|
626
|
-
case 'Http3WTSessionVisitor':
|
|
627
|
-
// create Http3 Visitor
|
|
628
|
-
if (args.object) {
|
|
629
|
-
const sesobj = new Http3WTSession({
|
|
630
|
-
object: args.session,
|
|
631
|
-
parentobj: this
|
|
632
|
-
})
|
|
633
|
-
if (this.sessionController[args.path])
|
|
634
|
-
this.sessionController[args.path].enqueue(sesobj)
|
|
635
|
-
} else throw new Error('Http3WTSessionVisitor')
|
|
636
|
-
|
|
637
|
-
break
|
|
638
|
-
|
|
639
|
-
default: {
|
|
640
|
-
throw new Error('unknown purpose')
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
class Http3Client extends Http3WebTransport {
|
|
648
|
-
constructor(args) {
|
|
649
|
-
super(args, 'client')
|
|
650
|
-
|
|
651
|
-
this.sessionobj = new Promise((resolve, reject) => {
|
|
652
|
-
this.sessionProm = { resolve, reject }
|
|
653
|
-
}).catch(() => {}) // add default handler if no one cares
|
|
654
|
-
this.sessionobjint = null
|
|
655
|
-
this.closeHookSession = this.closeHookSession.bind(this)
|
|
656
|
-
|
|
657
|
-
this.handleConnection()
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
async handleConnection() {
|
|
661
|
-
this.quicconnected = new Promise((resolve, reject) => {
|
|
662
|
-
this.quicconnectedProm = { resolve, reject }
|
|
663
|
-
}).catch(() => {}) // add default handler if no one cares
|
|
664
|
-
|
|
665
|
-
this.webtransport = new Promise((resolve, reject) => {
|
|
666
|
-
this.webtransportProm = { resolve, reject }
|
|
667
|
-
}).catch(() => {}) // add default handler if no one cares
|
|
668
|
-
|
|
669
|
-
try {
|
|
670
|
-
await this.quicconnected
|
|
671
|
-
// now create Webtransport session
|
|
672
|
-
setTimeout(() => {
|
|
673
|
-
if (this.webtransportProm) {
|
|
674
|
-
this.webtransportProm.reject(
|
|
675
|
-
new Error('Timeout webtransport support')
|
|
676
|
-
)
|
|
677
|
-
delete this.webtransportProm
|
|
678
|
-
}
|
|
679
|
-
}, 2000)
|
|
680
|
-
} catch (error) {
|
|
681
|
-
console.log('Connecting failed for client:' + error)
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
async createWTSession(sessionobj, path) {
|
|
686
|
-
// TODO
|
|
687
|
-
try {
|
|
688
|
-
await this.webtransport // wait for webtransport support
|
|
689
|
-
// ok now we open the session
|
|
690
|
-
this.sessionobjint = sessionobj
|
|
691
|
-
this.transportInt.openWTSession(path)
|
|
692
|
-
console.log('wait for session')
|
|
693
|
-
// we wait for a new session
|
|
694
|
-
const sessobj = await this.sessionobj
|
|
695
|
-
|
|
696
|
-
delete this.sessionobj
|
|
697
|
-
|
|
698
|
-
return sessobj
|
|
699
|
-
} catch (error) {
|
|
700
|
-
throw new Error('createWTSession failed ' + error)
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
closeHookSession() {
|
|
705
|
-
this.transportInt.closeClient()
|
|
706
|
-
this.stopped = true
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
customCallback(args) {
|
|
710
|
-
// console.log('incoming callback custom client', args)
|
|
711
|
-
if (args.purpose) {
|
|
712
|
-
switch (args.purpose) {
|
|
713
|
-
case 'ClientConnected':
|
|
714
|
-
if (this.quicconnectedProm) {
|
|
715
|
-
if (args.success) this.quicconnectedProm.resolve()
|
|
716
|
-
else
|
|
717
|
-
this.quicconnectedProm.reject(
|
|
718
|
-
new Error('Connecting quic client failed')
|
|
719
|
-
)
|
|
720
|
-
} else throw new Error('Client connected with no pending promise')
|
|
721
|
-
break
|
|
722
|
-
case 'ClientWebtransportSupport':
|
|
723
|
-
if (this.webtransportProm) {
|
|
724
|
-
this.webtransportProm.resolve()
|
|
725
|
-
delete this.webtransportProm
|
|
726
|
-
}
|
|
727
|
-
break
|
|
728
|
-
case 'Http3WTSessionVisitor':
|
|
729
|
-
// create Http3 Visitor
|
|
730
|
-
if (args.session && this.sessionProm && this.sessionobjint) {
|
|
731
|
-
this.sessionobjint.setSessionObj(args.session)
|
|
732
|
-
args.session.jsobj.closeHook = this.closeHookSession
|
|
733
|
-
delete this.sessionobjint
|
|
734
|
-
this.sessionProm.resolve(args.session)
|
|
735
|
-
delete this.sessionProm
|
|
736
|
-
} else
|
|
737
|
-
throw new Error(
|
|
738
|
-
'Http3WTSessionVisitor no object session or nor sessionprom'
|
|
739
|
-
)
|
|
740
|
-
break
|
|
741
|
-
|
|
742
|
-
default: {
|
|
743
|
-
throw new Error('unknown purpose')
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
export class WebTransport {
|
|
751
|
-
constructor(url, args) {
|
|
752
|
-
if (!url) throw new Error('no URL supplied')
|
|
753
|
-
const ourl = (this.urlint = new URL(url))
|
|
754
|
-
|
|
755
|
-
if (ourl.protocol !== 'https:')
|
|
756
|
-
return new Error('URL is not supported for webtransport')
|
|
757
|
-
const hostname = ourl.hostname
|
|
758
|
-
let port = ourl.port
|
|
759
|
-
if (port === '') port = 443
|
|
760
|
-
|
|
761
|
-
this.client = new Http3Client({ hostname, port, ...args })
|
|
762
|
-
|
|
763
|
-
this.sessionint = new Http3WTSession({
|
|
764
|
-
/* object: args.session, */
|
|
765
|
-
parentobj: this.client
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
this.ready = this.sessionint.ready
|
|
769
|
-
this.closed = this.sessionint.closed
|
|
770
|
-
|
|
771
|
-
this.datagrams = this.sessionint.datagrams
|
|
772
|
-
|
|
773
|
-
this.incomingBidirectionalStreams =
|
|
774
|
-
this.sessionint.incomingBidirectionalStreams
|
|
775
|
-
|
|
776
|
-
this.incomingUnidirectionalStreams =
|
|
777
|
-
this.sessionint.incomingUnidirectionalStreams
|
|
778
|
-
|
|
779
|
-
this.establishSession()
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
async establishSession() {
|
|
783
|
-
try {
|
|
784
|
-
await this.client.quicconnected
|
|
785
|
-
await this.client.createWTSession(this.sessionint, this.urlint.pathname)
|
|
786
|
-
} catch (error) {
|
|
787
|
-
this.sessionint.readyReject(
|
|
788
|
-
new Error('Establishing session failed ' + error)
|
|
789
|
-
)
|
|
790
|
-
console.log('Establishing session failed ' + error)
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
close(closeinfo) {
|
|
795
|
-
return this.sessionint.close(closeinfo)
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
createBidirectionalStream() {
|
|
799
|
-
return this.sessionint.createBidirectionalStream()
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
createUnidirectionalStream() {
|
|
803
|
-
return this.sessionint.createUnidirectionalStream()
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
class Http3EventLoop {
|
|
808
|
-
static globalLoop = null
|
|
809
|
-
constructor(args) {
|
|
810
|
-
this.eventloopInt = new wtrouter.Http3EventLoop({
|
|
811
|
-
transportCallback: Http3WebTransport.transportCallback,
|
|
812
|
-
streamCallback: Http3WTStream.callback,
|
|
813
|
-
sessionCallback: Http3WTSession.callback,
|
|
814
|
-
eventloopCallback: Http3EventLoop.callback
|
|
815
|
-
})
|
|
816
|
-
this.eventloopInt.jsobj = this
|
|
817
|
-
|
|
818
|
-
this.refObjects = new Set()
|
|
819
|
-
this.loopGuardian = this.loopGuardian.bind(this)
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
startEventLoop() {
|
|
823
|
-
console.log('start GlobalEventLoop')
|
|
824
|
-
this.eventloopInt.startEventLoop()
|
|
825
|
-
this.loopGuardianTimer = setInterval(this.loopGuardian, 5000)
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
shutdownEventLoop() {
|
|
829
|
-
console.log('shutdown GlobalEventLoop')
|
|
830
|
-
Http3EventLoop.globalLoop = null
|
|
831
|
-
clearInterval(this.loopGuardianTimer)
|
|
832
|
-
this.eventloopInt.shutDownEventLoop()
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
loopGuardian() {
|
|
836
|
-
for (const item of this.refObjects) {
|
|
837
|
-
if (typeof item.deref() === 'undefined' || item.deref()?.stopped)
|
|
838
|
-
this.refObjects.delete(item)
|
|
839
|
-
}
|
|
840
|
-
if (this.refObjects.size === 0) {
|
|
841
|
-
const now = Date.now()
|
|
842
|
-
if (!this.refObjectsEmptyTime) this.refObjectsEmptyTime = now
|
|
843
|
-
else if (now - this.refObjectsEmptyTime > 20 * 1000)
|
|
844
|
-
this.shutdownEventLoop()
|
|
845
|
-
} else if (this.refObjectsEmptyTime) delete this.refObjectsEmptyTime
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
static callback() {
|
|
849
|
-
console.log('final eventloop callback called')
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
static createGlobalEventLoop() {
|
|
853
|
-
if (!Http3EventLoop.globalLoop) {
|
|
854
|
-
Http3EventLoop.globalLoop = new Http3EventLoop()
|
|
855
|
-
Http3EventLoop.globalLoop.startEventLoop()
|
|
856
|
-
console.log('createGlobalEventLoop')
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
static getGlobalEventLoop(object) {
|
|
861
|
-
if (!object) throw new Error('getGlobalEventLoop without reference object')
|
|
862
|
-
if (!Http3EventLoop.globalLoop) Http3EventLoop.createGlobalEventLoop()
|
|
863
|
-
Http3EventLoop.globalLoop.refObjects.add(new WeakRef(object))
|
|
864
|
-
return Http3EventLoop.globalLoop
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
export function testcheck() {
|
|
869
|
-
return !Http3EventLoop.globalLoop
|
|
870
|
-
}
|