@lakutata/nats 2.3.0 → 3.0.0-beta.0

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 (121) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +288 -25
  3. package/dist/cjs/codecs/JSONCodec.d.ts +32 -0
  4. package/dist/cjs/codecs/JSONCodec.js +44 -0
  5. package/dist/cjs/codecs/MessagePackCodec.d.ts +22 -0
  6. package/dist/cjs/codecs/MessagePackCodec.js +35 -0
  7. package/dist/cjs/components/NATS.d.ts +376 -0
  8. package/dist/cjs/components/NATS.js +772 -0
  9. package/dist/cjs/entrypoints/NatsServiceEntrypoint.d.ts +102 -0
  10. package/dist/cjs/entrypoints/NatsServiceEntrypoint.js +127 -0
  11. package/dist/cjs/exceptions/NatsBadRequestException.d.ts +11 -0
  12. package/dist/cjs/exceptions/NatsBadRequestException.js +18 -0
  13. package/dist/cjs/exceptions/NatsBulkException.d.ts +19 -0
  14. package/dist/cjs/exceptions/NatsBulkException.js +26 -0
  15. package/dist/cjs/exceptions/NatsForbiddenException.d.ts +11 -0
  16. package/dist/cjs/exceptions/NatsForbiddenException.js +18 -0
  17. package/dist/cjs/exceptions/NatsInternalServerException.d.ts +12 -0
  18. package/dist/cjs/exceptions/NatsInternalServerException.js +19 -0
  19. package/dist/cjs/exceptions/NatsNoRespondersAvailableException.d.ts +12 -0
  20. package/dist/cjs/exceptions/NatsNoRespondersAvailableException.js +19 -0
  21. package/dist/cjs/exceptions/NatsNotFoundException.d.ts +11 -0
  22. package/dist/cjs/exceptions/NatsNotFoundException.js +18 -0
  23. package/dist/cjs/exceptions/NatsRequestTimeoutException.d.ts +11 -0
  24. package/dist/cjs/exceptions/NatsRequestTimeoutException.js +18 -0
  25. package/dist/cjs/exports/Nats.d.ts +18 -0
  26. package/dist/cjs/exports/Nats.js +39 -0
  27. package/dist/cjs/interfaces/NatsClientOptions.d.ts +106 -0
  28. package/dist/{lib → cjs/lib}/ServiceEventCodec.d.ts +0 -1
  29. package/dist/{lib → cjs/lib}/ServiceEventCodec.js +19 -9
  30. package/dist/cjs/package.json +1 -0
  31. package/dist/cjs/providers/ServiceProxy.d.ts +210 -0
  32. package/dist/cjs/providers/ServiceProxy.js +274 -0
  33. package/dist/cjs/types/SubscribeOptions.d.ts +33 -0
  34. package/dist/esm/codecs/JSONCodec.js +41 -0
  35. package/dist/esm/codecs/MessagePackCodec.js +32 -0
  36. package/dist/esm/components/NATS.js +767 -0
  37. package/dist/esm/entrypoints/NatsServiceEntrypoint.js +123 -0
  38. package/dist/esm/exceptions/NatsBadRequestException.js +14 -0
  39. package/dist/esm/exceptions/NatsBulkException.js +22 -0
  40. package/dist/esm/exceptions/NatsForbiddenException.js +14 -0
  41. package/dist/esm/exceptions/NatsInternalServerException.js +15 -0
  42. package/dist/esm/exceptions/NatsNoRespondersAvailableException.js +15 -0
  43. package/dist/esm/exceptions/NatsNotFoundException.js +14 -0
  44. package/dist/esm/exceptions/NatsRequestTimeoutException.js +14 -0
  45. package/dist/esm/exports/Nats.js +15 -0
  46. package/dist/esm/interfaces/NatsClientOptions.js +1 -0
  47. package/dist/esm/lib/ServiceEventCodec.js +21 -0
  48. package/dist/esm/providers/ServiceProxy.js +270 -0
  49. package/dist/esm/types/SubscribeOptions.js +1 -0
  50. package/dist/types/codecs/JSONCodec.d.ts +32 -0
  51. package/dist/types/codecs/MessagePackCodec.d.ts +22 -0
  52. package/dist/types/components/NATS.d.ts +376 -0
  53. package/dist/types/entrypoints/NatsServiceEntrypoint.d.ts +102 -0
  54. package/dist/types/exceptions/NatsBadRequestException.d.ts +11 -0
  55. package/dist/types/exceptions/NatsBulkException.d.ts +19 -0
  56. package/dist/types/exceptions/NatsForbiddenException.d.ts +11 -0
  57. package/dist/types/exceptions/NatsInternalServerException.d.ts +12 -0
  58. package/dist/types/exceptions/NatsNoRespondersAvailableException.d.ts +12 -0
  59. package/dist/types/exceptions/NatsNotFoundException.d.ts +11 -0
  60. package/dist/types/exceptions/NatsRequestTimeoutException.d.ts +11 -0
  61. package/dist/types/exports/Nats.d.ts +18 -0
  62. package/dist/types/interfaces/NatsClientOptions.d.ts +106 -0
  63. package/dist/types/lib/ServiceEventCodec.d.ts +9 -0
  64. package/dist/types/providers/ServiceProxy.d.ts +210 -0
  65. package/dist/types/types/SubscribeOptions.d.ts +33 -0
  66. package/package.json +42 -33
  67. package/CHANGELOG.md +0 -288
  68. package/dist/CommonExports.d.ts +0 -19
  69. package/dist/CommonExports.d.ts.map +0 -1
  70. package/dist/CommonExports.js +0 -35
  71. package/dist/codecs/JSONCodec.d.ts +0 -3
  72. package/dist/codecs/JSONCodec.d.ts.map +0 -1
  73. package/dist/codecs/JSONCodec.js +0 -15
  74. package/dist/codecs/MessagePackCodec.d.ts +0 -3
  75. package/dist/codecs/MessagePackCodec.d.ts.map +0 -1
  76. package/dist/codecs/MessagePackCodec.js +0 -16
  77. package/dist/components/NATS.d.ts +0 -252
  78. package/dist/components/NATS.d.ts.map +0 -1
  79. package/dist/components/NATS.js +0 -564
  80. package/dist/entrypoints/SetupNatsServiceEntrypoint.d.ts +0 -8
  81. package/dist/entrypoints/SetupNatsServiceEntrypoint.d.ts.map +0 -1
  82. package/dist/entrypoints/SetupNatsServiceEntrypoint.js +0 -41
  83. package/dist/exceptions/NatsBadRequestException.d.ts +0 -7
  84. package/dist/exceptions/NatsBadRequestException.d.ts.map +0 -1
  85. package/dist/exceptions/NatsBadRequestException.js +0 -13
  86. package/dist/exceptions/NatsBulkException.d.ts +0 -11
  87. package/dist/exceptions/NatsBulkException.d.ts.map +0 -1
  88. package/dist/exceptions/NatsBulkException.js +0 -17
  89. package/dist/exceptions/NatsForbiddenException.d.ts +0 -7
  90. package/dist/exceptions/NatsForbiddenException.d.ts.map +0 -1
  91. package/dist/exceptions/NatsForbiddenException.js +0 -13
  92. package/dist/exceptions/NatsInternalServerException.d.ts +0 -7
  93. package/dist/exceptions/NatsInternalServerException.d.ts.map +0 -1
  94. package/dist/exceptions/NatsInternalServerException.js +0 -13
  95. package/dist/exceptions/NatsNoRespondersAvailableException.d.ts +0 -7
  96. package/dist/exceptions/NatsNoRespondersAvailableException.d.ts.map +0 -1
  97. package/dist/exceptions/NatsNoRespondersAvailableException.js +0 -13
  98. package/dist/exceptions/NatsNotFoundException.d.ts +0 -7
  99. package/dist/exceptions/NatsNotFoundException.d.ts.map +0 -1
  100. package/dist/exceptions/NatsNotFoundException.js +0 -13
  101. package/dist/exceptions/NatsRequestTimeoutException.d.ts +0 -7
  102. package/dist/exceptions/NatsRequestTimeoutException.d.ts.map +0 -1
  103. package/dist/exceptions/NatsRequestTimeoutException.js +0 -13
  104. package/dist/exceptions/ServiceInvokeException.d.ts +0 -6
  105. package/dist/exceptions/ServiceInvokeException.d.ts.map +0 -1
  106. package/dist/exceptions/ServiceInvokeException.js +0 -11
  107. package/dist/interfaces/NatsClientOptions.d.ts +0 -26
  108. package/dist/interfaces/NatsClientOptions.d.ts.map +0 -1
  109. package/dist/lib/ServiceEventCodec.d.ts.map +0 -1
  110. package/dist/lib/ServiceResponseCodec.d.ts +0 -22
  111. package/dist/lib/ServiceResponseCodec.d.ts.map +0 -1
  112. package/dist/lib/ServiceResponseCodec.js +0 -50
  113. package/dist/providers/ServiceProxy.d.ts +0 -85
  114. package/dist/providers/ServiceProxy.d.ts.map +0 -1
  115. package/dist/providers/ServiceProxy.js +0 -124
  116. package/dist/types/SubscribeOptions.d.ts +0 -12
  117. package/dist/types/SubscribeOptions.d.ts.map +0 -1
  118. package/docs/large-payload-design.md +0 -199
  119. package/tsconfig.json +0 -8
  120. /package/dist/{interfaces → cjs/interfaces}/NatsClientOptions.js +0 -0
  121. /package/dist/{types → cjs/types}/SubscribeOptions.js +0 -0
