@ldtr/nestjs-webtransport 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/dist/index.cjs +430 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.mts +87 -0
- package/dist/index.mjs +423 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
NestJS module for creating WebTransport servers and gateways.
|
|
2
|
+
|
|
3
|
+
Installation
|
|
4
|
+
============
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @ldtr/nestjs-webtransport
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Usage
|
|
11
|
+
=====
|
|
12
|
+
|
|
13
|
+
Declare a server and a gateway:
|
|
14
|
+
```ts
|
|
15
|
+
import {
|
|
16
|
+
WtSession,
|
|
17
|
+
WtStreamRW,
|
|
18
|
+
WtStreamRO,
|
|
19
|
+
WebTransportServer,
|
|
20
|
+
WebTransportGateway,
|
|
21
|
+
type HttpServerInit,
|
|
22
|
+
type WebTransportGatewayLifecycle,
|
|
23
|
+
type WebTransportServerOptionsFactory
|
|
24
|
+
} from "@ldtr/nestjs-webtransport"
|
|
25
|
+
import { readFile } from "node:fs/promises"
|
|
26
|
+
|
|
27
|
+
@WebTransportServer({ name: "main" })
|
|
28
|
+
export class MainWebTransportServer implements WebTransportServerOptionsFactory {
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly configService: ConfigService
|
|
31
|
+
) {
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async options(): Promise<HttpServerInit> {
|
|
35
|
+
const cert = await readFile("./certs/cert.pem", { encoding: "utf-8" })
|
|
36
|
+
const privKey = await readFile("./certs/key.pem", { encoding: "utf-8" })
|
|
37
|
+
const secret = this.configService.secret
|
|
38
|
+
return {
|
|
39
|
+
port: 3001,
|
|
40
|
+
host: "0.0.0.0",
|
|
41
|
+
secret,
|
|
42
|
+
cert,
|
|
43
|
+
privKey,
|
|
44
|
+
defaultDatagramsReadableMode: "bytes",
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@WebTransportGateway({ server: "main", path: "/events" })
|
|
50
|
+
export class EventsGateway implements WebTransportGatewayLifecycle {
|
|
51
|
+
onSession(session: WtSession): void {
|
|
52
|
+
// new WebTransport session
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onStreamRW(stream: WtStreamRW): void {
|
|
56
|
+
// new bidirectional stream
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onStreamRO(stream: WtStreamRO): void {
|
|
60
|
+
// new unidirectional stream
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then import them in your application with the ```WebTransportModule```:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { WebTransportModule } from "@ldtr/nestjs-webtransport"
|
|
69
|
+
import { Module } from "@nestjs/common"
|
|
70
|
+
|
|
71
|
+
@Module({
|
|
72
|
+
imports: [
|
|
73
|
+
WebTransportModule],
|
|
74
|
+
providers: [
|
|
75
|
+
EventsGateway,
|
|
76
|
+
MainWebTransportServer]
|
|
77
|
+
})
|
|
78
|
+
export class AppModule {
|
|
79
|
+
}
|
|
80
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _nestjs_common = require("@nestjs/common");
|
|
3
|
+
let _nestjs_core = require("@nestjs/core");
|
|
4
|
+
//#region src/lib/decorators/webtransport-gateway.decorator.ts
|
|
5
|
+
const DiscoverableWebTransportGateway = _nestjs_core.DiscoveryService.createDecorator();
|
|
6
|
+
function WebTransportGateway(options) {
|
|
7
|
+
return (0, _nestjs_common.applyDecorators)((0, _nestjs_common.Injectable)(), DiscoverableWebTransportGateway(options));
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/lib/decorators/webtransport-server.decorator.ts
|
|
11
|
+
const DiscoverableWebTransportServer = _nestjs_core.DiscoveryService.createDecorator();
|
|
12
|
+
function WebTransportServer(options) {
|
|
13
|
+
return (0, _nestjs_common.applyDecorators)((0, _nestjs_common.Injectable)(), DiscoverableWebTransportServer(options));
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/lib/bootstraps/helpers.ts
|
|
17
|
+
async function* generateSession(stream) {
|
|
18
|
+
const reader = stream.getReader();
|
|
19
|
+
try {
|
|
20
|
+
while (true) {
|
|
21
|
+
const { done, value } = await reader.read();
|
|
22
|
+
if (done) break;
|
|
23
|
+
yield value;
|
|
24
|
+
}
|
|
25
|
+
} finally {
|
|
26
|
+
reader.releaseLock();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function* generateBStream(session) {
|
|
30
|
+
const reader = session.incomingBidirectionalStreams.getReader();
|
|
31
|
+
try {
|
|
32
|
+
while (true) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done) break;
|
|
35
|
+
yield value;
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
reader.releaseLock();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function* generateUStream(session) {
|
|
42
|
+
const reader = session.incomingUnidirectionalStreams.getReader();
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done) break;
|
|
47
|
+
yield value;
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
reader.releaseLock();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function* generateChunks(reader) {
|
|
54
|
+
try {
|
|
55
|
+
while (true) {
|
|
56
|
+
const { done, value } = await reader.read();
|
|
57
|
+
if (done) break;
|
|
58
|
+
yield value;
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
reader.releaseLock();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function fireAndForget(task, logger) {
|
|
65
|
+
if (!task) return;
|
|
66
|
+
try {
|
|
67
|
+
Promise.resolve(task()).catch((error) => {
|
|
68
|
+
logger?.error(error);
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger?.error(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/lib/classes/wt-stream.ts
|
|
76
|
+
var AbstractStreamHandler = class {
|
|
77
|
+
closed = false;
|
|
78
|
+
async close() {
|
|
79
|
+
if (this.closed) return;
|
|
80
|
+
this.closed = true;
|
|
81
|
+
await this.localClose();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var ReadableStreamHandler = class extends AbstractStreamHandler {
|
|
85
|
+
reader;
|
|
86
|
+
chunks;
|
|
87
|
+
constructor(reader) {
|
|
88
|
+
super();
|
|
89
|
+
this.reader = reader;
|
|
90
|
+
this.chunks = generateChunks(this.reader);
|
|
91
|
+
}
|
|
92
|
+
read() {
|
|
93
|
+
if (this.closed) throw new Error("Can't read stream");
|
|
94
|
+
return this.chunks;
|
|
95
|
+
}
|
|
96
|
+
async localClose() {
|
|
97
|
+
await this.reader.cancel();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var WritableStreamHandler = class extends AbstractStreamHandler {
|
|
101
|
+
writer;
|
|
102
|
+
constructor(writer) {
|
|
103
|
+
super();
|
|
104
|
+
this.writer = writer;
|
|
105
|
+
}
|
|
106
|
+
async write(chunk) {
|
|
107
|
+
if (this.closed) throw new Error("Can't write stream");
|
|
108
|
+
await this.writer.write(chunk);
|
|
109
|
+
}
|
|
110
|
+
async localClose() {
|
|
111
|
+
await this.writer.close();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var AbstractWtStream = class {
|
|
115
|
+
session;
|
|
116
|
+
webTransportStream;
|
|
117
|
+
constructor(session, webTransportStream) {
|
|
118
|
+
this.session = session;
|
|
119
|
+
this.webTransportStream = webTransportStream;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var WtStreamRW = class extends AbstractWtStream {
|
|
123
|
+
readableStreamHandler;
|
|
124
|
+
writableStreamHandler;
|
|
125
|
+
constructor(session, webTransportStream) {
|
|
126
|
+
super(session, webTransportStream);
|
|
127
|
+
this.readableStreamHandler = new ReadableStreamHandler(webTransportStream.readable.getReader());
|
|
128
|
+
this.writableStreamHandler = new WritableStreamHandler(webTransportStream.writable.getWriter());
|
|
129
|
+
}
|
|
130
|
+
read() {
|
|
131
|
+
return this.readableStreamHandler.read();
|
|
132
|
+
}
|
|
133
|
+
async write(data) {
|
|
134
|
+
await this.writableStreamHandler.write(data);
|
|
135
|
+
}
|
|
136
|
+
async closeReadable() {
|
|
137
|
+
await this.readableStreamHandler.close();
|
|
138
|
+
}
|
|
139
|
+
async closeWritable() {
|
|
140
|
+
await this.writableStreamHandler.close();
|
|
141
|
+
}
|
|
142
|
+
async close() {
|
|
143
|
+
await Promise.all([this.closeReadable(), this.closeWritable()]);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var WtStreamRO = class extends AbstractWtStream {
|
|
147
|
+
readableStreamHandler;
|
|
148
|
+
constructor(session, webTransportStream) {
|
|
149
|
+
super(session, webTransportStream);
|
|
150
|
+
this.readableStreamHandler = new ReadableStreamHandler(webTransportStream.getReader());
|
|
151
|
+
}
|
|
152
|
+
read() {
|
|
153
|
+
return this.readableStreamHandler.read();
|
|
154
|
+
}
|
|
155
|
+
async closeReadable() {
|
|
156
|
+
await this.readableStreamHandler.close();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var WtStreamWO = class extends AbstractWtStream {
|
|
160
|
+
writableStreamHandler;
|
|
161
|
+
constructor(session, webTransportStream) {
|
|
162
|
+
super(session, webTransportStream);
|
|
163
|
+
this.writableStreamHandler = new WritableStreamHandler(webTransportStream.getWriter());
|
|
164
|
+
}
|
|
165
|
+
async write(data) {
|
|
166
|
+
await this.writableStreamHandler.write(data);
|
|
167
|
+
}
|
|
168
|
+
async closeWritable() {
|
|
169
|
+
await this.writableStreamHandler.close();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/lib/explorers/webtransport-gateway.explorer.ts
|
|
174
|
+
var WebTransportGatewayExplorer = @((0, _nestjs_common.Injectable)()) class {
|
|
175
|
+
discoveryService;
|
|
176
|
+
constructor(discoveryService) {
|
|
177
|
+
this.discoveryService = discoveryService;
|
|
178
|
+
}
|
|
179
|
+
discover(serverNames) {
|
|
180
|
+
const gatewaysByNameByPath = /* @__PURE__ */ new Map();
|
|
181
|
+
const wrappers = this.discoveryService.getProviders({ metadataKey: DiscoverableWebTransportGateway.KEY });
|
|
182
|
+
for (const wrapper of wrappers) {
|
|
183
|
+
const metadata = this.discoveryService.getMetadataByDecorator(DiscoverableWebTransportGateway, wrapper);
|
|
184
|
+
if (!metadata || !wrapper.metatype) continue;
|
|
185
|
+
const { server } = metadata;
|
|
186
|
+
let { path } = metadata;
|
|
187
|
+
if (!path.startsWith("/")) path = "/" + path;
|
|
188
|
+
const gateway = wrapper.instance;
|
|
189
|
+
if (!gatewaysByNameByPath.has(server)) {
|
|
190
|
+
if (!serverNames.includes(server)) throw new Error(`@WebTransportGateway() server name doesn't exist: ${server}`);
|
|
191
|
+
gatewaysByNameByPath.set(server, /* @__PURE__ */ new Map());
|
|
192
|
+
}
|
|
193
|
+
if (gatewaysByNameByPath.get(server).has(path)) throw new Error(`Duplicate @WebTransportGateway() path: ${path}`);
|
|
194
|
+
gatewaysByNameByPath.get(server).set(path, gateway);
|
|
195
|
+
}
|
|
196
|
+
return gatewaysByNameByPath;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/lib/explorers/webtransport-server.explorer.ts
|
|
201
|
+
var WebTransportServerExplorer = @((0, _nestjs_common.Injectable)()) class {
|
|
202
|
+
discoveryService;
|
|
203
|
+
constructor(discoveryService) {
|
|
204
|
+
this.discoveryService = discoveryService;
|
|
205
|
+
}
|
|
206
|
+
discover() {
|
|
207
|
+
const serversByName = /* @__PURE__ */ new Map();
|
|
208
|
+
const wrappers = this.discoveryService.getProviders({ metadataKey: DiscoverableWebTransportServer.KEY });
|
|
209
|
+
for (const wrapper of wrappers) {
|
|
210
|
+
const metadata = this.discoveryService.getMetadataByDecorator(DiscoverableWebTransportServer, wrapper);
|
|
211
|
+
if (!metadata || !wrapper.metatype) continue;
|
|
212
|
+
const { name } = metadata;
|
|
213
|
+
if (serversByName.has(name)) throw new Error(`Duplicate @WebTransportServer() name: ${name}`);
|
|
214
|
+
const server = wrapper.instance;
|
|
215
|
+
serversByName.set(name, server);
|
|
216
|
+
}
|
|
217
|
+
return serversByName;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/lib/bootstraps/web-transport-bootstrapper.ts
|
|
222
|
+
var WebTransportBootstrapper = @((0, _nestjs_common.Injectable)()) class WebTransportBootstrapper {
|
|
223
|
+
serverFactory;
|
|
224
|
+
webTransportExplorer;
|
|
225
|
+
logger = new _nestjs_common.Logger(WebTransportBootstrapper.name);
|
|
226
|
+
h3ServersWithInfo = [];
|
|
227
|
+
constructor(serverFactory, webTransportExplorer) {
|
|
228
|
+
this.serverFactory = serverFactory;
|
|
229
|
+
this.webTransportExplorer = webTransportExplorer;
|
|
230
|
+
}
|
|
231
|
+
async onApplicationBootstrap() {
|
|
232
|
+
const serverBindings = this.webTransportExplorer.discover();
|
|
233
|
+
this.h3ServersWithInfo = await this.serverFactory.createH3Servers(serverBindings);
|
|
234
|
+
await Promise.all(this.h3ServersWithInfo.map(async ({ options, name, h3Server }) => {
|
|
235
|
+
try {
|
|
236
|
+
h3Server.startServer();
|
|
237
|
+
await h3Server.ready;
|
|
238
|
+
this.logger.log(`WebTransport server "${name}" is ready on ${options.host}:${options.port}`);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
this.logger.error(`Failed to start WebTransport server "${name}" on ${options.host}:${options.port}`);
|
|
241
|
+
throw e;
|
|
242
|
+
}
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
async onApplicationShutdown() {
|
|
246
|
+
await Promise.all(this.h3ServersWithInfo.map(async ({ name, h3Server }) => {
|
|
247
|
+
h3Server.stopServer();
|
|
248
|
+
await h3Server.closed;
|
|
249
|
+
this.logger.log(`WebTransport server "${name}" stopped`);
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/lib/explorers/web-transport.explorer.ts
|
|
255
|
+
var WebTransportExplorer = @((0, _nestjs_common.Injectable)()) class WebTransportExplorer {
|
|
256
|
+
webTransportServerExplorer;
|
|
257
|
+
webTransportGatewayExplorer;
|
|
258
|
+
logger = new _nestjs_common.Logger(WebTransportExplorer.name);
|
|
259
|
+
constructor(webTransportServerExplorer, webTransportGatewayExplorer) {
|
|
260
|
+
this.webTransportServerExplorer = webTransportServerExplorer;
|
|
261
|
+
this.webTransportGatewayExplorer = webTransportGatewayExplorer;
|
|
262
|
+
}
|
|
263
|
+
discover() {
|
|
264
|
+
const serversByName = this.webTransportServerExplorer.discover();
|
|
265
|
+
const gatewaysByNameByPath = this.webTransportGatewayExplorer.discover(Array.from(serversByName.keys()));
|
|
266
|
+
const serverBindings = [];
|
|
267
|
+
serversByName.forEach((server, name) => {
|
|
268
|
+
const gatewaysByPath = gatewaysByNameByPath.get(name);
|
|
269
|
+
if (gatewaysByPath === void 0) this.logger.warn(`WebTransport server "${name}" has no gateways, skipping`);
|
|
270
|
+
else serverBindings.push({
|
|
271
|
+
server,
|
|
272
|
+
name,
|
|
273
|
+
gatewaysByPath
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
return serverBindings;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/lib/bootstraps/consumers/abstract-stream.consumer.ts
|
|
281
|
+
var AbstractStreamConsumer = class {
|
|
282
|
+
async consume(gateway, webTransportSession, session) {
|
|
283
|
+
for await (const webTransportStream of this.generateStream(webTransportSession)) this.handleStream(gateway, session, webTransportStream);
|
|
284
|
+
}
|
|
285
|
+
handleStream(gateway, session, webTransportStream) {
|
|
286
|
+
const stream = this.createStream(session, webTransportStream);
|
|
287
|
+
this.consumeHook(gateway, stream);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/lib/bootstraps/consumers/stream-ro.consumer.ts
|
|
292
|
+
var StreamROConsumer = @((0, _nestjs_common.Injectable)()) class StreamROConsumer extends AbstractStreamConsumer {
|
|
293
|
+
logger = new _nestjs_common.Logger(StreamROConsumer.name);
|
|
294
|
+
generateStream(session) {
|
|
295
|
+
return generateUStream(session);
|
|
296
|
+
}
|
|
297
|
+
createStream(session, webTransportStream) {
|
|
298
|
+
return new WtStreamRO(session, webTransportStream);
|
|
299
|
+
}
|
|
300
|
+
consumeHook(gateway, stream) {
|
|
301
|
+
fireAndForget(() => gateway.onStreamRO?.(stream), this.logger);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/lib/bootstraps/consumers/stream-rw.consumer.ts
|
|
306
|
+
var StreamRWConsumer = @((0, _nestjs_common.Injectable)()) class StreamRWConsumer extends AbstractStreamConsumer {
|
|
307
|
+
logger = new _nestjs_common.Logger(StreamRWConsumer.name);
|
|
308
|
+
generateStream(session) {
|
|
309
|
+
return generateBStream(session);
|
|
310
|
+
}
|
|
311
|
+
createStream(session, webTransportStream) {
|
|
312
|
+
return new WtStreamRW(session, webTransportStream);
|
|
313
|
+
}
|
|
314
|
+
consumeHook(gateway, stream) {
|
|
315
|
+
fireAndForget(() => gateway.onStreamRW?.(stream), this.logger);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/lib/classes/wt-session.ts
|
|
320
|
+
var WtSession = class {
|
|
321
|
+
webTransportSession;
|
|
322
|
+
constructor(webTransportSession) {
|
|
323
|
+
this.webTransportSession = webTransportSession;
|
|
324
|
+
}
|
|
325
|
+
async createStreamWO() {
|
|
326
|
+
const stream = await this.webTransportSession.createUnidirectionalStream();
|
|
327
|
+
return new WtStreamWO(this, stream);
|
|
328
|
+
}
|
|
329
|
+
async createStreamRW() {
|
|
330
|
+
const stream = await this.webTransportSession.createBidirectionalStream();
|
|
331
|
+
return new WtStreamRW(this, stream);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/lib/bootstraps/server-factory.ts
|
|
336
|
+
let webtransport;
|
|
337
|
+
const getLib = eval(`import('@fails-components/webtransport')`).then((module) => {
|
|
338
|
+
webtransport = module;
|
|
339
|
+
});
|
|
340
|
+
function createHandshakeResponse(request, host, status) {
|
|
341
|
+
const url = new URL(request.header[":path"], `https://${host}`);
|
|
342
|
+
return {
|
|
343
|
+
...request,
|
|
344
|
+
path: url.pathname,
|
|
345
|
+
header: {
|
|
346
|
+
...request.header,
|
|
347
|
+
":path": url.pathname
|
|
348
|
+
},
|
|
349
|
+
status
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
var ServerFactory = @((0, _nestjs_common.Injectable)()) class ServerFactory {
|
|
353
|
+
streamRWConsumer;
|
|
354
|
+
streamROConsumer;
|
|
355
|
+
logger = new _nestjs_common.Logger(ServerFactory.name);
|
|
356
|
+
constructor(streamRWConsumer, streamROConsumer) {
|
|
357
|
+
this.streamRWConsumer = streamRWConsumer;
|
|
358
|
+
this.streamROConsumer = streamROConsumer;
|
|
359
|
+
}
|
|
360
|
+
async createH3Servers(serverBindings) {
|
|
361
|
+
await getLib;
|
|
362
|
+
const h3ServersWithInfo = [];
|
|
363
|
+
for (const { server, name, gatewaysByPath } of serverBindings) {
|
|
364
|
+
const options = await server.options();
|
|
365
|
+
const h3Server = this.createH3Server(options, gatewaysByPath);
|
|
366
|
+
h3ServersWithInfo.push({
|
|
367
|
+
options,
|
|
368
|
+
h3Server,
|
|
369
|
+
name
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return h3ServersWithInfo;
|
|
373
|
+
}
|
|
374
|
+
createH3Server(options, gatewaysByPath) {
|
|
375
|
+
const h3Server = new webtransport.Http3Server(options);
|
|
376
|
+
h3Server.setRequestCallback(async (request) => {
|
|
377
|
+
const status = await this.getRequestStatus(request, gatewaysByPath);
|
|
378
|
+
return createHandshakeResponse(request, options.host, status);
|
|
379
|
+
});
|
|
380
|
+
for (const [path, gateway] of gatewaysByPath.entries()) this.consumeSession(gateway, h3Server, path).catch((error) => this.logger.error(error));
|
|
381
|
+
return h3Server;
|
|
382
|
+
}
|
|
383
|
+
async getRequestStatus(request, gatewaysByPath) {
|
|
384
|
+
const { pathname } = new URL(request.header[":path"], "https://0.0.0.0");
|
|
385
|
+
const gateway = gatewaysByPath.get(pathname);
|
|
386
|
+
if (gateway === void 0) return 404;
|
|
387
|
+
try {
|
|
388
|
+
await gateway.allowRequest?.(request);
|
|
389
|
+
return 200;
|
|
390
|
+
} catch (e) {
|
|
391
|
+
if (e instanceof _nestjs_common.HttpException) return e.getStatus();
|
|
392
|
+
return 400;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async consumeSession(gateway, h3Server, path) {
|
|
396
|
+
const stream = h3Server.sessionStream(path);
|
|
397
|
+
for await (const session of generateSession(stream)) this.handleSession(gateway, session);
|
|
398
|
+
}
|
|
399
|
+
handleSession(gateway, webTransportSession) {
|
|
400
|
+
const session = new WtSession(webTransportSession);
|
|
401
|
+
fireAndForget(() => gateway.onSession?.(session), this.logger);
|
|
402
|
+
webTransportSession.closed.catch((error) => this.logger.error(error)).finally(() => {
|
|
403
|
+
fireAndForget(() => gateway.onSessionClosed?.(session), this.logger);
|
|
404
|
+
});
|
|
405
|
+
this.streamRWConsumer.consume(gateway, webTransportSession, session).catch((error) => this.logger.error(error));
|
|
406
|
+
this.streamROConsumer.consume(gateway, webTransportSession, session).catch((error) => this.logger.error(error));
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/lib/webtransport.module.ts
|
|
411
|
+
var WebTransportModule = @((0, _nestjs_common.Module)({
|
|
412
|
+
imports: [_nestjs_core.DiscoveryModule],
|
|
413
|
+
providers: [
|
|
414
|
+
ServerFactory,
|
|
415
|
+
StreamRWConsumer,
|
|
416
|
+
StreamROConsumer,
|
|
417
|
+
WebTransportExplorer,
|
|
418
|
+
WebTransportBootstrapper,
|
|
419
|
+
WebTransportServerExplorer,
|
|
420
|
+
WebTransportGatewayExplorer
|
|
421
|
+
]
|
|
422
|
+
})) class {};
|
|
423
|
+
//#endregion
|
|
424
|
+
exports.WebTransportGateway = WebTransportGateway;
|
|
425
|
+
exports.WebTransportModule = WebTransportModule;
|
|
426
|
+
exports.WebTransportServer = WebTransportServer;
|
|
427
|
+
exports.WtSession = WtSession;
|
|
428
|
+
exports.WtStreamRO = WtStreamRO;
|
|
429
|
+
exports.WtStreamRW = WtStreamRW;
|
|
430
|
+
exports.WtStreamWO = WtStreamWO;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { HttpServerInit, HttpServerInit as HttpServerInit$1, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream, WebTransportSession } from "@fails-components/webtransport";
|
|
2
|
+
//#region src/lib/decorators/webtransport-gateway.decorator.d.ts
|
|
3
|
+
type WebTransportGatewayOptions = {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly server: string;
|
|
6
|
+
};
|
|
7
|
+
declare function WebTransportGateway(options: WebTransportGatewayOptions): ClassDecorator;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/lib/decorators/webtransport-server.decorator.d.ts
|
|
10
|
+
interface WebTransportServerOptions {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
}
|
|
13
|
+
declare function WebTransportServer(options: WebTransportServerOptions): ClassDecorator;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/lib/explorers/webtransport-server.explorer.d.ts
|
|
16
|
+
type WebTransportServerOptionsFactory = {
|
|
17
|
+
options(): HttpServerInit$1 | Promise<HttpServerInit$1>;
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/lib/classes/wt-session.d.ts
|
|
21
|
+
declare class WtSession {
|
|
22
|
+
private readonly webTransportSession;
|
|
23
|
+
constructor(webTransportSession: WebTransportSession);
|
|
24
|
+
createStreamWO(): Promise<WtStreamWO>;
|
|
25
|
+
createStreamRW(): Promise<WtStreamRW>;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/lib/classes/wt-stream.d.ts
|
|
29
|
+
declare abstract class AbstractWtStream<TWebStream> {
|
|
30
|
+
readonly session: WtSession;
|
|
31
|
+
protected readonly webTransportStream: TWebStream;
|
|
32
|
+
protected constructor(session: WtSession, webTransportStream: TWebStream);
|
|
33
|
+
}
|
|
34
|
+
interface WtReadableStream {
|
|
35
|
+
read(): AsyncIterableIterator<Uint8Array>;
|
|
36
|
+
closeReadable(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
interface WtWritableStream {
|
|
39
|
+
write(data: Uint8Array): Promise<void>;
|
|
40
|
+
closeWritable(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
declare class WtStreamRW extends AbstractWtStream<WebTransportBidirectionalStream> implements WtReadableStream, WtWritableStream {
|
|
43
|
+
private readonly readableStreamHandler;
|
|
44
|
+
private readonly writableStreamHandler;
|
|
45
|
+
constructor(session: WtSession, webTransportStream: WebTransportBidirectionalStream);
|
|
46
|
+
read(): AsyncIterableIterator<Uint8Array>;
|
|
47
|
+
write(data: Uint8Array): Promise<void>;
|
|
48
|
+
closeReadable(): Promise<void>;
|
|
49
|
+
closeWritable(): Promise<void>;
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
declare class WtStreamRO extends AbstractWtStream<WebTransportReceiveStream> implements WtReadableStream {
|
|
53
|
+
private readonly readableStreamHandler;
|
|
54
|
+
constructor(session: WtSession, webTransportStream: WebTransportReceiveStream);
|
|
55
|
+
read(): AsyncIterableIterator<Uint8Array>;
|
|
56
|
+
closeReadable(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
declare class WtStreamWO extends AbstractWtStream<WebTransportSendStream> implements WtWritableStream {
|
|
59
|
+
private readonly writableStreamHandler;
|
|
60
|
+
constructor(session: WtSession, webTransportStream: WebTransportSendStream);
|
|
61
|
+
write(data: Uint8Array): Promise<void>;
|
|
62
|
+
closeWritable(): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/lib/bootstraps/types.d.ts
|
|
66
|
+
type WebTransportRequestHeader = {
|
|
67
|
+
readonly ':path': string;
|
|
68
|
+
};
|
|
69
|
+
type WebTransportRequest = {
|
|
70
|
+
readonly header: WebTransportRequestHeader;
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/lib/explorers/webtransport-gateway.explorer.d.ts
|
|
74
|
+
type WebTransportGatewayLifecycle = {
|
|
75
|
+
allowRequest?(request: WebTransportRequest): void | Promise<void>;
|
|
76
|
+
onSession?(session: WtSession): void | Promise<void>;
|
|
77
|
+
onSessionClosed?(session: WtSession): void | Promise<void>;
|
|
78
|
+
onStreamRW?(stream: WtStreamRW): void | Promise<void>;
|
|
79
|
+
onStreamRO?(stream: WtStreamRO): void | Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/lib/webtransport.module.d.ts
|
|
83
|
+
declare class WebTransportModule {}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { type HttpServerInit, WebTransportGateway, type WebTransportGatewayLifecycle, type WebTransportGatewayOptions, WebTransportModule, WebTransportServer, type WebTransportServerOptions, type WebTransportServerOptionsFactory, WtSession, WtStreamRO, WtStreamRW, WtStreamWO };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DiscoveryService } from "@nestjs/core";
|
|
2
|
+
import { HttpServerInit, HttpServerInit as HttpServerInit$1, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream, WebTransportSession } from "@fails-components/webtransport";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/decorators/webtransport-gateway.decorator.d.ts
|
|
5
|
+
type WebTransportGatewayOptions = {
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly server: string;
|
|
8
|
+
};
|
|
9
|
+
declare function WebTransportGateway(options: WebTransportGatewayOptions): ClassDecorator;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/lib/decorators/webtransport-server.decorator.d.ts
|
|
12
|
+
interface WebTransportServerOptions {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
}
|
|
15
|
+
declare function WebTransportServer(options: WebTransportServerOptions): ClassDecorator;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/lib/explorers/webtransport-server.explorer.d.ts
|
|
18
|
+
type WebTransportServerOptionsFactory = {
|
|
19
|
+
options(): HttpServerInit$1 | Promise<HttpServerInit$1>;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/lib/classes/wt-session.d.ts
|
|
23
|
+
declare class WtSession {
|
|
24
|
+
private readonly webTransportSession;
|
|
25
|
+
constructor(webTransportSession: WebTransportSession);
|
|
26
|
+
createStreamWO(): Promise<WtStreamWO>;
|
|
27
|
+
createStreamRW(): Promise<WtStreamRW>;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/lib/classes/wt-stream.d.ts
|
|
31
|
+
declare abstract class AbstractWtStream<TWebStream> {
|
|
32
|
+
readonly session: WtSession;
|
|
33
|
+
protected readonly webTransportStream: TWebStream;
|
|
34
|
+
protected constructor(session: WtSession, webTransportStream: TWebStream);
|
|
35
|
+
}
|
|
36
|
+
interface WtReadableStream {
|
|
37
|
+
read(): AsyncIterableIterator<Uint8Array>;
|
|
38
|
+
closeReadable(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
interface WtWritableStream {
|
|
41
|
+
write(data: Uint8Array): Promise<void>;
|
|
42
|
+
closeWritable(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
declare class WtStreamRW extends AbstractWtStream<WebTransportBidirectionalStream> implements WtReadableStream, WtWritableStream {
|
|
45
|
+
private readonly readableStreamHandler;
|
|
46
|
+
private readonly writableStreamHandler;
|
|
47
|
+
constructor(session: WtSession, webTransportStream: WebTransportBidirectionalStream);
|
|
48
|
+
read(): AsyncIterableIterator<Uint8Array>;
|
|
49
|
+
write(data: Uint8Array): Promise<void>;
|
|
50
|
+
closeReadable(): Promise<void>;
|
|
51
|
+
closeWritable(): Promise<void>;
|
|
52
|
+
close(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
declare class WtStreamRO extends AbstractWtStream<WebTransportReceiveStream> implements WtReadableStream {
|
|
55
|
+
private readonly readableStreamHandler;
|
|
56
|
+
constructor(session: WtSession, webTransportStream: WebTransportReceiveStream);
|
|
57
|
+
read(): AsyncIterableIterator<Uint8Array>;
|
|
58
|
+
closeReadable(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
declare class WtStreamWO extends AbstractWtStream<WebTransportSendStream> implements WtWritableStream {
|
|
61
|
+
private readonly writableStreamHandler;
|
|
62
|
+
constructor(session: WtSession, webTransportStream: WebTransportSendStream);
|
|
63
|
+
write(data: Uint8Array): Promise<void>;
|
|
64
|
+
closeWritable(): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/lib/bootstraps/types.d.ts
|
|
68
|
+
type WebTransportRequestHeader = {
|
|
69
|
+
readonly ':path': string;
|
|
70
|
+
};
|
|
71
|
+
type WebTransportRequest = {
|
|
72
|
+
readonly header: WebTransportRequestHeader;
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/lib/explorers/webtransport-gateway.explorer.d.ts
|
|
76
|
+
type WebTransportGatewayLifecycle = {
|
|
77
|
+
allowRequest?(request: WebTransportRequest): void | Promise<void>;
|
|
78
|
+
onSession?(session: WtSession): void | Promise<void>;
|
|
79
|
+
onSessionClosed?(session: WtSession): void | Promise<void>;
|
|
80
|
+
onStreamRW?(stream: WtStreamRW): void | Promise<void>;
|
|
81
|
+
onStreamRO?(stream: WtStreamRO): void | Promise<void>;
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/lib/webtransport.module.d.ts
|
|
85
|
+
declare class WebTransportModule {}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { type HttpServerInit, WebTransportGateway, type WebTransportGatewayLifecycle, type WebTransportGatewayOptions, WebTransportModule, WebTransportServer, type WebTransportServerOptions, type WebTransportServerOptionsFactory, WtSession, WtStreamRO, WtStreamRW, WtStreamWO };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { HttpException, Injectable, Logger, Module, applyDecorators } from "@nestjs/common";
|
|
2
|
+
import { DiscoveryModule, DiscoveryService } from "@nestjs/core";
|
|
3
|
+
//#region src/lib/decorators/webtransport-gateway.decorator.ts
|
|
4
|
+
const DiscoverableWebTransportGateway = DiscoveryService.createDecorator();
|
|
5
|
+
function WebTransportGateway(options) {
|
|
6
|
+
return applyDecorators(Injectable(), DiscoverableWebTransportGateway(options));
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/lib/decorators/webtransport-server.decorator.ts
|
|
10
|
+
const DiscoverableWebTransportServer = DiscoveryService.createDecorator();
|
|
11
|
+
function WebTransportServer(options) {
|
|
12
|
+
return applyDecorators(Injectable(), DiscoverableWebTransportServer(options));
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/lib/bootstraps/helpers.ts
|
|
16
|
+
async function* generateSession(stream) {
|
|
17
|
+
const reader = stream.getReader();
|
|
18
|
+
try {
|
|
19
|
+
while (true) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done) break;
|
|
22
|
+
yield value;
|
|
23
|
+
}
|
|
24
|
+
} finally {
|
|
25
|
+
reader.releaseLock();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function* generateBStream(session) {
|
|
29
|
+
const reader = session.incomingBidirectionalStreams.getReader();
|
|
30
|
+
try {
|
|
31
|
+
while (true) {
|
|
32
|
+
const { done, value } = await reader.read();
|
|
33
|
+
if (done) break;
|
|
34
|
+
yield value;
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
reader.releaseLock();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function* generateUStream(session) {
|
|
41
|
+
const reader = session.incomingUnidirectionalStreams.getReader();
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
yield value;
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
reader.releaseLock();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function* generateChunks(reader) {
|
|
53
|
+
try {
|
|
54
|
+
while (true) {
|
|
55
|
+
const { done, value } = await reader.read();
|
|
56
|
+
if (done) break;
|
|
57
|
+
yield value;
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
reader.releaseLock();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function fireAndForget(task, logger) {
|
|
64
|
+
if (!task) return;
|
|
65
|
+
try {
|
|
66
|
+
Promise.resolve(task()).catch((error) => {
|
|
67
|
+
logger?.error(error);
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger?.error(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/lib/classes/wt-stream.ts
|
|
75
|
+
var AbstractStreamHandler = class {
|
|
76
|
+
closed = false;
|
|
77
|
+
async close() {
|
|
78
|
+
if (this.closed) return;
|
|
79
|
+
this.closed = true;
|
|
80
|
+
await this.localClose();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var ReadableStreamHandler = class extends AbstractStreamHandler {
|
|
84
|
+
reader;
|
|
85
|
+
chunks;
|
|
86
|
+
constructor(reader) {
|
|
87
|
+
super();
|
|
88
|
+
this.reader = reader;
|
|
89
|
+
this.chunks = generateChunks(this.reader);
|
|
90
|
+
}
|
|
91
|
+
read() {
|
|
92
|
+
if (this.closed) throw new Error("Can't read stream");
|
|
93
|
+
return this.chunks;
|
|
94
|
+
}
|
|
95
|
+
async localClose() {
|
|
96
|
+
await this.reader.cancel();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var WritableStreamHandler = class extends AbstractStreamHandler {
|
|
100
|
+
writer;
|
|
101
|
+
constructor(writer) {
|
|
102
|
+
super();
|
|
103
|
+
this.writer = writer;
|
|
104
|
+
}
|
|
105
|
+
async write(chunk) {
|
|
106
|
+
if (this.closed) throw new Error("Can't write stream");
|
|
107
|
+
await this.writer.write(chunk);
|
|
108
|
+
}
|
|
109
|
+
async localClose() {
|
|
110
|
+
await this.writer.close();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var AbstractWtStream = class {
|
|
114
|
+
session;
|
|
115
|
+
webTransportStream;
|
|
116
|
+
constructor(session, webTransportStream) {
|
|
117
|
+
this.session = session;
|
|
118
|
+
this.webTransportStream = webTransportStream;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var WtStreamRW = class extends AbstractWtStream {
|
|
122
|
+
readableStreamHandler;
|
|
123
|
+
writableStreamHandler;
|
|
124
|
+
constructor(session, webTransportStream) {
|
|
125
|
+
super(session, webTransportStream);
|
|
126
|
+
this.readableStreamHandler = new ReadableStreamHandler(webTransportStream.readable.getReader());
|
|
127
|
+
this.writableStreamHandler = new WritableStreamHandler(webTransportStream.writable.getWriter());
|
|
128
|
+
}
|
|
129
|
+
read() {
|
|
130
|
+
return this.readableStreamHandler.read();
|
|
131
|
+
}
|
|
132
|
+
async write(data) {
|
|
133
|
+
await this.writableStreamHandler.write(data);
|
|
134
|
+
}
|
|
135
|
+
async closeReadable() {
|
|
136
|
+
await this.readableStreamHandler.close();
|
|
137
|
+
}
|
|
138
|
+
async closeWritable() {
|
|
139
|
+
await this.writableStreamHandler.close();
|
|
140
|
+
}
|
|
141
|
+
async close() {
|
|
142
|
+
await Promise.all([this.closeReadable(), this.closeWritable()]);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var WtStreamRO = class extends AbstractWtStream {
|
|
146
|
+
readableStreamHandler;
|
|
147
|
+
constructor(session, webTransportStream) {
|
|
148
|
+
super(session, webTransportStream);
|
|
149
|
+
this.readableStreamHandler = new ReadableStreamHandler(webTransportStream.getReader());
|
|
150
|
+
}
|
|
151
|
+
read() {
|
|
152
|
+
return this.readableStreamHandler.read();
|
|
153
|
+
}
|
|
154
|
+
async closeReadable() {
|
|
155
|
+
await this.readableStreamHandler.close();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var WtStreamWO = class extends AbstractWtStream {
|
|
159
|
+
writableStreamHandler;
|
|
160
|
+
constructor(session, webTransportStream) {
|
|
161
|
+
super(session, webTransportStream);
|
|
162
|
+
this.writableStreamHandler = new WritableStreamHandler(webTransportStream.getWriter());
|
|
163
|
+
}
|
|
164
|
+
async write(data) {
|
|
165
|
+
await this.writableStreamHandler.write(data);
|
|
166
|
+
}
|
|
167
|
+
async closeWritable() {
|
|
168
|
+
await this.writableStreamHandler.close();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/lib/explorers/webtransport-gateway.explorer.ts
|
|
173
|
+
var WebTransportGatewayExplorer = @Injectable() class {
|
|
174
|
+
discoveryService;
|
|
175
|
+
constructor(discoveryService) {
|
|
176
|
+
this.discoveryService = discoveryService;
|
|
177
|
+
}
|
|
178
|
+
discover(serverNames) {
|
|
179
|
+
const gatewaysByNameByPath = /* @__PURE__ */ new Map();
|
|
180
|
+
const wrappers = this.discoveryService.getProviders({ metadataKey: DiscoverableWebTransportGateway.KEY });
|
|
181
|
+
for (const wrapper of wrappers) {
|
|
182
|
+
const metadata = this.discoveryService.getMetadataByDecorator(DiscoverableWebTransportGateway, wrapper);
|
|
183
|
+
if (!metadata || !wrapper.metatype) continue;
|
|
184
|
+
const { server } = metadata;
|
|
185
|
+
let { path } = metadata;
|
|
186
|
+
if (!path.startsWith("/")) path = "/" + path;
|
|
187
|
+
const gateway = wrapper.instance;
|
|
188
|
+
if (!gatewaysByNameByPath.has(server)) {
|
|
189
|
+
if (!serverNames.includes(server)) throw new Error(`@WebTransportGateway() server name doesn't exist: ${server}`);
|
|
190
|
+
gatewaysByNameByPath.set(server, /* @__PURE__ */ new Map());
|
|
191
|
+
}
|
|
192
|
+
if (gatewaysByNameByPath.get(server).has(path)) throw new Error(`Duplicate @WebTransportGateway() path: ${path}`);
|
|
193
|
+
gatewaysByNameByPath.get(server).set(path, gateway);
|
|
194
|
+
}
|
|
195
|
+
return gatewaysByNameByPath;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/lib/explorers/webtransport-server.explorer.ts
|
|
200
|
+
var WebTransportServerExplorer = @Injectable() class {
|
|
201
|
+
discoveryService;
|
|
202
|
+
constructor(discoveryService) {
|
|
203
|
+
this.discoveryService = discoveryService;
|
|
204
|
+
}
|
|
205
|
+
discover() {
|
|
206
|
+
const serversByName = /* @__PURE__ */ new Map();
|
|
207
|
+
const wrappers = this.discoveryService.getProviders({ metadataKey: DiscoverableWebTransportServer.KEY });
|
|
208
|
+
for (const wrapper of wrappers) {
|
|
209
|
+
const metadata = this.discoveryService.getMetadataByDecorator(DiscoverableWebTransportServer, wrapper);
|
|
210
|
+
if (!metadata || !wrapper.metatype) continue;
|
|
211
|
+
const { name } = metadata;
|
|
212
|
+
if (serversByName.has(name)) throw new Error(`Duplicate @WebTransportServer() name: ${name}`);
|
|
213
|
+
const server = wrapper.instance;
|
|
214
|
+
serversByName.set(name, server);
|
|
215
|
+
}
|
|
216
|
+
return serversByName;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/lib/bootstraps/web-transport-bootstrapper.ts
|
|
221
|
+
var WebTransportBootstrapper = @Injectable() class WebTransportBootstrapper {
|
|
222
|
+
serverFactory;
|
|
223
|
+
webTransportExplorer;
|
|
224
|
+
logger = new Logger(WebTransportBootstrapper.name);
|
|
225
|
+
h3ServersWithInfo = [];
|
|
226
|
+
constructor(serverFactory, webTransportExplorer) {
|
|
227
|
+
this.serverFactory = serverFactory;
|
|
228
|
+
this.webTransportExplorer = webTransportExplorer;
|
|
229
|
+
}
|
|
230
|
+
async onApplicationBootstrap() {
|
|
231
|
+
const serverBindings = this.webTransportExplorer.discover();
|
|
232
|
+
this.h3ServersWithInfo = await this.serverFactory.createH3Servers(serverBindings);
|
|
233
|
+
await Promise.all(this.h3ServersWithInfo.map(async ({ options, name, h3Server }) => {
|
|
234
|
+
try {
|
|
235
|
+
h3Server.startServer();
|
|
236
|
+
await h3Server.ready;
|
|
237
|
+
this.logger.log(`WebTransport server "${name}" is ready on ${options.host}:${options.port}`);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
this.logger.error(`Failed to start WebTransport server "${name}" on ${options.host}:${options.port}`);
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
async onApplicationShutdown() {
|
|
245
|
+
await Promise.all(this.h3ServersWithInfo.map(async ({ name, h3Server }) => {
|
|
246
|
+
h3Server.stopServer();
|
|
247
|
+
await h3Server.closed;
|
|
248
|
+
this.logger.log(`WebTransport server "${name}" stopped`);
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/lib/explorers/web-transport.explorer.ts
|
|
254
|
+
var WebTransportExplorer = @Injectable() class WebTransportExplorer {
|
|
255
|
+
webTransportServerExplorer;
|
|
256
|
+
webTransportGatewayExplorer;
|
|
257
|
+
logger = new Logger(WebTransportExplorer.name);
|
|
258
|
+
constructor(webTransportServerExplorer, webTransportGatewayExplorer) {
|
|
259
|
+
this.webTransportServerExplorer = webTransportServerExplorer;
|
|
260
|
+
this.webTransportGatewayExplorer = webTransportGatewayExplorer;
|
|
261
|
+
}
|
|
262
|
+
discover() {
|
|
263
|
+
const serversByName = this.webTransportServerExplorer.discover();
|
|
264
|
+
const gatewaysByNameByPath = this.webTransportGatewayExplorer.discover(Array.from(serversByName.keys()));
|
|
265
|
+
const serverBindings = [];
|
|
266
|
+
serversByName.forEach((server, name) => {
|
|
267
|
+
const gatewaysByPath = gatewaysByNameByPath.get(name);
|
|
268
|
+
if (gatewaysByPath === void 0) this.logger.warn(`WebTransport server "${name}" has no gateways, skipping`);
|
|
269
|
+
else serverBindings.push({
|
|
270
|
+
server,
|
|
271
|
+
name,
|
|
272
|
+
gatewaysByPath
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
return serverBindings;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/lib/bootstraps/consumers/abstract-stream.consumer.ts
|
|
280
|
+
var AbstractStreamConsumer = class {
|
|
281
|
+
async consume(gateway, webTransportSession, session) {
|
|
282
|
+
for await (const webTransportStream of this.generateStream(webTransportSession)) this.handleStream(gateway, session, webTransportStream);
|
|
283
|
+
}
|
|
284
|
+
handleStream(gateway, session, webTransportStream) {
|
|
285
|
+
const stream = this.createStream(session, webTransportStream);
|
|
286
|
+
this.consumeHook(gateway, stream);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/lib/bootstraps/consumers/stream-ro.consumer.ts
|
|
291
|
+
var StreamROConsumer = @Injectable() class StreamROConsumer extends AbstractStreamConsumer {
|
|
292
|
+
logger = new Logger(StreamROConsumer.name);
|
|
293
|
+
generateStream(session) {
|
|
294
|
+
return generateUStream(session);
|
|
295
|
+
}
|
|
296
|
+
createStream(session, webTransportStream) {
|
|
297
|
+
return new WtStreamRO(session, webTransportStream);
|
|
298
|
+
}
|
|
299
|
+
consumeHook(gateway, stream) {
|
|
300
|
+
fireAndForget(() => gateway.onStreamRO?.(stream), this.logger);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/lib/bootstraps/consumers/stream-rw.consumer.ts
|
|
305
|
+
var StreamRWConsumer = @Injectable() class StreamRWConsumer extends AbstractStreamConsumer {
|
|
306
|
+
logger = new Logger(StreamRWConsumer.name);
|
|
307
|
+
generateStream(session) {
|
|
308
|
+
return generateBStream(session);
|
|
309
|
+
}
|
|
310
|
+
createStream(session, webTransportStream) {
|
|
311
|
+
return new WtStreamRW(session, webTransportStream);
|
|
312
|
+
}
|
|
313
|
+
consumeHook(gateway, stream) {
|
|
314
|
+
fireAndForget(() => gateway.onStreamRW?.(stream), this.logger);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/lib/classes/wt-session.ts
|
|
319
|
+
var WtSession = class {
|
|
320
|
+
webTransportSession;
|
|
321
|
+
constructor(webTransportSession) {
|
|
322
|
+
this.webTransportSession = webTransportSession;
|
|
323
|
+
}
|
|
324
|
+
async createStreamWO() {
|
|
325
|
+
const stream = await this.webTransportSession.createUnidirectionalStream();
|
|
326
|
+
return new WtStreamWO(this, stream);
|
|
327
|
+
}
|
|
328
|
+
async createStreamRW() {
|
|
329
|
+
const stream = await this.webTransportSession.createBidirectionalStream();
|
|
330
|
+
return new WtStreamRW(this, stream);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/lib/bootstraps/server-factory.ts
|
|
335
|
+
let webtransport;
|
|
336
|
+
const getLib = eval(`import('@fails-components/webtransport')`).then((module) => {
|
|
337
|
+
webtransport = module;
|
|
338
|
+
});
|
|
339
|
+
function createHandshakeResponse(request, host, status) {
|
|
340
|
+
const url = new URL(request.header[":path"], `https://${host}`);
|
|
341
|
+
return {
|
|
342
|
+
...request,
|
|
343
|
+
path: url.pathname,
|
|
344
|
+
header: {
|
|
345
|
+
...request.header,
|
|
346
|
+
":path": url.pathname
|
|
347
|
+
},
|
|
348
|
+
status
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
var ServerFactory = @Injectable() class ServerFactory {
|
|
352
|
+
streamRWConsumer;
|
|
353
|
+
streamROConsumer;
|
|
354
|
+
logger = new Logger(ServerFactory.name);
|
|
355
|
+
constructor(streamRWConsumer, streamROConsumer) {
|
|
356
|
+
this.streamRWConsumer = streamRWConsumer;
|
|
357
|
+
this.streamROConsumer = streamROConsumer;
|
|
358
|
+
}
|
|
359
|
+
async createH3Servers(serverBindings) {
|
|
360
|
+
await getLib;
|
|
361
|
+
const h3ServersWithInfo = [];
|
|
362
|
+
for (const { server, name, gatewaysByPath } of serverBindings) {
|
|
363
|
+
const options = await server.options();
|
|
364
|
+
const h3Server = this.createH3Server(options, gatewaysByPath);
|
|
365
|
+
h3ServersWithInfo.push({
|
|
366
|
+
options,
|
|
367
|
+
h3Server,
|
|
368
|
+
name
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return h3ServersWithInfo;
|
|
372
|
+
}
|
|
373
|
+
createH3Server(options, gatewaysByPath) {
|
|
374
|
+
const h3Server = new webtransport.Http3Server(options);
|
|
375
|
+
h3Server.setRequestCallback(async (request) => {
|
|
376
|
+
const status = await this.getRequestStatus(request, gatewaysByPath);
|
|
377
|
+
return createHandshakeResponse(request, options.host, status);
|
|
378
|
+
});
|
|
379
|
+
for (const [path, gateway] of gatewaysByPath.entries()) this.consumeSession(gateway, h3Server, path).catch((error) => this.logger.error(error));
|
|
380
|
+
return h3Server;
|
|
381
|
+
}
|
|
382
|
+
async getRequestStatus(request, gatewaysByPath) {
|
|
383
|
+
const { pathname } = new URL(request.header[":path"], "https://0.0.0.0");
|
|
384
|
+
const gateway = gatewaysByPath.get(pathname);
|
|
385
|
+
if (gateway === void 0) return 404;
|
|
386
|
+
try {
|
|
387
|
+
await gateway.allowRequest?.(request);
|
|
388
|
+
return 200;
|
|
389
|
+
} catch (e) {
|
|
390
|
+
if (e instanceof HttpException) return e.getStatus();
|
|
391
|
+
return 400;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async consumeSession(gateway, h3Server, path) {
|
|
395
|
+
const stream = h3Server.sessionStream(path);
|
|
396
|
+
for await (const session of generateSession(stream)) this.handleSession(gateway, session);
|
|
397
|
+
}
|
|
398
|
+
handleSession(gateway, webTransportSession) {
|
|
399
|
+
const session = new WtSession(webTransportSession);
|
|
400
|
+
fireAndForget(() => gateway.onSession?.(session), this.logger);
|
|
401
|
+
webTransportSession.closed.catch((error) => this.logger.error(error)).finally(() => {
|
|
402
|
+
fireAndForget(() => gateway.onSessionClosed?.(session), this.logger);
|
|
403
|
+
});
|
|
404
|
+
this.streamRWConsumer.consume(gateway, webTransportSession, session).catch((error) => this.logger.error(error));
|
|
405
|
+
this.streamROConsumer.consume(gateway, webTransportSession, session).catch((error) => this.logger.error(error));
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/lib/webtransport.module.ts
|
|
410
|
+
var WebTransportModule = @Module({
|
|
411
|
+
imports: [DiscoveryModule],
|
|
412
|
+
providers: [
|
|
413
|
+
ServerFactory,
|
|
414
|
+
StreamRWConsumer,
|
|
415
|
+
StreamROConsumer,
|
|
416
|
+
WebTransportExplorer,
|
|
417
|
+
WebTransportBootstrapper,
|
|
418
|
+
WebTransportServerExplorer,
|
|
419
|
+
WebTransportGatewayExplorer
|
|
420
|
+
]
|
|
421
|
+
}) class {};
|
|
422
|
+
//#endregion
|
|
423
|
+
export { WebTransportGateway, WebTransportModule, WebTransportServer, WtSession, WtStreamRO, WtStreamRW, WtStreamWO };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ldtr/nestjs-webtransport",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": ["dist"],
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsdown",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@fails-components/webtransport": "^1.6.3",
|
|
22
|
+
"@fails-components/webtransport-transport-http3-quiche": "^1.6.3",
|
|
23
|
+
"reflect-metadata": "^0.1.13",
|
|
24
|
+
"tslib": "^2.3.0",
|
|
25
|
+
"tsdown": "^0.22.3",
|
|
26
|
+
"typescript": "^6.0.3"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@nestjs/common": "^11.0.0",
|
|
30
|
+
"@nestjs/core": "^11.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|