@lakutata/nats 2.2.2 → 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 -277
  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 -250
  78. package/dist/components/NATS.d.ts.map +0 -1
  79. package/dist/components/NATS.js +0 -562
  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
@@ -15,20 +15,30 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
25
35
  Object.defineProperty(exports, "__esModule", { value: true });
26
36
  exports.ServiceEventCodec = void 0;
27
- const helper_1 = require("lakutata/helper");
37
+ const utils_1 = require("@lakutata/utils");
28
38
  const querystring = __importStar(require("node:querystring"));
29
39
  class ServiceEventCodec {
30
40
  static hashEventName(serviceId, eventName) {
31
- return (0, helper_1.MD5)(querystring.encode({
41
+ return (0, utils_1.md5)(querystring.encode({
32
42
  serviceId: serviceId,
33
43
  eventName: eventName
34
44
  })).toString('hex');
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,210 @@
1
+ import { ActionPattern, Application, Provider, type ProviderOptionsBuilder } from '@lakutata/core';
2
+ import { NATS } from '../components/NATS.js';
3
+ /**
4
+ * The options of a {@link ServiceProxy}, given to {@link buildServiceProxy}.
5
+ */
6
+ export type BuildServiceProxyOptions = {
7
+ /**
8
+ * The name of the {@link NATS} component the proxy sends its requests through, as registered in the components.
9
+ */
10
+ readonly natsComponentName: string;
11
+ /**
12
+ * The id of the remote service: the subject its {@link natsServiceEntrypoint} listens on, which is its application
13
+ * id (its `id` option) unless the entrypoint's `subject` option changes it. Its events are listened to under this id
14
+ * too, which only matches the application id.
15
+ */
16
+ readonly serviceId: string;
17
+ /**
18
+ * The number of times a request nobody received (no responder: the service is not running or is restarting) is
19
+ * sent again before {@link NatsNoRespondersAvailableException} is thrown. The timeouts and the failures of the
20
+ * service are never retried, since the request may have been processed.
21
+ * @default 3
22
+ */
23
+ readonly maxRetries?: number;
24
+ /**
25
+ * The delay before each retry, in milliseconds.
26
+ * @default 100
27
+ */
28
+ readonly retryDelay?: number;
29
+ };
30
+ /**
31
+ * Build the provider options of a {@link ServiceProxy}, to register it in the `providers` of an application under the
32
+ * name of your choice (as in 2.x); declare one per remote service. The options are validated when the proxy is created,
33
+ * not here.
34
+ * @param options The NATS component to use, the id of the remote service, and the retries of the requests nobody
35
+ * received.
36
+ * @returns The provider options (`{class: ServiceProxy, ...options}`).
37
+ * @example
38
+ * ```typescript
39
+ * import {Application} from 'lakutata'
40
+ * import {buildNatsClientOptions, buildServiceProxy, type ServiceProxy} from '@lakutata/nats'
41
+ *
42
+ * Application
43
+ * .run({
44
+ * id: 'shop.app',
45
+ * name: 'Shop',
46
+ * components: {
47
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'})
48
+ * },
49
+ * providers: {
50
+ * //The actions and the events of the application whose id is "order.service"
51
+ * orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service', maxRetries: 5})
52
+ * }
53
+ * })
54
+ * .onLaunched(async (app: Application): Promise<void> => {
55
+ * const orderService: ServiceProxy = await app.getObject('orderService')
56
+ * console.log(await orderService.invoke({cmd: 'order.get', id: 1}))
57
+ * })
58
+ * ```
59
+ */
60
+ export declare const buildServiceProxy: ProviderOptionsBuilder<BuildServiceProxyOptions>;
61
+ /**
62
+ * The proxy of a remote service served by {@link natsServiceEntrypoint}: it invokes the service's actions and listens to
63
+ * its events, over a {@link NATS} component. Register it in the `providers` of an application with
64
+ * {@link buildServiceProxy} (as in 2.x), one per remote service, and get it with `getObject()` or `@Inject`; it is a
65
+ * singleton provider, shared by the scopes. It resolves its NATS component when it is created (which fails when that
66
+ * component is not registered).
67
+ *
68
+ * An invocation is a NATS request to the service id, answered by one instance of the service (its queue group): the
69
+ * action's result is returned, the action's failure thrown as a `ServiceInvokeException` carrying the remote error's
70
+ * properties (`errno`, `statusCode`, `message`…). A request nobody received is sent again (`maxRetries`, `retryDelay`);
71
+ * a timeout is never retried, since the action may have run. Large requests and responses go through the bulk channel
72
+ * of the NATS component (see {@link NATS}).
73
+ *
74
+ * The event listeners registered through the proxy are removed when it is destroyed.
75
+ * @example
76
+ * ```typescript
77
+ * import {Application, Component} from 'lakutata'
78
+ * import {Inject} from 'lakutata/decorator/di'
79
+ * import {buildNatsClientOptions, buildServiceProxy, ServiceInvokeException, type ServiceProxy} from '@lakutata/nats'
80
+ *
81
+ * type Order = {id: number, total: number}
82
+ *
83
+ * class Billing extends Component {
84
+ * @Inject('orderService')
85
+ * protected readonly orderService: ServiceProxy
86
+ *
87
+ * public async total(id: number): Promise<number | undefined> {
88
+ * try {
89
+ * //The action of the order service matching {cmd: 'order.get'}, its input {id}; 30 seconds at most
90
+ * const order: Order = await this.orderService.invoke({cmd: 'order.get', id: id}, 30000)
91
+ * return order.total
92
+ * } catch (error) {
93
+ * //The action failed: the remote error's errno and statusCode are copied
94
+ * if (error instanceof ServiceInvokeException && error.statusCode === 404) return undefined
95
+ * //NatsNoRespondersAvailableException (not running, after the retries), NatsRequestTimeoutException…
96
+ * throw error
97
+ * }
98
+ * }
99
+ * }
100
+ *
101
+ * Application
102
+ * .run({
103
+ * id: 'billing.app',
104
+ * name: 'Billing',
105
+ * components: {
106
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'}),
107
+ * billing: {class: Billing}
108
+ * },
109
+ * providers: {
110
+ * orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service'})
111
+ * }
112
+ * })
113
+ * .onLaunched(async (app: Application): Promise<void> => {
114
+ * const orderService: ServiceProxy = await app.getObject('orderService')
115
+ * //The events emitted by the order service with NATS.emitServiceEvent('orderPaid', id, total)
116
+ * orderService.on('orderPaid', (id: number, total: number): void => console.log(`Order ${id} paid: ${total}`))
117
+ * const billing: Billing = await app.getObject('billing')
118
+ * console.log(await billing.total(1))
119
+ * })
120
+ * ```
121
+ */
122
+ export declare class ServiceProxy extends Provider {
123
+ /**
124
+ * The application the proxy belongs to, to resolve its NATS component.
125
+ */
126
+ protected readonly app: Application;
127
+ /**
128
+ * The name of the NATS component the proxy uses.
129
+ */
130
+ protected readonly natsComponentName: string;
131
+ /**
132
+ * The id of the remote service: the subject of its requests and the id of its events.
133
+ */
134
+ protected readonly serviceId: string;
135
+ /**
136
+ * The number of times a request nobody received is sent again; the timeouts and the failures of the service are
137
+ * never retried.
138
+ * @default 3
139
+ */
140
+ protected readonly maxRetries: number;
141
+ /**
142
+ * The delay before each retry, in milliseconds.
143
+ * @default 100
144
+ */
145
+ protected readonly retryDelay: number;
146
+ /**
147
+ * The NATS component, resolved when the proxy is created.
148
+ */
149
+ protected nats: NATS;
150
+ /**
151
+ * The event listeners registered through this proxy, by event name, removed when it is destroyed.
152
+ */
153
+ protected readonly eventListeners: Map<string, Map<(...args: any[]) => void, (...args: any[]) => unknown>>;
154
+ /**
155
+ * Resolve the NATS component.
156
+ * @throws {Error} When no component is registered under `natsComponentName`.
157
+ */
158
+ protected init(): Promise<void>;
159
+ /**
160
+ * Remove the event listeners registered through this proxy.
161
+ */
162
+ protected destroy(): Promise<void>;
163
+ /**
164
+ * Record an event listener registered through this proxy, to remove it when the proxy is destroyed.
165
+ * @param eventName The event name.
166
+ * @param listener The listener.
167
+ */
168
+ protected keepListener(eventName: string, listener: (...args: any[]) => void): ((...args: any[]) => unknown) | undefined;
169
+ /**
170
+ * Invoke an action of the remote service and return its result.
171
+ * @param input The message: the action's pattern (as declared by its `@ServiceAction`) and its input, in one object
172
+ * (`{cmd: 'order.get', id: 1}`). It is encoded with the NATS component's codec (plain JSON data).
173
+ * @param timeout The time to wait for the response of each attempt, in milliseconds. A missing service fails at once
174
+ * (no responder), so it only bounds a request received and processed too slowly; `0` waits 600000 (10 minutes).
175
+ * @returns The action's result (the data of a `StatusResult`).
176
+ * @throws {ServiceInvokeException} When the action fails, or no action matches: its message is the remote error's, its
177
+ * `errno`, `statusCode` and other properties are copied from it, its `service` is the service id.
178
+ * @throws {NatsNoRespondersAvailableException} When nobody received the request, after `maxRetries` retries.
179
+ * @throws {NatsRequestTimeoutException} When no response arrives in time (never retried: the action may have run).
180
+ * @throws {NatsBulkException} When a large request cannot be stored, or a large response cannot be read.
181
+ * @throws {Error} The NATS client error `MAX_PAYLOAD_EXCEEDED` when a request too large is not relayed (see
182
+ * {@link NATS.request}), `CONNECTION_CLOSED` once the connection is closed.
183
+ */
184
+ invoke<T extends Record<string, any>>(input: ActionPattern<T>, timeout?: number): Promise<any>;
185
+ /**
186
+ * Listen to an event of the remote service, emitted by its NATS component with `emitServiceEvent()`. A listener
187
+ * registered again for the same event on the same NATS component is ignored.
188
+ * @param eventName The event name.
189
+ * @param listener The listener, receiving the event's arguments.
190
+ * @param onlySingleClientReceived Whether each event goes to one of the listeners registered with `true` for it, across
191
+ * all the applications (a queue group, such as the instances of one service), instead of to each of them.
192
+ * @returns The proxy, for chaining.
193
+ */
194
+ on(eventName: string, listener: (...args: any[]) => void, onlySingleClientReceived?: boolean): this;
195
+ /**
196
+ * Listen to the next occurrence of an event of the remote service only.
197
+ * @param eventName The event name.
198
+ * @param listener The listener, receiving the event's arguments.
199
+ * @returns The proxy, for chaining.
200
+ */
201
+ once(eventName: string, listener: (...args: any[]) => void): this;
202
+ /**
203
+ * Stop listening to an event of the remote service.
204
+ * @param eventName The event name.
205
+ * @param listener The listener to remove; without it, every listener of the event added through this proxy (the
206
+ * listeners added through the other proxies or `NATS.onServiceEvent()` are kept).
207
+ * @returns The proxy, for chaining.
208
+ */
209
+ off(eventName: string, listener?: (...args: any[]) => void): this;
210
+ }
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ 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;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ServiceProxy = exports.buildServiceProxy = void 0;
13
+ const core_1 = require("@lakutata/core");
14
+ const di_1 = require("@lakutata/core/decorator/di");
15
+ const utils_1 = require("@lakutata/utils");
16
+ const channel_1 = require("@lakutata/entrypoint/channel");
17
+ const NatsNoRespondersAvailableException_js_1 = require("../exceptions/NatsNoRespondersAvailableException.js");
18
+ /**
19
+ * Build the provider options of a {@link ServiceProxy}, to register it in the `providers` of an application under the
20
+ * name of your choice (as in 2.x); declare one per remote service. The options are validated when the proxy is created,
21
+ * not here.
22
+ * @param options The NATS component to use, the id of the remote service, and the retries of the requests nobody
23
+ * received.
24
+ * @returns The provider options (`{class: ServiceProxy, ...options}`).
25
+ * @example
26
+ * ```typescript
27
+ * import {Application} from 'lakutata'
28
+ * import {buildNatsClientOptions, buildServiceProxy, type ServiceProxy} from '@lakutata/nats'
29
+ *
30
+ * Application
31
+ * .run({
32
+ * id: 'shop.app',
33
+ * name: 'Shop',
34
+ * components: {
35
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'})
36
+ * },
37
+ * providers: {
38
+ * //The actions and the events of the application whose id is "order.service"
39
+ * orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service', maxRetries: 5})
40
+ * }
41
+ * })
42
+ * .onLaunched(async (app: Application): Promise<void> => {
43
+ * const orderService: ServiceProxy = await app.getObject('orderService')
44
+ * console.log(await orderService.invoke({cmd: 'order.get', id: 1}))
45
+ * })
46
+ * ```
47
+ */
48
+ const buildServiceProxy = (options) => {
49
+ return {
50
+ class: ServiceProxy,
51
+ natsComponentName: options.natsComponentName,
52
+ serviceId: options.serviceId,
53
+ maxRetries: options.maxRetries,
54
+ retryDelay: options.retryDelay
55
+ };
56
+ };
57
+ exports.buildServiceProxy = buildServiceProxy;
58
+ /**
59
+ * The proxy of a remote service served by {@link natsServiceEntrypoint}: it invokes the service's actions and listens to
60
+ * its events, over a {@link NATS} component. Register it in the `providers` of an application with
61
+ * {@link buildServiceProxy} (as in 2.x), one per remote service, and get it with `getObject()` or `@Inject`; it is a
62
+ * singleton provider, shared by the scopes. It resolves its NATS component when it is created (which fails when that
63
+ * component is not registered).
64
+ *
65
+ * An invocation is a NATS request to the service id, answered by one instance of the service (its queue group): the
66
+ * action's result is returned, the action's failure thrown as a `ServiceInvokeException` carrying the remote error's
67
+ * properties (`errno`, `statusCode`, `message`…). A request nobody received is sent again (`maxRetries`, `retryDelay`);
68
+ * a timeout is never retried, since the action may have run. Large requests and responses go through the bulk channel
69
+ * of the NATS component (see {@link NATS}).
70
+ *
71
+ * The event listeners registered through the proxy are removed when it is destroyed.
72
+ * @example
73
+ * ```typescript
74
+ * import {Application, Component} from 'lakutata'
75
+ * import {Inject} from 'lakutata/decorator/di'
76
+ * import {buildNatsClientOptions, buildServiceProxy, ServiceInvokeException, type ServiceProxy} from '@lakutata/nats'
77
+ *
78
+ * type Order = {id: number, total: number}
79
+ *
80
+ * class Billing extends Component {
81
+ * @Inject('orderService')
82
+ * protected readonly orderService: ServiceProxy
83
+ *
84
+ * public async total(id: number): Promise<number | undefined> {
85
+ * try {
86
+ * //The action of the order service matching {cmd: 'order.get'}, its input {id}; 30 seconds at most
87
+ * const order: Order = await this.orderService.invoke({cmd: 'order.get', id: id}, 30000)
88
+ * return order.total
89
+ * } catch (error) {
90
+ * //The action failed: the remote error's errno and statusCode are copied
91
+ * if (error instanceof ServiceInvokeException && error.statusCode === 404) return undefined
92
+ * //NatsNoRespondersAvailableException (not running, after the retries), NatsRequestTimeoutException…
93
+ * throw error
94
+ * }
95
+ * }
96
+ * }
97
+ *
98
+ * Application
99
+ * .run({
100
+ * id: 'billing.app',
101
+ * name: 'Billing',
102
+ * components: {
103
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'}),
104
+ * billing: {class: Billing}
105
+ * },
106
+ * providers: {
107
+ * orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service'})
108
+ * }
109
+ * })
110
+ * .onLaunched(async (app: Application): Promise<void> => {
111
+ * const orderService: ServiceProxy = await app.getObject('orderService')
112
+ * //The events emitted by the order service with NATS.emitServiceEvent('orderPaid', id, total)
113
+ * orderService.on('orderPaid', (id: number, total: number): void => console.log(`Order ${id} paid: ${total}`))
114
+ * const billing: Billing = await app.getObject('billing')
115
+ * console.log(await billing.total(1))
116
+ * })
117
+ * ```
118
+ */
119
+ let ServiceProxy = class ServiceProxy extends core_1.Provider {
120
+ constructor() {
121
+ super(...arguments);
122
+ /**
123
+ * The event listeners registered through this proxy, by event name, removed when it is destroyed.
124
+ */
125
+ this.eventListeners = new Map();
126
+ }
127
+ /**
128
+ * Resolve the NATS component.
129
+ * @throws {Error} When no component is registered under `natsComponentName`.
130
+ */
131
+ async init() {
132
+ this.nats = await this.app.getObject(this.natsComponentName);
133
+ }
134
+ /**
135
+ * Remove the event listeners registered through this proxy.
136
+ */
137
+ async destroy() {
138
+ this.eventListeners.forEach((listeners, eventName) => listeners.forEach((wrapper) => this.nats.offServiceEvent(this.serviceId, eventName, wrapper)));
139
+ this.eventListeners.clear();
140
+ }
141
+ /**
142
+ * Record an event listener registered through this proxy, to remove it when the proxy is destroyed.
143
+ * @param eventName The event name.
144
+ * @param listener The listener.
145
+ */
146
+ keepListener(eventName, listener) {
147
+ const listeners = this.eventListeners.get(eventName) ?? new Map();
148
+ this.eventListeners.set(eventName, listeners);
149
+ //Added once
150
+ if (listeners.has(listener))
151
+ return undefined;
152
+ //A wrapper of this proxy: the NATS component shared by the proxies keeps their listeners apart, the same function
153
+ //added by two proxies included
154
+ const wrapper = (...args) => listener(...args);
155
+ listeners.set(listener, wrapper);
156
+ return wrapper;
157
+ }
158
+ /**
159
+ * Invoke an action of the remote service and return its result.
160
+ * @param input The message: the action's pattern (as declared by its `@ServiceAction`) and its input, in one object
161
+ * (`{cmd: 'order.get', id: 1}`). It is encoded with the NATS component's codec (plain JSON data).
162
+ * @param timeout The time to wait for the response of each attempt, in milliseconds. A missing service fails at once
163
+ * (no responder), so it only bounds a request received and processed too slowly; `0` waits 600000 (10 minutes).
164
+ * @returns The action's result (the data of a `StatusResult`).
165
+ * @throws {ServiceInvokeException} When the action fails, or no action matches: its message is the remote error's, its
166
+ * `errno`, `statusCode` and other properties are copied from it, its `service` is the service id.
167
+ * @throws {NatsNoRespondersAvailableException} When nobody received the request, after `maxRetries` retries.
168
+ * @throws {NatsRequestTimeoutException} When no response arrives in time (never retried: the action may have run).
169
+ * @throws {NatsBulkException} When a large request cannot be stored, or a large response cannot be read.
170
+ * @throws {Error} The NATS client error `MAX_PAYLOAD_EXCEEDED` when a request too large is not relayed (see
171
+ * {@link NATS.request}), `CONNECTION_CLOSED` once the connection is closed.
172
+ */
173
+ async invoke(input, timeout = 10 * 60 * 1000) {
174
+ //Only a request nobody received is retried: no instance processed it, so the retry is its first processing.
175
+ //A timeout (the request may be processed and its response lost) and a failure of the service (processed) are thrown,
176
+ //a non-idempotent action is never run twice
177
+ for (let attempt = 0;; attempt++) {
178
+ try {
179
+ const response = await this.nats.request(this.serviceId, input, timeout);
180
+ return channel_1.ServiceResponseCodec.decode(response, this.serviceId);
181
+ }
182
+ catch (e) {
183
+ if (e instanceof NatsNoRespondersAvailableException_js_1.NatsNoRespondersAvailableException && attempt < this.maxRetries) {
184
+ await (0, utils_1.delay)(this.retryDelay);
185
+ continue;
186
+ }
187
+ throw e;
188
+ }
189
+ }
190
+ }
191
+ /**
192
+ * Listen to an event of the remote service, emitted by its NATS component with `emitServiceEvent()`. A listener
193
+ * registered again for the same event on the same NATS component is ignored.
194
+ * @param eventName The event name.
195
+ * @param listener The listener, receiving the event's arguments.
196
+ * @param onlySingleClientReceived Whether each event goes to one of the listeners registered with `true` for it, across
197
+ * all the applications (a queue group, such as the instances of one service), instead of to each of them.
198
+ * @returns The proxy, for chaining.
199
+ */
200
+ on(eventName, listener, onlySingleClientReceived = false) {
201
+ const wrapper = this.keepListener(eventName, listener);
202
+ if (wrapper)
203
+ this.nats.onServiceEvent(this.serviceId, eventName, wrapper, onlySingleClientReceived);
204
+ return this;
205
+ }
206
+ /**
207
+ * Listen to the next occurrence of an event of the remote service only.
208
+ * @param eventName The event name.
209
+ * @param listener The listener, receiving the event's arguments.
210
+ * @returns The proxy, for chaining.
211
+ */
212
+ once(eventName, listener) {
213
+ if (!this.keepListener(eventName, listener))
214
+ return this;
215
+ //The function registered is the one kept: off() removes it
216
+ const registered = (...args) => {
217
+ this.eventListeners.get(eventName)?.delete(listener);
218
+ return listener(...args);
219
+ };
220
+ this.eventListeners.get(eventName).set(listener, registered);
221
+ this.nats.onceServiceEvent(this.serviceId, eventName, registered);
222
+ return this;
223
+ }
224
+ /**
225
+ * Stop listening to an event of the remote service.
226
+ * @param eventName The event name.
227
+ * @param listener The listener to remove; without it, every listener of the event added through this proxy (the
228
+ * listeners added through the other proxies or `NATS.onServiceEvent()` are kept).
229
+ * @returns The proxy, for chaining.
230
+ */
231
+ off(eventName, listener) {
232
+ //This proxy's listeners only: the other proxies of the NATS component keep theirs
233
+ const listeners = this.eventListeners.get(eventName);
234
+ if (!listeners)
235
+ return this;
236
+ for (const [registered, wrapper] of [...listeners]) {
237
+ if (listener && registered !== listener)
238
+ continue;
239
+ this.nats.offServiceEvent(this.serviceId, eventName, wrapper);
240
+ listeners.delete(registered);
241
+ }
242
+ if (!listeners.size)
243
+ this.eventListeners.delete(eventName);
244
+ return this;
245
+ }
246
+ };
247
+ exports.ServiceProxy = ServiceProxy;
248
+ __decorate([
249
+ (0, di_1.Inject)(core_1.Application),
250
+ __metadata("design:type", core_1.Application
251
+ /**
252
+ * The name of the NATS component the proxy uses.
253
+ */
254
+ )
255
+ ], ServiceProxy.prototype, "app", void 0);
256
+ __decorate([
257
+ (0, di_1.Configurable)(core_1.DTO.String().required()),
258
+ __metadata("design:type", String)
259
+ ], ServiceProxy.prototype, "natsComponentName", void 0);
260
+ __decorate([
261
+ (0, di_1.Configurable)(core_1.DTO.String().required()),
262
+ __metadata("design:type", String)
263
+ ], ServiceProxy.prototype, "serviceId", void 0);
264
+ __decorate([
265
+ (0, di_1.Configurable)(core_1.DTO.Number().integer().optional().default(3)),
266
+ __metadata("design:type", Number)
267
+ ], ServiceProxy.prototype, "maxRetries", void 0);
268
+ __decorate([
269
+ (0, di_1.Configurable)(core_1.DTO.Number().integer().optional().default(100)),
270
+ __metadata("design:type", Number)
271
+ ], ServiceProxy.prototype, "retryDelay", void 0);
272
+ exports.ServiceProxy = ServiceProxy = __decorate([
273
+ (0, di_1.Singleton)()
274
+ ], ServiceProxy);
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The options of {@link NATS.subscribe}.
3
+ */
4
+ export type SubscribeOptions = {
5
+ /**
6
+ * The queue group of the subscription: the subscriptions of one queue group on one subject share its messages, each
7
+ * message delivered to one of them only (load balancing between the instances of a service).
8
+ * @default none: every subscription receives every message
9
+ */
10
+ queue?: string;
11
+ /**
12
+ * The number of messages after which the subscription ends by itself (unsubscribed automatically).
13
+ * @default none: no limit
14
+ */
15
+ max?: number;
16
+ /**
17
+ * Whether the messages are handled one after the other, each callback awaited before the next message, instead of
18
+ * concurrently (a callback started for each message as it arrives).
19
+ * @default false
20
+ */
21
+ iterator?: boolean;
22
+ /**
23
+ * Build the response to a request (a message with a reply subject) whose handling failed: the callback threw, the
24
+ * payload could not be decoded or read from the object store, or the response could not be encoded or sent. Its
25
+ * return value is encoded with the codec and sent (never relayed through the object store); `undefined` answers
26
+ * nothing, and the requester waits for its timeout. Without it, a failed request is not answered either.
27
+ * It lets an upper layer (such as the service entrypoint) answer with its own error protocol.
28
+ * @param error The error of the handling.
29
+ * @returns The payload of the response, or `undefined` to answer nothing.
30
+ * @default none: a failed request is not answered
31
+ */
32
+ errorResponse?: (error: any) => any;
33
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Create the JSON codec of the NATS messages, for the `codec` option of {@link buildNatsClientOptions}. A value is sent
3
+ * as the UTF-8 JSON text of a one-element array (`{id: 1}` is sent as `[{"id":1}]`), so that every value, `null` and
4
+ * the strings included, has a JSON form; the peers written in other languages wrap and unwrap that array.
5
+ *
6
+ * The values go through `JSON.stringify()`: a `Date` arrives as its ISO string, a `Buffer` as
7
+ * `{type: 'Buffer', data: [...]}`, a class instance as a plain object, `undefined` as `null` (and is dropped from the
8
+ * objects), `NaN` and `Infinity` as `null`; a `bigint` or a circular value fails the encoding with a `TypeError`.
9
+ * Decoding bytes which are not such JSON throws a `SyntaxError`. Both sides of a subject must use the same codec.
10
+ * @returns The codec.
11
+ * @example
12
+ * ```typescript
13
+ * import {Application} from 'lakutata'
14
+ * import {buildNatsClientOptions, jsonCodec} from '@lakutata/nats'
15
+ *
16
+ * Application.run({
17
+ * id: 'json.app',
18
+ * name: 'JSON',
19
+ * components: {
20
+ * //Readable messages, for peers which do not speak MessagePack
21
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222', codec: jsonCodec()})
22
+ * }
23
+ * })
24
+ *
25
+ * const codec = jsonCodec()
26
+ * const bytes: Uint8Array = codec.encode({id: 1, at: new Date(0)})
27
+ * console.log(Buffer.from(bytes).toString()) //[{"id":1,"at":"1970-01-01T00:00:00.000Z"}]
28
+ * console.log(codec.decode(bytes)) //{id: 1, at: '1970-01-01T00:00:00.000Z'}
29
+ * ```
30
+ */
31
+ export function jsonCodec() {
32
+ return {
33
+ encode(d) {
34
+ return Buffer.from(JSON.stringify([d]));
35
+ },
36
+ decode(a) {
37
+ const [res] = JSON.parse(Buffer.from(a).toString());
38
+ return res;
39
+ }
40
+ };
41
+ }
@@ -0,0 +1,32 @@
1
+ import { MessagePack } from '@lakutata/utils';
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 function messagePackCodec() {
23
+ return {
24
+ encode(d) {
25
+ return MessagePack.encode(JSON.parse(JSON.stringify([d])));
26
+ },
27
+ decode(a) {
28
+ const [res] = MessagePack.decode(a);
29
+ return res;
30
+ }
31
+ };
32
+ }