@@ -0,0 +1,123 @@
1
+ import { Application } from '@lakutata/core';
2
+ import { ServiceContext } from '@lakutata/entrypoint';
3
+ import { ServiceChannel, ServiceResponseCodec } from '@lakutata/entrypoint/channel';
4
+ import { ServiceEventCodec } from '../lib/ServiceEventCodec.js';
5
+ /**
6
+ * Create a service adapter of an entrypoint served over NATS: the requests sent to the application's id (by the
7
+ * {@link ServiceProxy} components of the other applications, or any NATS client) are dispatched to the controllers'
8
+ * service actions, through the {@link NATS} component registered under `natsComponentName`.
9
+ *
10
+ * A request's payload is the message matched against the `@ServiceAction` patterns: the action's pattern and its input
11
+ * in one object. It is answered with a service response `{success, error, payload}` (see `ServiceResponseCodec`):
12
+ * `payload` is the action's result, or `error` the properties of the error when the action failed (no matching action,
13
+ * validation, access denied…). A request whose payload cannot be decoded, or whose response cannot be sent, is answered
14
+ * with such an error too, so the requester does not wait for its timeout. A published message (without reply subject)
15
+ * runs the action without answer.
16
+ *
17
+ * The instances of the service share the requests through a queue group: each request is handled by one of them. Large
18
+ * requests and responses go through the NATS component's bulk channel. `Entrypoint.broadcast()` publishes its events
19
+ * under the entrypoint's subject, to the proxies of the service (it counts them 0: NATS does not tell how many receive
20
+ * them); `Entrypoint.notify()` does not reach it (NATS has no client ids). When the entrypoint stops, the subscription is drained: no request is received any more
21
+ * (the callers get no responder and retry), the requests in progress are answered.
22
+ * @param natsComponentName The name of the NATS component, as registered in the components; the entrypoint fails to
23
+ * start when it is not registered.
24
+ * @param options The subject, the queue group, the adapter name and its middlewares.
25
+ * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
26
+ * @example
27
+ * ```typescript
28
+ * import {Application, DTO} from 'lakutata'
29
+ * import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
30
+ * import {ServiceAction} from 'lakutata/decorator/ctrl'
31
+ * import {Expect} from 'lakutata/decorator/dto'
32
+ * import {buildNatsClientOptions, natsServiceEntrypoint} from '@lakutata/nats'
33
+ *
34
+ * class GetOrderDTO extends DTO {
35
+ * @Expect(DTO.Number().integer().required())
36
+ * public id!: number
37
+ * }
38
+ *
39
+ * class OrderController extends Controller {
40
+ * //Invoked by orderService.invoke({cmd: 'order.get', id: 1}) in the other applications
41
+ * @ServiceAction({cmd: 'order.get'}, GetOrderDTO)
42
+ * public async get(input: GetOrderDTO): Promise<{ id: number, total: number }> {
43
+ * return {id: input.id, total: 42}
44
+ * }
45
+ * }
46
+ *
47
+ * Application.run({
48
+ * //The subject of the requests: the serviceId of the proxies
49
+ * id: 'order.service',
50
+ * name: 'OrderService',
51
+ * components: {
52
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'}),
53
+ * entrypoint: buildEntrypoints({
54
+ * controllers: [OrderController],
55
+ * service: natsServiceEntrypoint('nats')
56
+ * })
57
+ * },
58
+ * bootstrap: ['entrypoint']
59
+ * })
60
+ * ```
61
+ */
62
+ export function natsServiceEntrypoint(natsComponentName, options = {}) {
63
+ let subscription;
64
+ let nats;
65
+ let subject;
66
+ return {
67
+ channel: 'service',
68
+ definition: ServiceChannel,
69
+ name: options.name,
70
+ middlewares: options.middlewares,
71
+ start: async (runtime) => {
72
+ nats = await runtime.module.getObject(natsComponentName);
73
+ subject = options.subject ?? Application.appId;
74
+ subscription = nats.subscribe(subject, async (payload) => {
75
+ try {
76
+ return ServiceResponseCodec.encode(await runtime.dispatch(new ServiceContext({ data: payload })), false);
77
+ }
78
+ catch (e) {
79
+ return ServiceResponseCodec.encodeError(e);
80
+ }
81
+ }, {
82
+ queue: options.queue ?? `${subject}.serviceQueue`,
83
+ //The failures outside of the dispatch (a payload which cannot be decoded, a response which cannot be encoded)
84
+ //are answered too, so the requester does not wait for its timeout
85
+ errorResponse: (error) => ServiceResponseCodec.encodeError(error)
86
+ });
87
+ },
88
+ //The events published under the subject of the entrypoint, to the proxies of its service (their serviceId); NATS
89
+ //does not tell how many receive them: counted 0. NATS has no client ids: notify() pushes to none
90
+ broadcast: (event, args) => {
91
+ if (nats && subject)
92
+ nats.publish(ServiceEventCodec.formatSubject(subject, event), ServiceEventCodec.encode(args));
93
+ return 0;
94
+ },
95
+ stop: async () => {
96
+ //No request is received any more, the requests in progress are awaited by the entrypoint
97
+ try {
98
+ await subscription?.drain();
99
+ }
100
+ catch {
101
+ //The connection is already closed
102
+ }
103
+ }
104
+ };
105
+ }
106
+ /**
107
+ * The former name of {@link natsServiceEntrypoint}, kept for the applications written for 2.x; it behaves the same.
108
+ * @param natsComponentName The name of the NATS component, as registered in the components.
109
+ * @param options The subject, the queue group, the adapter name and its middlewares.
110
+ * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
111
+ * @example
112
+ * ```typescript
113
+ * import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
114
+ * import {setupNatsServiceEntrypoint} from '@lakutata/nats'
115
+ *
116
+ * class OrderController extends Controller {
117
+ * }
118
+ *
119
+ * //The same as natsServiceEntrypoint('nats')
120
+ * export const entrypoint = buildEntrypoints({controllers: [OrderController], service: setupNatsServiceEntrypoint('nats')})
121
+ * ```
122
+ */
123
+ export const setupNatsServiceEntrypoint = natsServiceEntrypoint;
@@ -0,0 +1,14 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by `NATS.request()` (and so `ServiceProxy.invoke()`) when the request fails with an error of the NATS client
4
+ * whose code is `400` (a bad request). The client reports such codes for some JetStream operations, not for the core
5
+ * requests the component sends, so it is rarely seen. Its `errno` is `E_NATS_BAD_REQUEST`, its `statusCode` 400.
6
+ */
7
+ export class NatsBadRequestException extends Exception {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.errno = 'E_NATS_BAD_REQUEST';
11
+ this.message = 'Bad Request';
12
+ this.statusCode = 400;
13
+ }
14
+ }
@@ -0,0 +1,22 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * A failure of the bulk channel, which relays the large payloads through a JetStream object store; its message tells
4
+ * the cause:
5
+ * - `bulk channel is not enabled`: `putBulk()` or `getBulk()` called, or a reference received, while the channel is
6
+ * disabled (the `bulk` option is `false`, or the channel failed to start);
7
+ * - `bulk put failed: …` / `bulk get failed: …`: the object store rejected the write or the read;
8
+ * - `bulk object not found: <id>`: the referenced object is gone (its time to live passed) or is in another bucket
9
+ * (the peers do not share their `bulkBucket`).
10
+ *
11
+ * `NATS.request()` and `ServiceProxy.invoke()` throw it when the large request cannot be stored or the large response
12
+ * cannot be read; `ServiceProxy.invoke()` never retries it, since the service may have processed the request. On the
13
+ * service side, the failure is answered to the requester, which receives a `ServiceInvokeException` whose `errno` is
14
+ * `E_NATS_BULK`. Its `errno` is `E_NATS_BULK`, its `statusCode` 502.
15
+ */
16
+ export class NatsBulkException extends Exception {
17
+ constructor() {
18
+ super(...arguments);
19
+ this.errno = 'E_NATS_BULK';
20
+ this.statusCode = 502;
21
+ }
22
+ }
@@ -0,0 +1,14 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by `NATS.request()` (and so `ServiceProxy.invoke()`) when the request fails with an error of the NATS client
4
+ * whose code is `403` (a forbidden request). The client reports such codes for some JetStream operations, not for the
5
+ * core requests the component sends, so it is rarely seen. Its `errno` is `E_NATS_FORBIDDEN`, its `statusCode` 403.
6
+ */
7
+ export class NatsForbiddenException extends Exception {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.errno = 'E_NATS_FORBIDDEN';
11
+ this.message = 'Forbidden';
12
+ this.statusCode = 403;
13
+ }
14
+ }
@@ -0,0 +1,15 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by `NATS.request()` (and so `ServiceProxy.invoke()`) when the request fails with an error of the NATS client
4
+ * whose code is `500` (an internal error of the server). The client reports such codes for some JetStream operations,
5
+ * not for the core requests the component sends, so it is rarely seen. Its `errno` is `E_NATS_INTERNAL_SERVER`, its
6
+ * `statusCode` 500.
7
+ */
8
+ export class NatsInternalServerException extends Exception {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.errno = 'E_NATS_INTERNAL_SERVER';
12
+ this.message = 'Internal Server Error';
13
+ this.statusCode = 500;
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by `NATS.request()` when no subscription receives the request: nobody listens on the subject (the service is
4
+ * not running, or is stopping). It is thrown at once, without waiting for the timeout, and the request was not
5
+ * processed: `ServiceProxy.invoke()` retries it (`maxRetries` times) before throwing it. Its `errno` is
6
+ * `E_NATS_NO_RESPONDERS_AVAILABLE`, its `statusCode` 503.
7
+ */
8
+ export class NatsNoRespondersAvailableException extends Exception {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.errno = 'E_NATS_NO_RESPONDERS_AVAILABLE';
12
+ this.message = 'No Responders Available';
13
+ this.statusCode = 503;
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by `NATS.request()` (and so `ServiceProxy.invoke()`) when the request fails with an error of the NATS client
4
+ * whose code is `404` (a subject not found). The client reports such codes for some JetStream operations, not for the
5
+ * core requests the component sends, so it is rarely seen. Its `errno` is `E_NATS_NOT_FOUND`, its `statusCode` 404.
6
+ */
7
+ export class NatsNotFoundException extends Exception {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.errno = 'E_NATS_NOT_FOUND';
11
+ this.message = 'Subject Not Found';
12
+ this.statusCode = 404;
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ import { Exception } from '@lakutata/core';
2
+ /**
3
+ * Thrown by `NATS.request()` (and so `ServiceProxy.invoke()`) when no response arrives within the request's timeout.
4
+ * The request may have been received and processed, its response lost or late: it is never retried. Its `errno` is
5
+ * `E_NATS_REQUEST_TIMEOUT`, its `statusCode` 408.
6
+ */
7
+ export class NatsRequestTimeoutException extends Exception {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.errno = 'E_NATS_REQUEST_TIMEOUT';
11
+ this.message = 'Request Timeout';
12
+ this.statusCode = 408;
13
+ }
14
+ }
@@ -0,0 +1,15 @@
1
+ export { natsServiceEntrypoint, setupNatsServiceEntrypoint } from '../entrypoints/NatsServiceEntrypoint.js';
2
+ export { ServiceProxy, buildServiceProxy } from '../providers/ServiceProxy.js';
3
+ export { RetentionPolicy } from 'nats';
4
+ export { NATS, buildNatsClientOptions } from '../components/NATS.js';
5
+ export { jsonCodec } from '../codecs/JSONCodec.js';
6
+ export { messagePackCodec } from '../codecs/MessagePackCodec.js';
7
+ export { StringCodec } from 'nats';
8
+ export { NatsBadRequestException } from '../exceptions/NatsBadRequestException.js';
9
+ export { NatsForbiddenException } from '../exceptions/NatsForbiddenException.js';
10
+ export { NatsInternalServerException } from '../exceptions/NatsInternalServerException.js';
11
+ export { NatsNoRespondersAvailableException } from '../exceptions/NatsNoRespondersAvailableException.js';
12
+ export { NatsNotFoundException } from '../exceptions/NatsNotFoundException.js';
13
+ export { NatsRequestTimeoutException } from '../exceptions/NatsRequestTimeoutException.js';
14
+ export { ServiceInvokeException } from '@lakutata/entrypoint';
15
+ export { NatsBulkException } from '../exceptions/NatsBulkException.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { md5 } from '@lakutata/utils';
2
+ import * as querystring from 'node:querystring';
3
+ export class ServiceEventCodec {
4
+ static hashEventName(serviceId, eventName) {
5
+ return md5(querystring.encode({
6
+ serviceId: serviceId,
7
+ eventName: eventName
8
+ })).toString('hex');
9
+ }
10
+ static encode(args) {
11
+ return {
12
+ args: args
13
+ };
14
+ }
15
+ static decode(eventData) {
16
+ return eventData.args;
17
+ }
18
+ static formatSubject(serviceId, eventName) {
19
+ return `framework.internal.service.event.${this.hashEventName(serviceId, eventName)}`;
20
+ }
21
+ }
@@ -0,0 +1,270 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Application, DTO, Provider } from '@lakutata/core';
11
+ import { Configurable, Inject, Singleton } from '@lakutata/core/decorator/di';
12
+ import { delay } from '@lakutata/utils';
13
+ import { ServiceResponseCodec } from '@lakutata/entrypoint/channel';
14
+ import { NatsNoRespondersAvailableException } from '../exceptions/NatsNoRespondersAvailableException.js';
15
+ /**
16
+ * Build the provider options of a {@link ServiceProxy}, to register it in the `providers` of an application under the
17
+ * name of your choice (as in 2.x); declare one per remote service. The options are validated when the proxy is created,
18
+ * not here.
19
+ * @param options The NATS component to use, the id of the remote service, and the retries of the requests nobody
20
+ * received.
21
+ * @returns The provider options (`{class: ServiceProxy, ...options}`).
22
+ * @example
23
+ * ```typescript
24
+ * import {Application} from 'lakutata'
25
+ * import {buildNatsClientOptions, buildServiceProxy, type ServiceProxy} from '@lakutata/nats'
26
+ *
27
+ * Application
28
+ * .run({
29
+ * id: 'shop.app',
30
+ * name: 'Shop',
31
+ * components: {
32
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'})
33
+ * },
34
+ * providers: {
35
+ * //The actions and the events of the application whose id is "order.service"
36
+ * orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service', maxRetries: 5})
37
+ * }
38
+ * })
39
+ * .onLaunched(async (app: Application): Promise<void> => {
40
+ * const orderService: ServiceProxy = await app.getObject('orderService')
41
+ * console.log(await orderService.invoke({cmd: 'order.get', id: 1}))
42
+ * })
43
+ * ```
44
+ */
45
+ export const buildServiceProxy = (options) => {
46
+ return {
47
+ class: ServiceProxy,
48
+ natsComponentName: options.natsComponentName,
49
+ serviceId: options.serviceId,
50
+ maxRetries: options.maxRetries,
51
+ retryDelay: options.retryDelay
52
+ };
53
+ };
54
+ /**
55
+ * The proxy of a remote service served by {@link natsServiceEntrypoint}: it invokes the service's actions and listens to
56
+ * its events, over a {@link NATS} component. Register it in the `providers` of an application with
57
+ * {@link buildServiceProxy} (as in 2.x), one per remote service, and get it with `getObject()` or `@Inject`; it is a
58
+ * singleton provider, shared by the scopes. It resolves its NATS component when it is created (which fails when that
59
+ * component is not registered).
60
+ *
61
+ * An invocation is a NATS request to the service id, answered by one instance of the service (its queue group): the
62
+ * action's result is returned, the action's failure thrown as a `ServiceInvokeException` carrying the remote error's
63
+ * properties (`errno`, `statusCode`, `message`…). A request nobody received is sent again (`maxRetries`, `retryDelay`);
64
+ * a timeout is never retried, since the action may have run. Large requests and responses go through the bulk channel
65
+ * of the NATS component (see {@link NATS}).
66
+ *
67
+ * The event listeners registered through the proxy are removed when it is destroyed.
68
+ * @example
69
+ * ```typescript
70
+ * import {Application, Component} from 'lakutata'
71
+ * import {Inject} from 'lakutata/decorator/di'
72
+ * import {buildNatsClientOptions, buildServiceProxy, ServiceInvokeException, type ServiceProxy} from '@lakutata/nats'
73
+ *
74
+ * type Order = {id: number, total: number}
75
+ *
76
+ * class Billing extends Component {
77
+ * @Inject('orderService')
78
+ * protected readonly orderService: ServiceProxy
79
+ *
80
+ * public async total(id: number): Promise<number | undefined> {
81
+ * try {
82
+ * //The action of the order service matching {cmd: 'order.get'}, its input {id}; 30 seconds at most
83
+ * const order: Order = await this.orderService.invoke({cmd: 'order.get', id: id}, 30000)
84
+ * return order.total
85
+ * } catch (error) {
86
+ * //The action failed: the remote error's errno and statusCode are copied
87
+ * if (error instanceof ServiceInvokeException && error.statusCode === 404) return undefined
88
+ * //NatsNoRespondersAvailableException (not running, after the retries), NatsRequestTimeoutException…
89
+ * throw error
90
+ * }
91
+ * }
92
+ * }
93
+ *
94
+ * Application
95
+ * .run({
96
+ * id: 'billing.app',
97
+ * name: 'Billing',
98
+ * components: {
99
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'}),
100
+ * billing: {class: Billing}
101
+ * },
102
+ * providers: {
103
+ * orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service'})
104
+ * }
105
+ * })
106
+ * .onLaunched(async (app: Application): Promise<void> => {
107
+ * const orderService: ServiceProxy = await app.getObject('orderService')
108
+ * //The events emitted by the order service with NATS.emitServiceEvent('orderPaid', id, total)
109
+ * orderService.on('orderPaid', (id: number, total: number): void => console.log(`Order ${id} paid: ${total}`))
110
+ * const billing: Billing = await app.getObject('billing')
111
+ * console.log(await billing.total(1))
112
+ * })
113
+ * ```
114
+ */
115
+ let ServiceProxy = class ServiceProxy extends Provider {
116
+ constructor() {
117
+ super(...arguments);
118
+ /**
119
+ * The event listeners registered through this proxy, by event name, removed when it is destroyed.
120
+ */
121
+ this.eventListeners = new Map();
122
+ }
123
+ /**
124
+ * Resolve the NATS component.
125
+ * @throws {Error} When no component is registered under `natsComponentName`.
126
+ */
127
+ async init() {
128
+ this.nats = await this.app.getObject(this.natsComponentName);
129
+ }
130
+ /**
131
+ * Remove the event listeners registered through this proxy.
132
+ */
133
+ async destroy() {
134
+ this.eventListeners.forEach((listeners, eventName) => listeners.forEach((wrapper) => this.nats.offServiceEvent(this.serviceId, eventName, wrapper)));
135
+ this.eventListeners.clear();
136
+ }
137
+ /**
138
+ * Record an event listener registered through this proxy, to remove it when the proxy is destroyed.
139
+ * @param eventName The event name.
140
+ * @param listener The listener.
141
+ */
142
+ keepListener(eventName, listener) {
143
+ const listeners = this.eventListeners.get(eventName) ?? new Map();
144
+ this.eventListeners.set(eventName, listeners);
145
+ //Added once
146
+ if (listeners.has(listener))
147
+ return undefined;
148
+ //A wrapper of this proxy: the NATS component shared by the proxies keeps their listeners apart, the same function
149
+ //added by two proxies included
150
+ const wrapper = (...args) => listener(...args);
151
+ listeners.set(listener, wrapper);
152
+ return wrapper;
153
+ }
154
+ /**
155
+ * Invoke an action of the remote service and return its result.
156
+ * @param input The message: the action's pattern (as declared by its `@ServiceAction`) and its input, in one object
157
+ * (`{cmd: 'order.get', id: 1}`). It is encoded with the NATS component's codec (plain JSON data).
158
+ * @param timeout The time to wait for the response of each attempt, in milliseconds. A missing service fails at once
159
+ * (no responder), so it only bounds a request received and processed too slowly; `0` waits 600000 (10 minutes).
160
+ * @returns The action's result (the data of a `StatusResult`).
161
+ * @throws {ServiceInvokeException} When the action fails, or no action matches: its message is the remote error's, its
162
+ * `errno`, `statusCode` and other properties are copied from it, its `service` is the service id.
163
+ * @throws {NatsNoRespondersAvailableException} When nobody received the request, after `maxRetries` retries.
164
+ * @throws {NatsRequestTimeoutException} When no response arrives in time (never retried: the action may have run).
165
+ * @throws {NatsBulkException} When a large request cannot be stored, or a large response cannot be read.
166
+ * @throws {Error} The NATS client error `MAX_PAYLOAD_EXCEEDED` when a request too large is not relayed (see
167
+ * {@link NATS.request}), `CONNECTION_CLOSED` once the connection is closed.
168
+ */
169
+ async invoke(input, timeout = 10 * 60 * 1000) {
170
+ //Only a request nobody received is retried: no instance processed it, so the retry is its first processing.
171
+ //A timeout (the request may be processed and its response lost) and a failure of the service (processed) are thrown,
172
+ //a non-idempotent action is never run twice
173
+ for (let attempt = 0;; attempt++) {
174
+ try {
175
+ const response = await this.nats.request(this.serviceId, input, timeout);
176
+ return ServiceResponseCodec.decode(response, this.serviceId);
177
+ }
178
+ catch (e) {
179
+ if (e instanceof NatsNoRespondersAvailableException && attempt < this.maxRetries) {
180
+ await delay(this.retryDelay);
181
+ continue;
182
+ }
183
+ throw e;
184
+ }
185
+ }
186
+ }
187
+ /**
188
+ * Listen to an event of the remote service, emitted by its NATS component with `emitServiceEvent()`. A listener
189
+ * registered again for the same event on the same NATS component is ignored.
190
+ * @param eventName The event name.
191
+ * @param listener The listener, receiving the event's arguments.
192
+ * @param onlySingleClientReceived Whether each event goes to one of the listeners registered with `true` for it, across
193
+ * all the applications (a queue group, such as the instances of one service), instead of to each of them.
194
+ * @returns The proxy, for chaining.
195
+ */
196
+ on(eventName, listener, onlySingleClientReceived = false) {
197
+ const wrapper = this.keepListener(eventName, listener);
198
+ if (wrapper)
199
+ this.nats.onServiceEvent(this.serviceId, eventName, wrapper, onlySingleClientReceived);
200
+ return this;
201
+ }
202
+ /**
203
+ * Listen to the next occurrence of an event of the remote service only.
204
+ * @param eventName The event name.
205
+ * @param listener The listener, receiving the event's arguments.
206
+ * @returns The proxy, for chaining.
207
+ */
208
+ once(eventName, listener) {
209
+ if (!this.keepListener(eventName, listener))
210
+ return this;
211
+ //The function registered is the one kept: off() removes it
212
+ const registered = (...args) => {
213
+ this.eventListeners.get(eventName)?.delete(listener);
214
+ return listener(...args);
215
+ };
216
+ this.eventListeners.get(eventName).set(listener, registered);
217
+ this.nats.onceServiceEvent(this.serviceId, eventName, registered);
218
+ return this;
219
+ }
220
+ /**
221
+ * Stop listening to an event of the remote service.
222
+ * @param eventName The event name.
223
+ * @param listener The listener to remove; without it, every listener of the event added through this proxy (the
224
+ * listeners added through the other proxies or `NATS.onServiceEvent()` are kept).
225
+ * @returns The proxy, for chaining.
226
+ */
227
+ off(eventName, listener) {
228
+ //This proxy's listeners only: the other proxies of the NATS component keep theirs
229
+ const listeners = this.eventListeners.get(eventName);
230
+ if (!listeners)
231
+ return this;
232
+ for (const [registered, wrapper] of [...listeners]) {
233
+ if (listener && registered !== listener)
234
+ continue;
235
+ this.nats.offServiceEvent(this.serviceId, eventName, wrapper);
236
+ listeners.delete(registered);
237
+ }
238
+ if (!listeners.size)
239
+ this.eventListeners.delete(eventName);
240
+ return this;
241
+ }
242
+ };
243
+ __decorate([
244
+ Inject(Application),
245
+ __metadata("design:type", Application
246
+ /**
247
+ * The name of the NATS component the proxy uses.
248
+ */
249
+ )
250
+ ], ServiceProxy.prototype, "app", void 0);
251
+ __decorate([
252
+ Configurable(DTO.String().required()),
253
+ __metadata("design:type", String)
254
+ ], ServiceProxy.prototype, "natsComponentName", void 0);
255
+ __decorate([
256
+ Configurable(DTO.String().required()),
257
+ __metadata("design:type", String)
258
+ ], ServiceProxy.prototype, "serviceId", void 0);
259
+ __decorate([
260
+ Configurable(DTO.Number().integer().optional().default(3)),
261
+ __metadata("design:type", Number)
262
+ ], ServiceProxy.prototype, "maxRetries", void 0);
263
+ __decorate([
264
+ Configurable(DTO.Number().integer().optional().default(100)),
265
+ __metadata("design:type", Number)
266
+ ], ServiceProxy.prototype, "retryDelay", void 0);
267
+ ServiceProxy = __decorate([
268
+ Singleton()
269
+ ], ServiceProxy);
270
+ export { ServiceProxy };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { Codec } from 'nats';
2
+ /**
3
+ * Create the JSON codec of the NATS messages, for the `codec` option of {@link buildNatsClientOptions}. A value is sent
4
+ * as the UTF-8 JSON text of a one-element array (`{id: 1}` is sent as `[{"id":1}]`), so that every value, `null` and
5
+ * the strings included, has a JSON form; the peers written in other languages wrap and unwrap that array.
6
+ *
7
+ * The values go through `JSON.stringify()`: a `Date` arrives as its ISO string, a `Buffer` as
8
+ * `{type: 'Buffer', data: [...]}`, a class instance as a plain object, `undefined` as `null` (and is dropped from the
9
+ * objects), `NaN` and `Infinity` as `null`; a `bigint` or a circular value fails the encoding with a `TypeError`.
10
+ * Decoding bytes which are not such JSON throws a `SyntaxError`. Both sides of a subject must use the same codec.
11
+ * @returns The codec.
12
+ * @example
13
+ * ```typescript
14
+ * import {Application} from 'lakutata'
15
+ * import {buildNatsClientOptions, jsonCodec} from '@lakutata/nats'
16
+ *
17
+ * Application.run({
18
+ * id: 'json.app',
19
+ * name: 'JSON',
20
+ * components: {
21
+ * //Readable messages, for peers which do not speak MessagePack
22
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222', codec: jsonCodec()})
23
+ * }
24
+ * })
25
+ *
26
+ * const codec = jsonCodec()
27
+ * const bytes: Uint8Array = codec.encode({id: 1, at: new Date(0)})
28
+ * console.log(Buffer.from(bytes).toString()) //[{"id":1,"at":"1970-01-01T00:00:00.000Z"}]
29
+ * console.log(codec.decode(bytes)) //{id: 1, at: '1970-01-01T00:00:00.000Z'}
30
+ * ```
31
+ */
32
+ export declare function jsonCodec(): Codec<any>;
@@ -0,0 +1,22 @@
1
+ import { Codec } from 'nats';
2
+ /**
3
+ * Create the MessagePack codec of the NATS messages, the default `codec` of the {@link NATS} component. A value is sent
4
+ * as the MessagePack encoding of a one-element array holding it, smaller and faster to decode than JSON.
5
+ *
6
+ * The value is first normalized through JSON (`JSON.parse(JSON.stringify(value))`), so that the peers written in other
7
+ * languages receive plain JSON data: a `Date` arrives as its ISO string, a `Buffer` as `{type: 'Buffer', data: [...]}`
8
+ * (the binary data is not kept as binary), a class instance as a plain object, `undefined` as `null` (and is dropped
9
+ * from the objects); a `bigint` or a circular value fails the encoding with a `TypeError`. Decoding bytes which are not
10
+ * MessagePack throws. Both sides of a subject must use the same codec.
11
+ * @returns The codec.
12
+ * @example
13
+ * ```typescript
14
+ * import {messagePackCodec} from '@lakutata/nats'
15
+ *
16
+ * const codec = messagePackCodec()
17
+ * const bytes: Uint8Array = codec.encode({id: 1, tags: ['a', 'b']})
18
+ * console.log(codec.decode(bytes)) //{id: 1, tags: ['a', 'b']}
19
+ * console.log(codec.decode(codec.encode(undefined))) //null
20
+ * ```
21
+ */
22
+ export declare function messagePackCodec(): Codec<any>;