@rsdk/nats.transport 5.4.0-next.2

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/events.deserializer.d.ts +23 -0
  3. package/dist/events.deserializer.js +94 -0
  4. package/dist/events.deserializer.js.map +1 -0
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.js +10 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/metadata.decorator.d.ts +3 -0
  9. package/dist/metadata.decorator.js +41 -0
  10. package/dist/metadata.decorator.js.map +1 -0
  11. package/dist/nats-jetstream-errors.formatter.d.ts +6 -0
  12. package/dist/nats-jetstream-errors.formatter.js +14 -0
  13. package/dist/nats-jetstream-errors.formatter.js.map +1 -0
  14. package/dist/nats-jetstream-transport.module.d.ts +2 -0
  15. package/dist/nats-jetstream-transport.module.js +17 -0
  16. package/dist/nats-jetstream-transport.module.js.map +1 -0
  17. package/dist/nats-jetstream.config.d.ts +5 -0
  18. package/dist/nats-jetstream.config.js +29 -0
  19. package/dist/nats-jetstream.config.js.map +1 -0
  20. package/dist/nats-jetstream.transport.d.ts +20 -0
  21. package/dist/nats-jetstream.transport.js +73 -0
  22. package/dist/nats-jetstream.transport.js.map +1 -0
  23. package/dist/nats.headers.d.ts +10 -0
  24. package/dist/nats.headers.js +28 -0
  25. package/dist/nats.headers.js.map +1 -0
  26. package/dist/payload.decorator.d.ts +8 -0
  27. package/dist/payload.decorator.js +9 -0
  28. package/dist/payload.decorator.js.map +1 -0
  29. package/dist/server.d.ts +136 -0
  30. package/dist/server.js +365 -0
  31. package/dist/server.js.map +1 -0
  32. package/dist/types/consume.options.d.ts +6 -0
  33. package/dist/types/consume.options.js +3 -0
  34. package/dist/types/consume.options.js.map +1 -0
  35. package/dist/types/consumer-info.type.d.ts +5 -0
  36. package/dist/types/consumer-info.type.js +3 -0
  37. package/dist/types/consumer-info.type.js.map +1 -0
  38. package/dist/types/consumers-map.type.d.ts +2 -0
  39. package/dist/types/consumers-map.type.js +3 -0
  40. package/dist/types/consumers-map.type.js.map +1 -0
  41. package/dist/types/event-type-with-options.type.d.ts +6 -0
  42. package/dist/types/event-type-with-options.type.js +3 -0
  43. package/dist/types/event-type-with-options.type.js.map +1 -0
  44. package/dist/types/mapping.type.d.ts +6 -0
  45. package/dist/types/mapping.type.js +3 -0
  46. package/dist/types/mapping.type.js.map +1 -0
  47. package/dist/types/nats-jetstream-transport-options.type.d.ts +2 -0
  48. package/dist/types/nats-jetstream-transport-options.type.js +3 -0
  49. package/dist/types/nats-jetstream-transport-options.type.js.map +1 -0
  50. package/jest.config.js +1 -0
  51. package/jest.config.unit.js +1 -0
  52. package/package.json +43 -0
  53. package/src/events.deserializer.ts +133 -0
  54. package/src/index.ts +5 -0
  55. package/src/metadata.decorator.ts +54 -0
  56. package/src/nats-jetstream-errors.formatter.ts +13 -0
  57. package/src/nats-jetstream-transport.module.ts +4 -0
  58. package/src/nats-jetstream.config.ts +14 -0
  59. package/src/nats-jetstream.transport.ts +94 -0
  60. package/src/nats.headers.ts +33 -0
  61. package/src/payload.decorator.ts +17 -0
  62. package/src/server.ts +491 -0
  63. package/src/types/consume.options.ts +6 -0
  64. package/src/types/consumer-info.type.ts +6 -0
  65. package/src/types/consumers-map.type.ts +3 -0
  66. package/src/types/event-type-with-options.type.ts +8 -0
  67. package/src/types/mapping.type.ts +8 -0
  68. package/src/types/nats-jetstream-transport-options.type.ts +3 -0
  69. package/tsconfig.build.json +12 -0
  70. package/tsconfig.json +7 -0
