@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
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-present Lakutata
4
+
5
+ All rights reserved
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -1,38 +1,301 @@
1
- <div align="center">
1
+ # @lakutata/nats
2
2
 
3
- ![lakutata](https://socialify.git.ci/lakutata/lakutata/image?description=1&descriptionEditable=An%20IoC-based%20universal%20application%20framework&font=Source%20Code%20Pro&forks=1&language=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Flakutata%2Flakutata%2Fmain%2Fassets%2Flogo.svg&name=1&pattern=Circuit%20Board&stargazers=1&theme=Auto)
3
+ The NATS component of the lakutata framework: a connection to a NATS server to publish, subscribe and send requests
4
+ (MessagePack or JSON payloads), the service channel of an application served over NATS (`natsServiceEntrypoint`), the
5
+ proxies invoking the actions of the other services and listening to their events (`ServiceProxy`), and the large
6
+ payloads relayed through a JetStream object store. The umbrella package `lakutata` does not re-export it: import it
7
+ from `@lakutata/nats`.
4
8
 
5
- </div>
9
+ ## Installation
6
10
 
7
- <div align="center">
11
+ ```shell
12
+ npm install lakutata @lakutata/nats
13
+ ```
14
+
15
+ It uses the NATS client `nats` (a dependency) and needs, on the server side:
8
16
 
9
- ![node-lts](https://img.shields.io/node/v-lts/lakutata?style=for-the-badge&logo=nodedotjs&color=rgb(128%2C189%2C65))
10
- ![npm](https://img.shields.io/npm/v/lakutata?style=for-the-badge&logo=npm&color=rgb(128%2C189%2C65))
11
- ![npm](https://img.shields.io/npm/dm/lakutata?style=for-the-badge&logo=npm&color=rgb(128%2C189%2C65))
12
- ![NPM](https://img.shields.io/npm/l/lakutata?style=for-the-badge&logo=github&color=rgb(128%2C189%2C65))
13
- ![Codacy grade](https://img.shields.io/codacy/grade/0f16d1c355494415ad7733f8f22f7d36?style=for-the-badge&logo=codacy)
17
+ - a NATS server 2.2 or later (the message headers, which the large payloads need);
18
+ - JetStream enabled (`nats-server -js`, or `jetstream {}` in its configuration) for the large payloads. Without it the
19
+ component starts anyway, prints a warning, and only the payloads over the server's `max_payload` fail.
14
20
 
15
- </div>
21
+ ```shell
22
+ docker run -d --name nats -p 4222:4222 nats:2.12-alpine -js
23
+ ```
16
24
 
17
- ## 💫 Description
25
+ ## Usage
18
26
 
19
- Lakutata is a generic development framework written in TypeScript and designed with IoC principles. Its main objective
20
- is to provide a universal, efficient, and stable development framework. The design goals of Lakutata are not limited to
21
- web application development; it aims to serve as a foundational framework for desktop applications, embedded systems
22
- applications, and web applications. The framework primarily adopts an OOP (Object-Oriented Programming) approach and
23
- encapsulates functionalities such as subprocesses, threads, permission management, and database ORM, enabling the
24
- framework to be used out of the box.
27
+ ### The NATS component
25
28
 
26
- In addition, Lakutata also supports the integration of third-party libraries into the application, allowing developers
27
- to freely encapsulate and call third-party modules using Lakutata's dependency injection.
29
+ Register the `NATS` component with `buildNatsClientOptions`, then get it with `getObject()` or `@Inject`. It connects
30
+ when it is created: an unreachable server fails the application's start (after `timeout`, 20 seconds by default).
28
31
 
29
- ## ✨ Getting started
32
+ ```typescript
33
+ import {Application} from 'lakutata'
34
+ import {buildNatsClientOptions, type NATS} from '@lakutata/nats'
30
35
 
31
- - Install lakutata NATS client
32
- ```shell
33
- npm install @lakutata/nats
36
+ Application
37
+ .run({
38
+ id: 'pricing.app',
39
+ name: 'Pricing',
40
+ components: {
41
+ nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222', name: 'pricing'})
42
+ }
43
+ })
44
+ .onLaunched(async (app: Application): Promise<void> => {
45
+ const nats: NATS = await app.getObject('nats')
46
+ //The failures of the callbacks and the subscriptions
47
+ nats.on('error', (error: Error): void => console.error('NATS', error))
48
+ //A responder: the value returned answers the request; the instances of the queue group share the requests
49
+ nats.subscribe('price.get', async (input: {sku: string}): Promise<number> => input.sku.length * 10, {queue: 'pricing'})
50
+ //A subscriber with a wildcard: the second argument is the subject the message arrived on
51
+ nats.subscribe('price.changed.*', (price: number, subject: string): void => console.log(subject, price))
52
+ nats.publish('price.changed.eu', 42)
53
+ //The response, or NatsNoRespondersAvailableException at once, NatsRequestTimeoutException after 5 seconds
54
+ console.log(await nats.request('price.get', {sku: 'ABC'}, 5000)) //30
55
+ })
56
+ ```
57
+
58
+ - `publish()` sends a message to every subscription of the subject (one per queue group); it is lost when nobody
59
+ listens.
60
+ - `subscribe()` accepts the wildcards `*` (one token) and `>` (the remaining tokens). Its callback is called for each
61
+ message as it arrives, or one message after the other with `{iterator: true}`; `{max: n}` ends the subscription
62
+ after `n` messages. It returns the NATS `Subscription` (`unsubscribe()`, `drain()`).
63
+ - `request()` waits for the first response; its timeout is in milliseconds, 1000 when omitted. A callback which throws
64
+ leaves the request unanswered (the requester times out) unless the subscription has an `errorResponse`.
65
+ - A lost connection is reestablished in the background (`reconnect`, `maxReconnectAttempts`: 10 attempts per server
66
+ by default, `-1` for ever); the requests sent meanwhile fail when their timeout passes. Once the client gives up,
67
+ every call fails with the NATS client error `CONNECTION_CLOSED`.
68
+ - When the application stops, the component drains its connections: the messages in progress are handled and the
69
+ outgoing ones flushed before they close.
70
+
71
+ The connection options: `servers`, `codec`, `timeout`, `token`, `user`, `pass`, `debug`, `maxPingOut`,
72
+ `maxReconnectAttempts`, `name`, `noEcho`, `noRandomize`, `pingInterval`, `reconnect`, and the large payloads'
73
+ `bulk`, `bulkBucket`, `bulkTTL`, `bulkReplicas` (see [Large payloads](#large-payloads)).
74
+
75
+ ### The codecs
76
+
77
+ Every payload is encoded with the component's `codec`, the same on both sides of a subject:
78
+
79
+ - `messagePackCodec()` (the default): MessagePack, compact and fast;
80
+ - `jsonCodec()`: JSON text, readable and easy for the clients of other languages.
81
+
82
+ Both send a value as a one-element array (`[value]`) and normalize it through JSON first, so that the clients of
83
+ other languages receive plain JSON data: a `Date` arrives as its ISO string, a `Buffer` as
84
+ `{type: 'Buffer', data: [...]}`, a class instance as a plain object, `undefined` as `null`; a `bigint` fails the
85
+ encoding. Any object with `encode(value): Uint8Array` and `decode(bytes)` can be given, such as `StringCodec()`
86
+ (re-exported from `nats`) for string payloads.
87
+
88
+ ```typescript
89
+ import {Application} from 'lakutata'
90
+ import {buildNatsClientOptions, jsonCodec} from '@lakutata/nats'
91
+
92
+ Application.run({
93
+ id: 'json.app',
94
+ name: 'JSON',
95
+ components: {
96
+ nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222', codec: jsonCodec()})
97
+ }
98
+ })
99
+ ```
100
+
101
+ ### Serving the service actions over NATS
102
+
103
+ `natsServiceEntrypoint` serves the service channel of the `Entrypoint` component: the requests sent to the
104
+ application's id are dispatched to the `@ServiceAction` methods of the controllers, and answered with a service
105
+ response `{success, error, payload}`.
106
+
107
+ ```typescript
108
+ import {Application, DTO} from 'lakutata'
109
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
110
+ import {ServiceAction} from 'lakutata/decorator/ctrl'
111
+ import {Expect} from 'lakutata/decorator/dto'
112
+ import {buildNatsClientOptions, natsServiceEntrypoint, type NATS} from '@lakutata/nats'
113
+
114
+ class GetOrderDTO extends DTO {
115
+ @Expect(DTO.Number().integer().required())
116
+ public id!: number
117
+ }
118
+
119
+ class OrderController extends Controller {
120
+ @ServiceAction({cmd: 'order.get'}, GetOrderDTO)
121
+ public async get(input: GetOrderDTO): Promise<{ id: number, total: number }> {
122
+ return {id: input.id, total: 42}
123
+ }
124
+ }
125
+
126
+ Application
127
+ .run({
128
+ //The subject of the requests, and the serviceId of the proxies invoking this service
129
+ id: 'order.service',
130
+ name: 'OrderService',
131
+ components: {
132
+ nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'}),
133
+ entrypoint: buildEntrypoints({
134
+ controllers: [OrderController],
135
+ //Subject "order.service" (the application id), queue group "order.service.serviceQueue"
136
+ service: natsServiceEntrypoint('nats')
137
+ })
138
+ },
139
+ bootstrap: ['entrypoint']
140
+ })
141
+ .onLaunched(async (app: Application): Promise<void> => {
142
+ //The service events: received by the proxies of this service listening to "orderPaid"
143
+ const nats: NATS = await app.getObject('nats')
144
+ nats.emitServiceEvent('orderPaid', 1, 42)
145
+ })
34
146
  ```
35
147
 
36
- ## 🌎 License
148
+ - The subject is the application id unless the `subject` option sets another; the proxies use it as their
149
+ `serviceId`. The service events stay under the application id.
150
+ - The instances of the service join one queue group (`{subject}.serviceQueue` by default): each request is handled by
151
+ one instance.
152
+ - A failed action (no matching action, invalid input, access denied, an exception) is answered with its error's
153
+ properties; a request which cannot be decoded is answered with an error too, so the requester never waits for its
154
+ timeout.
155
+ - `Entrypoint.broadcast()` publishes its events under the entrypoint's subject, to the service proxies (counted 0:
156
+ NATS does not tell how many receive them); `NATS.emitServiceEvent()` publishes under the application id.
157
+ `Entrypoint.notify()` does not reach the NATS clients (NATS has no client ids).
158
+ - When the entrypoint stops, its subscription is drained: no new request is received (the callers retry, see below)
159
+ and the requests in progress are answered.
160
+ - `setupNatsServiceEntrypoint` is its 2.x name, kept as an alias.
161
+
162
+ ### Invoking a service: the service proxy
163
+
164
+ A `ServiceProxy` invokes the actions of one remote service and listens to its events. It is a singleton provider, shared
165
+ by the requests: declare one per service with `buildServiceProxy` in the `providers` of the application, as in 2.x.
166
+
167
+ ```typescript
168
+ import {Application} from 'lakutata'
169
+ import {
170
+ buildNatsClientOptions,
171
+ buildServiceProxy,
172
+ NatsRequestTimeoutException,
173
+ ServiceInvokeException,
174
+ type ServiceProxy
175
+ } from '@lakutata/nats'
176
+
177
+ Application
178
+ .run({
179
+ id: 'billing.app',
180
+ name: 'Billing',
181
+ components: {
182
+ nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'})
183
+ },
184
+ providers: {
185
+ orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service'})
186
+ }
187
+ })
188
+ .onLaunched(async (app: Application): Promise<void> => {
189
+ const orderService: ServiceProxy = await app.getObject('orderService')
190
+ orderService.on('orderPaid', (id: number, total: number): void => console.log(`Order ${id} paid: ${total}`))
191
+ try {
192
+ //The pattern of the action and its input in one object; 30 seconds at most
193
+ console.log(await orderService.invoke({cmd: 'order.get', id: 1}, 30000))
194
+ } catch (error) {
195
+ if (error instanceof ServiceInvokeException) console.error(error.service, error.errno, error.message)
196
+ else if (error instanceof NatsRequestTimeoutException) console.error('No response in time')
197
+ else throw error
198
+ }
199
+ })
200
+ ```
201
+
202
+ - `invoke(input, timeout)` returns the action's result. Its timeout defaults to 10 minutes: a service which is not
203
+ running fails at once, so the timeout only bounds an action which is too slow.
204
+ - A request nobody received (`NatsNoRespondersAvailableException`: the service is not running, or restarting) is sent
205
+ again, `maxRetries` times (3 by default) every `retryDelay` milliseconds (100 by default), then thrown. A timeout and
206
+ a failure of the service are never retried: the action may have run, and a non-idempotent action never runs twice.
207
+ - A failed action is thrown as a `ServiceInvokeException`: its message is the remote error's, its `errno`,
208
+ `statusCode` and other properties are copied from it, and its `service` is the service id.
209
+ - `on(event, listener)`, `once(event, listener)` and `off(event, listener?)` listen to the events the service emits
210
+ with `NATS.emitServiceEvent()`. With `on(event, listener, true)`, each event goes to one listener among those
211
+ registered so, across the applications (a queue group). The events are delivered at most once: an application not
212
+ connected when one is emitted misses it. The listeners registered through a proxy are removed when it is
213
+ destroyed.
214
+
215
+ ### Large payloads
216
+
217
+ A NATS message cannot exceed the server's `max_payload` (1 MiB by default). The component relays the requests and the
218
+ responses of 90% of `max_payload` or more through a JetStream object store, over a second connection, so the large
219
+ transfers never delay the small messages: the sender stores the encoded payload and sends a small reference, the
220
+ receiver reads the object and deletes it. The callbacks, the actions and `invoke()` see the whole value.
221
+
222
+ - It is on by default (`bulk: true`) and needs JetStream. When it cannot start (no JetStream, the bucket refused), the
223
+ component prints a warning and starts without it.
224
+ - The applications exchanging large payloads must share their bucket: set the same `bulkBucket` on all of them. The
225
+ default bucket depends on the `name` option (`lkt-bulk-{name}`, `lkt-bulk-default` without it), so two applications
226
+ with different names cannot read each other's objects (`NatsBulkException`).
227
+ - The objects not read (a requester gone, a response lost) expire after `bulkTTL` (5 minutes by default);
228
+ `bulkReplicas` sets the bucket's replicas in a cluster. Both apply when the bucket is created.
229
+ - A large response is relayed when the requester declared the bulk channel (its requests carry it). A large request
230
+ is relayed when the responders support it: learned from a previous response on the same subject, else asked once by
231
+ a probe of the subject (`_LKT.BULK.<subject>`), which the responders with the bulk channel answer and the older
232
+ versions ignore (the request is then sent as a message, and fails over `max_payload`).
233
+ - The applications without the bulk channel (`bulk: false`, or the versions before it) keep working with the others:
234
+ they receive the payloads themselves, and a payload over `max_payload` fails.
235
+ - The published messages and the service events are never relayed: over `max_payload`, `publish()` and
236
+ `emitServiceEvent()` throw the NATS client error `MAX_PAYLOAD_EXCEEDED`.
237
+
238
+ ```typescript
239
+ import {Application} from 'lakutata'
240
+ import {buildNatsClientOptions} from '@lakutata/nats'
241
+
242
+ Application.run({
243
+ id: 'report.service',
244
+ name: 'ReportService',
245
+ components: {
246
+ nats: buildNatsClientOptions({
247
+ servers: 'nats://127.0.0.1:4222',
248
+ //The same bucket in every application exchanging large payloads
249
+ bulkBucket: 'lkt-bulk-shared',
250
+ bulkTTL: 10 * 60 * 1000,
251
+ bulkReplicas: 3
252
+ })
253
+ }
254
+ })
255
+ ```
256
+
257
+ `NATS.bulkEnabled`, `bulkThreshold`, `bulkStatus()` and `headersSupported` tell the state of the bulk channel;
258
+ `putBulk()`, `getBulk()` and `deleteBulk()` use its object store directly. The design is described in
259
+ `docs/large-payload-design.md` (Chinese).
260
+
261
+ ## API
262
+
263
+ | Export | What it is |
264
+ |---|---|
265
+ | `NATS` | The component: `publish`, `request`, `subscribe`, `emitServiceEvent`, `onServiceEvent`, `onceServiceEvent`, `offServiceEvent`, the bulk channel (`bulkEnabled`, `bulkThreshold`, `headersSupported`, `putBulk`, `getBulk`, `deleteBulk`, `bulkStatus`), the event `error` |
266
+ | `buildNatsClientOptions` | Builds the component options of a `NATS` component |
267
+ | `NatsClientOptions` | Its options: the connection, the codec and the bulk channel |
268
+ | `SubscribeOptions` | The options of `subscribe()`: `queue`, `max`, `iterator`, `errorResponse` |
269
+ | `messagePackCodec`, `jsonCodec` | The codecs of the payloads |
270
+ | `StringCodec`, `Codec`, `Subscription`, `RetentionPolicy` | Re-exported from `nats` |
271
+ | `natsServiceEntrypoint` | The service adapter of an entrypoint served over NATS |
272
+ | `setupNatsServiceEntrypoint` | Its 2.x name |
273
+ | `NatsServiceEntrypointOptions` | Its options: `subject`, `queue`, `name`, `middlewares` |
274
+ | `ServiceProxy` | The singleton provider invoking the actions of a remote service and listening to its events |
275
+ | `buildServiceProxy` | Builds the provider options of a `ServiceProxy` |
276
+ | `BuildServiceProxyOptions` | Its options: `natsComponentName`, `serviceId`, `maxRetries`, `retryDelay` |
277
+ | `ServiceInvokeException` and the `Nats*Exception` classes | See [Errors](#errors) |
278
+
279
+ The typings document each declaration with examples.
280
+
281
+ ## Errors
282
+
283
+ | Exception | Thrown when |
284
+ |---|---|
285
+ | `ServiceInvokeException` (`E_SERVICE_INVOKE`, or the remote error's `errno`) | `ServiceProxy.invoke()`: the remote action failed; it carries the remote error's properties and the `service` id. Re-exported from `@lakutata/entrypoint` |
286
+ | `NatsNoRespondersAvailableException` (`E_NATS_NO_RESPONDERS_AVAILABLE`, 503) | `request()`: nobody listens on the subject; `invoke()` after its retries |
287
+ | `NatsRequestTimeoutException` (`E_NATS_REQUEST_TIMEOUT`, 408) | `request()`, `invoke()`: no response in time |
288
+ | `NatsBulkException` (`E_NATS_BULK`, 502) | The bulk channel failed: disabled, the object store refused a write or a read, an object expired or in another bucket. On the service side, the requester receives it as a `ServiceInvokeException` whose `errno` is `E_NATS_BULK` |
289
+ | `NatsBadRequestException` (400), `NatsForbiddenException` (403), `NatsNotFoundException` (404), `NatsInternalServerException` (500) | `request()`: an error of the NATS client with that code; the core requests do not produce them |
290
+ | `InvalidValueException` (from `lakutata`) | The component options are invalid, when the component is created |
291
+
292
+ The other failures are the NATS client's errors (`NatsError`, with a `code`): `MAX_PAYLOAD_EXCEEDED` for a payload
293
+ over `max_payload` which is not relayed, `CONNECTION_CLOSED` once the connection is closed, `CONNECTION_REFUSED` or a
294
+ connection timeout when no server can be reached at the start. The codec's errors (a `bigint`, a circular value) are thrown as they are.
295
+
296
+ ## See also
37
297
 
38
- Lakutata is [MIT licensed](LICENSE).
298
+ - `@lakutata/entrypoint` (`lakutata/com/entrypoint`, `lakutata/decorator/ctrl`): the controllers, `@ServiceAction` and
299
+ the other transports; `doc/en/ActionPattern.md` of the package `lakutata` (Chinese in `doc/zh`) for the patterns.
300
+ - `@lakutata/grpc/service`: the same service channel and proxy over gRPC.
301
+ - `docs/large-payload-design.md`: the design of the large payloads.
@@ -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,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jsonCodec = jsonCodec;
4
+ /**
5
+ * Create the JSON codec of the NATS messages, for the `codec` option of {@link buildNatsClientOptions}. A value is sent
6
+ * as the UTF-8 JSON text of a one-element array (`{id: 1}` is sent as `[{"id":1}]`), so that every value, `null` and
7
+ * the strings included, has a JSON form; the peers written in other languages wrap and unwrap that array.
8
+ *
9
+ * The values go through `JSON.stringify()`: a `Date` arrives as its ISO string, a `Buffer` as
10
+ * `{type: 'Buffer', data: [...]}`, a class instance as a plain object, `undefined` as `null` (and is dropped from the
11
+ * objects), `NaN` and `Infinity` as `null`; a `bigint` or a circular value fails the encoding with a `TypeError`.
12
+ * Decoding bytes which are not such JSON throws a `SyntaxError`. Both sides of a subject must use the same codec.
13
+ * @returns The codec.
14
+ * @example
15
+ * ```typescript
16
+ * import {Application} from 'lakutata'
17
+ * import {buildNatsClientOptions, jsonCodec} from '@lakutata/nats'
18
+ *
19
+ * Application.run({
20
+ * id: 'json.app',
21
+ * name: 'JSON',
22
+ * components: {
23
+ * //Readable messages, for peers which do not speak MessagePack
24
+ * nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222', codec: jsonCodec()})
25
+ * }
26
+ * })
27
+ *
28
+ * const codec = jsonCodec()
29
+ * const bytes: Uint8Array = codec.encode({id: 1, at: new Date(0)})
30
+ * console.log(Buffer.from(bytes).toString()) //[{"id":1,"at":"1970-01-01T00:00:00.000Z"}]
31
+ * console.log(codec.decode(bytes)) //{id: 1, at: '1970-01-01T00:00:00.000Z'}
32
+ * ```
33
+ */
34
+ function jsonCodec() {
35
+ return {
36
+ encode(d) {
37
+ return Buffer.from(JSON.stringify([d]));
38
+ },
39
+ decode(a) {
40
+ const [res] = JSON.parse(Buffer.from(a).toString());
41
+ return res;
42
+ }
43
+ };
44
+ }
@@ -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>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.messagePackCodec = messagePackCodec;
4
+ const utils_1 = require("@lakutata/utils");
5
+ /**
6
+ * Create the MessagePack codec of the NATS messages, the default `codec` of the {@link NATS} component. A value is sent
7
+ * as the MessagePack encoding of a one-element array holding it, smaller and faster to decode than JSON.
8
+ *
9
+ * The value is first normalized through JSON (`JSON.parse(JSON.stringify(value))`), so that the peers written in other
10
+ * languages receive plain JSON data: a `Date` arrives as its ISO string, a `Buffer` as `{type: 'Buffer', data: [...]}`
11
+ * (the binary data is not kept as binary), a class instance as a plain object, `undefined` as `null` (and is dropped
12
+ * from the objects); a `bigint` or a circular value fails the encoding with a `TypeError`. Decoding bytes which are not
13
+ * MessagePack throws. Both sides of a subject must use the same codec.
14
+ * @returns The codec.
15
+ * @example
16
+ * ```typescript
17
+ * import {messagePackCodec} from '@lakutata/nats'
18
+ *
19
+ * const codec = messagePackCodec()
20
+ * const bytes: Uint8Array = codec.encode({id: 1, tags: ['a', 'b']})
21
+ * console.log(codec.decode(bytes)) //{id: 1, tags: ['a', 'b']}
22
+ * console.log(codec.decode(codec.encode(undefined))) //null
23
+ * ```
24
+ */
25
+ function messagePackCodec() {
26
+ return {
27
+ encode(d) {
28
+ return utils_1.MessagePack.encode(JSON.parse(JSON.stringify([d])));
29
+ },
30
+ decode(a) {
31
+ const [res] = utils_1.MessagePack.decode(a);
32
+ return res;
33
+ }
34
+ };
35
+ }