@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 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;
@@ -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 };
@@ -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
+ }