package/src/server.ts ADDED
@@ -0,0 +1,491 @@
1
+ import type {
2
+ CustomTransportStrategy,
3
+ MessageHandler,
4
+ } from '@nestjs/microservices';
5
+ import { Server } from '@nestjs/microservices';
6
+ import type { NatsJetStreamServerOptions } from '@nestjs-plugins/nestjs-nats-jetstream-transport';
7
+ import {
8
+ NATS_JETSTREAM_TRANSPORT,
9
+ NatsContext,
10
+ NatsJetStreamContext,
11
+ } from '@nestjs-plugins/nestjs-nats-jetstream-transport';
12
+ import { InternalException, NotFoundException } from '@rsdk/core';
13
+ import type { EventType } from '@rsdk/events.common';
14
+ import { isEventType, X_TYPE_HEADER } from '@rsdk/events.common';
15
+ import { LoggerFactory } from '@rsdk/logging';
16
+ import { getStreamName } from '@rsdk/nats.common';
17
+ import { isEqual } from 'lodash';
18
+ import type {
19
+ Consumer,
20
+ ConsumerConfig,
21
+ JetStreamClient,
22
+ JetStreamManager,
23
+ NatsConnection,
24
+ SubscriptionOptions,
25
+ } from 'nats';
26
+ import { connect } from 'nats';
27
+ import { connectable, isObservable, Subject } from 'rxjs';
28
+
29
+ import type { ConsumerInfo } from './types/consumer-info.type';
30
+ import type { ConsumersMap } from './types/consumers-map.type';
31
+ import type { EventTypeWithOptions } from './types/event-type-with-options.type';
32
+ import type { Mapping } from './types/mapping.type';
33
+ import { EventsDeserializer } from './events.deserializer';
34
+
35
+ /**
36
+ * Класс, реализующий сервер NATS JetStream.
37
+ */
38
+ export class NatsJetStreamServer
39
+ extends Server
40
+ implements CustomTransportStrategy
41
+ {
42
+ readonly transportId = NATS_JETSTREAM_TRANSPORT;
43
+ override logger = LoggerFactory.create('NatsJetStreamServer');
44
+ override readonly deserializer = new EventsDeserializer();
45
+ private nc?: NatsConnection;
46
+ private jsm?: JetStreamManager;
47
+ private readonly streams = new Set<string>();
48
+
49
+ private readonly normalizedApplicationName: string;
50
+ private readonly autoUpdateConsumers: boolean;
51
+
52
+ /**
53
+ * Конструктор класса NatsJetStreamServer.
54
+ * @param options - Опции для подключения к NATS JetStream.
55
+ * @param config - Конфигурация приложения.
56
+ */
57
+ constructor(
58
+ private readonly options: NatsJetStreamServerOptions,
59
+ private readonly config: {
60
+ appName: string;
61
+ autoUpdateConsumers?: boolean;
62
+ },
63
+ ) {
64
+ super();
65
+ this.normalizedApplicationName = config.appName.replaceAll('.', '-');
66
+ this.autoUpdateConsumers = Boolean(config.autoUpdateConsumers);
67
+ }
68
+
69
+ static isNatsContext(
70
+ maybeNatsContext: unknown,
71
+ ): maybeNatsContext is NatsJetStreamContext {
72
+ return maybeNatsContext instanceof NatsJetStreamContext;
73
+ }
74
+
75
+ /**
76
+ * Запускает сервер и устанавливает соединение.
77
+ * @param callback - Функция обратного вызова для выполнения после подключения.
78
+ */
79
+ async listen(callback: () => void): Promise<void> {
80
+ if (!this.nc) {
81
+ this.nc = await connect(this.options.connectionOptions);
82
+ this.options.connectionOptions!.connectedHook?.(this.nc);
83
+ }
84
+ this.jsm = await this.nc.jetstreamManager(this.options.jetStreamOptions);
85
+
86
+ await this.bindEventHandlers();
87
+ this.bindMessageHandlers();
88
+ callback();
89
+ }
90
+
91
+ /**
92
+ * Закрывает соединение с сервером.
93
+ */
94
+ async close(): Promise<void> {
95
+ await this.nc?.drain();
96
+ delete this.nc;
97
+ }
98
+
99
+ /**
100
+ * Добавляет обработчик сообщения.
101
+ * @param pattern Шаблон для сообщения.
102
+ * @param callback Функция-обработчик сообщения.
103
+ * @param isEventHandler Указывает, является ли обработчик обработчиком событий.
104
+ * @param extras Дополнительные параметры.
105
+ */
106
+ public override addHandler(
107
+ pattern: any,
108
+ callback: MessageHandler,
109
+ isEventHandler = false,
110
+ extras: Record<string, any> = {},
111
+ ): void {
112
+ const eventType = this.getEventType(pattern);
113
+ let formattedPattern: string;
114
+
115
+ if (isEventType(eventType)) {
116
+ this.streams.add(this.getStreamName(pattern));
117
+ formattedPattern = eventType.$type;
118
+ this.deserializer.events.set(formattedPattern, pattern);
119
+ } else if (typeof eventType === 'string') {
120
+ formattedPattern = eventType;
121
+ this.streams.add(eventType);
122
+ }
123
+
124
+ super.addHandler(formattedPattern!, callback, isEventHandler, extras);
125
+ }
126
+
127
+ /**
128
+ * Получает тип события из шаблона.
129
+ * @param pattern - Шаблон для получения типа события.
130
+ * @returns Тип события.
131
+ */
132
+ private getEventType(pattern: any): any {
133
+ return typeof pattern === 'object' && 'eventType' in pattern
134
+ ? pattern.eventType
135
+ : pattern;
136
+ }
137
+
138
+ /**
139
+ * Связывает обработчики событий.
140
+ */
141
+ private async bindEventHandlers(): Promise<void> {
142
+ const eventHandlers = [...this.messageHandlers.entries()].filter(
143
+ ([, handler]) => handler.isEventHandler,
144
+ );
145
+
146
+ if (!this.nc) {
147
+ throw new InternalException('Connection must be established');
148
+ }
149
+ const js = this.nc.jetstream(this.options.jetStreamOptions);
150
+
151
+ const { orderedConsumers, unorderedConsumers } =
152
+ this.groupHandlersByOrderType(eventHandlers);
153
+
154
+ for (const [consumerName, consumer] of orderedConsumers) {
155
+ await this.createConsumer(js, consumerName, consumer, {
156
+ max_ack_pending: 1,
157
+ });
158
+ }
159
+
160
+ for (const [consumerName, consumer] of unorderedConsumers) {
161
+ await this.createConsumer(js, consumerName, consumer, {});
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Группирует обработчики по типу порядка (зависимые и независимые от порядка).
167
+ *
168
+ * @param {Array<[string, MessageHandler<any, any, any>]>} eventHandlers - Список обработчиков событий,
169
+ * где каждый элемент представляет собой кортеж из метаданных и обработчика.
170
+ *
171
+ * @returns {{
172
+ * orderedConsumers: ConsumersMap,
173
+ * unorderedConsumers: ConsumersMap
174
+ * }}
175
+ * Объект, содержащий две карты:
176
+ * - orderedConsumers: карта с зависимыми от порядка consumer'ами.
177
+ * - unorderedConsumers: карта с независимыми от порядка consumer'ами.
178
+ */
179
+ private groupHandlersByOrderType(
180
+ eventHandlers: [string, MessageHandler<any, any, any>][],
181
+ ): { orderedConsumers: ConsumersMap; unorderedConsumers: ConsumersMap } {
182
+ const orderedConsumers: ConsumersMap = new Map();
183
+ const unorderedConsumers: ConsumersMap = new Map();
184
+
185
+ for (const [meta, handler] of eventHandlers) {
186
+ const deserializer = this.deserializer.events.get(meta)!;
187
+ const consumerName = this.getConsumerName(deserializer);
188
+ const streamName = this.getStreamName(deserializer);
189
+
190
+ const targetMap = deserializer.options?.maxAckPending
191
+ ? orderedConsumers
192
+ : unorderedConsumers;
193
+
194
+ const aggregatedConsumer = targetMap.get(consumerName) ?? {
195
+ streamName,
196
+ mapping: new Map() as Mapping,
197
+ };
198
+
199
+ aggregatedConsumer.mapping.set(this.getTypePart(deserializer.eventType), {
200
+ deserializer,
201
+ handler,
202
+ });
203
+ targetMap.set(consumerName, aggregatedConsumer);
204
+ }
205
+
206
+ return { orderedConsumers, unorderedConsumers };
207
+ }
208
+
209
+ /**
210
+ * Получает информацию о консьюмере.
211
+ * @param js - Клиент JetStream.
212
+ * @param streamName - Имя потока.
213
+ * @param consumerName - Имя консьюмера.
214
+ * @returns Информация о консьюмере.
215
+ */
216
+ private async getConsumer(
217
+ js: JetStreamClient,
218
+ streamName: string,
219
+ consumerName: string,
220
+ ): Promise<Consumer> {
221
+ return js.consumers.get(streamName, consumerName);
222
+ }
223
+
224
+ /**
225
+ * Создаёт консьюмера для заданного потока.
226
+ * @param js - Клиент JetStream.
227
+ * @param consumerName - Имя консьюмера.
228
+ * @param consumer - Информация о консьюмере.
229
+ * @param options - Дополнительные параметры конфигурации консьюмера.
230
+ */
231
+ private async createConsumer(
232
+ js: JetStreamClient,
233
+ consumerName: string,
234
+ { streamName, mapping }: ConsumerInfo,
235
+ options: Partial<ConsumerConfig>,
236
+ ): Promise<void> {
237
+ try {
238
+ await js.streams.get(streamName);
239
+ this.logger.trace('Stream %s found', { streamName });
240
+ } catch (error) {
241
+ this.logger.error('Stream %s not found', { streamName });
242
+ throw new InternalException(`Stream not found: ${streamName}`, {
243
+ cause: error,
244
+ });
245
+ }
246
+
247
+ const jsm = await js.jetstreamManager();
248
+ const expectedSubjects: string[] = [...mapping.values()].flatMap((e) =>
249
+ this.getEventSubjects(e.deserializer),
250
+ );
251
+
252
+ const consumer: Consumer =
253
+ (await this.getConsumer(js, streamName, consumerName).catch(
254
+ () => null,
255
+ )) ??
256
+ (await jsm.consumers
257
+ .add(streamName, {
258
+ ...options,
259
+ durable_name: consumerName,
260
+ filter_subjects: expectedSubjects,
261
+ })
262
+ .then(() => this.getConsumer(js, streamName, consumerName)));
263
+
264
+ const info = await consumer.info();
265
+ const receivedSubjects = info.config.filter_subjects;
266
+
267
+ await this.handleCorrectness(
268
+ jsm,
269
+ streamName,
270
+ consumerName,
271
+ expectedSubjects,
272
+ receivedSubjects,
273
+ );
274
+
275
+ this.handleMessages(consumer, mapping, consumerName);
276
+ }
277
+
278
+ /**
279
+ * Обрабатывает сообщения из консьюмера.
280
+ * @param consumer - Консьюмер для обработки сообщений.
281
+ * @param mapping - Соответствия между типами сообщений и обработчиками.
282
+ * @param consumerName - Имя консьюмера.
283
+ */
284
+ private async handleMessages(
285
+ consumer: Consumer,
286
+ mapping: Mapping,
287
+ consumerName: string,
288
+ ): Promise<void> {
289
+ const msgs = await consumer.consume();
290
+
291
+ /**
292
+ * Слушаем все сообщения консьюмера
293
+ * @type {Promise<void>}
294
+ */
295
+ const done = (async (): Promise<void> => {
296
+ for await (const msg of msgs) {
297
+ try {
298
+ const data = this.deserializer.deserialize(msg);
299
+ const context = new NatsJetStreamContext([msg]);
300
+ const contract = mapping.get(this.getTypePart(data.pattern));
301
+
302
+ if (!contract) {
303
+ throw new NotFoundException(`Unknown ${X_TYPE_HEADER} header`);
304
+ }
305
+
306
+ const resultOrStream = await contract.handler(data, context);
307
+ if (isObservable(resultOrStream)) {
308
+ const connectableSource = connectable(resultOrStream, {
309
+ connector: () => new Subject(),
310
+ resetOnDisconnect: false,
311
+ });
312
+
313
+ connectableSource.connect();
314
+ }
315
+ } catch (error: any) {
316
+ this.logger.error(error.message, error.stack);
317
+ // specifies that you failed to process the server and instructs
318
+ // the server to not send it again (to any consumer)
319
+ msg.term();
320
+ continue;
321
+ }
322
+
323
+ /**
324
+ * Потенциально мы можем сюда придти если в одном консюьмере несколько сервисов
325
+ * Это не совсем корректное поведение потому что неясно есть ли такой обработчик, а мы можем встрять и ни разу не двинуть оффсет
326
+ */
327
+ msg.nak();
328
+ this.logger.warn('Unhandled message: ', { msg });
329
+ }
330
+ })();
331
+
332
+ done.then(() => {
333
+ if (this.nc?.isDraining() || this.nc?.isClosed()) {
334
+ return;
335
+ }
336
+
337
+ this.logger.debug(`Unsubscribed ${consumerName}`);
338
+ });
339
+ }
340
+
341
+ /**
342
+ * Проверяет корректность подписки консьюмера.
343
+ * @param jsm - Менеджер JetStream.
344
+ * @param streamName - Имя потока.
345
+ * @param consumerName - Имя консьюмера.
346
+ * @param expectedSubjects - Ожидаемые темы.
347
+ * @param receivedSubjects - Полученные темы.
348
+ */
349
+ private async handleCorrectness(
350
+ jsm: JetStreamManager,
351
+ streamName: string,
352
+ consumerName: string,
353
+ expectedSubjects: string[],
354
+ receivedSubjects: string[] | undefined,
355
+ ): Promise<void> {
356
+ if (isEqual(receivedSubjects, expectedSubjects)) {
357
+ return;
358
+ }
359
+ if (!this.autoUpdateConsumers) {
360
+ throw new InternalException('Incorrect subjects', {
361
+ cause: {
362
+ expectedSubjects,
363
+ receivedSubjects,
364
+ },
365
+ });
366
+ }
367
+ await jsm.consumers.update(streamName, consumerName, {
368
+ filter_subjects: expectedSubjects,
369
+ });
370
+ }
371
+
372
+ /**
373
+ * Связывает обработчики сообщений.
374
+ */
375
+ private bindMessageHandlers(): void {
376
+ const messageHandlers = [...this.messageHandlers.entries()].filter(
377
+ ([, handler]) => !handler.isEventHandler,
378
+ );
379
+
380
+ for (const [subject, messageHandler] of messageHandlers) {
381
+ const subscriptionOptions: SubscriptionOptions = {
382
+ callback: async (err, msg) => {
383
+ if (err) {
384
+ this.logger.error(err.message, err);
385
+ return;
386
+ }
387
+ const payload = this.deserializer.deserialize(msg);
388
+ const context = new NatsContext([msg]);
389
+ const response$ = this.transformToObservable(
390
+ messageHandler(payload.data, context),
391
+ );
392
+
393
+ this.send(response$, (response) =>
394
+ msg.respond(response as Uint8Array),
395
+ );
396
+ },
397
+ };
398
+
399
+ this.nc?.subscribe(subject, subscriptionOptions);
400
+ this.logger.debug(`Subscribed to ${subject} messages`);
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Настраивает поток для сервера.
406
+ */
407
+ private async setupStream(): Promise<void> {
408
+ const { streamConfig } = this.options;
409
+ const streams = await this.jsm?.streams.list().next();
410
+
411
+ const reqStreamConfigs = Array.isArray(streamConfig)
412
+ ? streamConfig
413
+ : [streamConfig];
414
+
415
+ for (const requiredStreamConfig of reqStreamConfigs) {
416
+ if (!requiredStreamConfig) {
417
+ continue;
418
+ }
419
+ const stream = streams?.find(
420
+ (stream) => stream.config.name === requiredStreamConfig?.name,
421
+ );
422
+
423
+ if (stream) {
424
+ const streamSubjects = new Set([
425
+ ...stream.config.subjects,
426
+ ...(requiredStreamConfig?.subjects ?? []),
427
+ ]);
428
+
429
+ const streamInfo = await this.jsm?.streams.update(stream.config.name, {
430
+ ...stream.config,
431
+ ...requiredStreamConfig,
432
+ subjects: [...streamSubjects.keys()],
433
+ });
434
+
435
+ this.logger.info(`Stream ${streamInfo?.config.name} updated`);
436
+ } else {
437
+ const streamInfo = await this.jsm?.streams.add(requiredStreamConfig);
438
+
439
+ this.logger.info(`Stream ${streamInfo?.config.name} created`);
440
+ }
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Получает имя потока для десериализатора.
446
+ * @param contract - Десериализатор событий.
447
+ * @returns Имя потока.
448
+ */
449
+ private getStreamName(contract: EventTypeWithOptions): string {
450
+ return contract.options?.streamName ?? getStreamName(contract.eventType);
451
+ }
452
+
453
+ /**
454
+ * Получает имя консьюмера по десериализатору.
455
+ * @param contract - Десериализатор событий.
456
+ * @returns Имя консьюмера.
457
+ */
458
+ private getConsumerName(contract: EventTypeWithOptions): string {
459
+ const arr = [
460
+ this.normalizedApplicationName,
461
+ this.getStreamName(contract),
462
+ 'consumer',
463
+ ];
464
+
465
+ if (contract.options?.consumerName) {
466
+ arr.unshift(contract.options.consumerName);
467
+ }
468
+
469
+ return arr.join('-');
470
+ }
471
+
472
+ /**
473
+ * Получает темы событий для десериализатора.
474
+ * @param contract - Десериализатор событий.
475
+ * @returns Темы событий.
476
+ */
477
+ private getEventSubjects(contract: EventTypeWithOptions): string[] {
478
+ return contract.options?.subjects ?? [];
479
+ }
480
+
481
+ /**
482
+ * Получает часть типа события.
483
+ * @param eventType - Тип события.
484
+ * @returns Часть типа.
485
+ */
486
+ private getTypePart(eventType: EventType | string): string {
487
+ return (
488
+ typeof eventType === 'string' ? eventType : eventType.$type
489
+ ).replaceAll('.', '-');
490
+ }
491
+ }
@@ -0,0 +1,6 @@
1
+ export type ConsumeOptions = {
2
+ streamName?: string;
3
+ subjects?: string[];
4
+ maxAckPending?: number;
5
+ consumerName?: string;
6
+ };
@@ -0,0 +1,6 @@
1
+ import type { Mapping } from './mapping.type';
2
+
3
+ export type ConsumerInfo = {
4
+ streamName: string;
5
+ mapping: Mapping;
6
+ };
@@ -0,0 +1,3 @@
1
+ import type { ConsumerInfo } from './consumer-info.type';
2
+
3
+ export type ConsumersMap = Map<string, ConsumerInfo>;
@@ -0,0 +1,8 @@
1
+ import type { EventType } from '@rsdk/events.common';
2
+
3
+ import type { ConsumeOptions } from './consume.options';
4
+
5
+ export type EventTypeWithOptions = {
6
+ eventType: EventType;
7
+ options?: ConsumeOptions;
8
+ };
@@ -0,0 +1,8 @@
1
+ import type { MessageHandler } from '@nestjs/microservices';
2
+
3
+ import type { EventTypeWithOptions } from './event-type-with-options.type';
4
+
5
+ export type Mapping = Map<
6
+ string,
7
+ { deserializer: EventTypeWithOptions; handler: MessageHandler }
8
+ >;
@@ -0,0 +1,3 @@
1
+ import type { NatsJetStreamServerOptions } from '@nestjs-plugins/nestjs-nats-jetstream-transport';
2
+
3
+ export type NatsJetstreamTransportOptions = Partial<NatsJetStreamServerOptions>;
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": [
4
+ "node_modules",
5
+ "dist",
6
+ "test",
7
+ "**/*.spec.ts",
8
+ "**/*.test.ts",
9
+ "**/*.test.e2e.ts",
10
+ "**/*.test.manual-e2e.ts",
11
+ ]
12
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "@rsdk/tsconfig/base.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "outDir": "dist"
6
+ }
7
+ }