@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.
- package/LICENSE +23 -0
- package/README.md +288 -25
- package/dist/cjs/codecs/JSONCodec.d.ts +32 -0
- package/dist/cjs/codecs/JSONCodec.js +44 -0
- package/dist/cjs/codecs/MessagePackCodec.d.ts +22 -0
- package/dist/cjs/codecs/MessagePackCodec.js +35 -0
- package/dist/cjs/components/NATS.d.ts +376 -0
- package/dist/cjs/components/NATS.js +772 -0
- package/dist/cjs/entrypoints/NatsServiceEntrypoint.d.ts +102 -0
- package/dist/cjs/entrypoints/NatsServiceEntrypoint.js +127 -0
- package/dist/cjs/exceptions/NatsBadRequestException.d.ts +11 -0
- package/dist/cjs/exceptions/NatsBadRequestException.js +18 -0
- package/dist/cjs/exceptions/NatsBulkException.d.ts +19 -0
- package/dist/cjs/exceptions/NatsBulkException.js +26 -0
- package/dist/cjs/exceptions/NatsForbiddenException.d.ts +11 -0
- package/dist/cjs/exceptions/NatsForbiddenException.js +18 -0
- package/dist/cjs/exceptions/NatsInternalServerException.d.ts +12 -0
- package/dist/cjs/exceptions/NatsInternalServerException.js +19 -0
- package/dist/cjs/exceptions/NatsNoRespondersAvailableException.d.ts +12 -0
- package/dist/cjs/exceptions/NatsNoRespondersAvailableException.js +19 -0
- package/dist/cjs/exceptions/NatsNotFoundException.d.ts +11 -0
- package/dist/cjs/exceptions/NatsNotFoundException.js +18 -0
- package/dist/cjs/exceptions/NatsRequestTimeoutException.d.ts +11 -0
- package/dist/cjs/exceptions/NatsRequestTimeoutException.js +18 -0
- package/dist/cjs/exports/Nats.d.ts +18 -0
- package/dist/cjs/exports/Nats.js +39 -0
- package/dist/cjs/interfaces/NatsClientOptions.d.ts +106 -0
- package/dist/{lib → cjs/lib}/ServiceEventCodec.d.ts +0 -1
- package/dist/{lib → cjs/lib}/ServiceEventCodec.js +19 -9
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/providers/ServiceProxy.d.ts +210 -0
- package/dist/cjs/providers/ServiceProxy.js +274 -0
- package/dist/cjs/types/SubscribeOptions.d.ts +33 -0
- package/dist/esm/codecs/JSONCodec.js +41 -0
- package/dist/esm/codecs/MessagePackCodec.js +32 -0
- package/dist/esm/components/NATS.js +767 -0
- package/dist/esm/entrypoints/NatsServiceEntrypoint.js +123 -0
- package/dist/esm/exceptions/NatsBadRequestException.js +14 -0
- package/dist/esm/exceptions/NatsBulkException.js +22 -0
- package/dist/esm/exceptions/NatsForbiddenException.js +14 -0
- package/dist/esm/exceptions/NatsInternalServerException.js +15 -0
- package/dist/esm/exceptions/NatsNoRespondersAvailableException.js +15 -0
- package/dist/esm/exceptions/NatsNotFoundException.js +14 -0
- package/dist/esm/exceptions/NatsRequestTimeoutException.js +14 -0
- package/dist/esm/exports/Nats.js +15 -0
- package/dist/esm/interfaces/NatsClientOptions.js +1 -0
- package/dist/esm/lib/ServiceEventCodec.js +21 -0
- package/dist/esm/providers/ServiceProxy.js +270 -0
- package/dist/esm/types/SubscribeOptions.js +1 -0
- package/dist/types/codecs/JSONCodec.d.ts +32 -0
- package/dist/types/codecs/MessagePackCodec.d.ts +22 -0
- package/dist/types/components/NATS.d.ts +376 -0
- package/dist/types/entrypoints/NatsServiceEntrypoint.d.ts +102 -0
- package/dist/types/exceptions/NatsBadRequestException.d.ts +11 -0
- package/dist/types/exceptions/NatsBulkException.d.ts +19 -0
- package/dist/types/exceptions/NatsForbiddenException.d.ts +11 -0
- package/dist/types/exceptions/NatsInternalServerException.d.ts +12 -0
- package/dist/types/exceptions/NatsNoRespondersAvailableException.d.ts +12 -0
- package/dist/types/exceptions/NatsNotFoundException.d.ts +11 -0
- package/dist/types/exceptions/NatsRequestTimeoutException.d.ts +11 -0
- package/dist/types/exports/Nats.d.ts +18 -0
- package/dist/types/interfaces/NatsClientOptions.d.ts +106 -0
- package/dist/types/lib/ServiceEventCodec.d.ts +9 -0
- package/dist/types/providers/ServiceProxy.d.ts +210 -0
- package/dist/types/types/SubscribeOptions.d.ts +33 -0
- package/package.json +42 -33
- package/CHANGELOG.md +0 -277
- package/dist/CommonExports.d.ts +0 -19
- package/dist/CommonExports.d.ts.map +0 -1
- package/dist/CommonExports.js +0 -35
- package/dist/codecs/JSONCodec.d.ts +0 -3
- package/dist/codecs/JSONCodec.d.ts.map +0 -1
- package/dist/codecs/JSONCodec.js +0 -15
- package/dist/codecs/MessagePackCodec.d.ts +0 -3
- package/dist/codecs/MessagePackCodec.d.ts.map +0 -1
- package/dist/codecs/MessagePackCodec.js +0 -16
- package/dist/components/NATS.d.ts +0 -250
- package/dist/components/NATS.d.ts.map +0 -1
- package/dist/components/NATS.js +0 -562
- package/dist/entrypoints/SetupNatsServiceEntrypoint.d.ts +0 -8
- package/dist/entrypoints/SetupNatsServiceEntrypoint.d.ts.map +0 -1
- package/dist/entrypoints/SetupNatsServiceEntrypoint.js +0 -41
- package/dist/exceptions/NatsBadRequestException.d.ts +0 -7
- package/dist/exceptions/NatsBadRequestException.d.ts.map +0 -1
- package/dist/exceptions/NatsBadRequestException.js +0 -13
- package/dist/exceptions/NatsBulkException.d.ts +0 -11
- package/dist/exceptions/NatsBulkException.d.ts.map +0 -1
- package/dist/exceptions/NatsBulkException.js +0 -17
- package/dist/exceptions/NatsForbiddenException.d.ts +0 -7
- package/dist/exceptions/NatsForbiddenException.d.ts.map +0 -1
- package/dist/exceptions/NatsForbiddenException.js +0 -13
- package/dist/exceptions/NatsInternalServerException.d.ts +0 -7
- package/dist/exceptions/NatsInternalServerException.d.ts.map +0 -1
- package/dist/exceptions/NatsInternalServerException.js +0 -13
- package/dist/exceptions/NatsNoRespondersAvailableException.d.ts +0 -7
- package/dist/exceptions/NatsNoRespondersAvailableException.d.ts.map +0 -1
- package/dist/exceptions/NatsNoRespondersAvailableException.js +0 -13
- package/dist/exceptions/NatsNotFoundException.d.ts +0 -7
- package/dist/exceptions/NatsNotFoundException.d.ts.map +0 -1
- package/dist/exceptions/NatsNotFoundException.js +0 -13
- package/dist/exceptions/NatsRequestTimeoutException.d.ts +0 -7
- package/dist/exceptions/NatsRequestTimeoutException.d.ts.map +0 -1
- package/dist/exceptions/NatsRequestTimeoutException.js +0 -13
- package/dist/exceptions/ServiceInvokeException.d.ts +0 -6
- package/dist/exceptions/ServiceInvokeException.d.ts.map +0 -1
- package/dist/exceptions/ServiceInvokeException.js +0 -11
- package/dist/interfaces/NatsClientOptions.d.ts +0 -26
- package/dist/interfaces/NatsClientOptions.d.ts.map +0 -1
- package/dist/lib/ServiceEventCodec.d.ts.map +0 -1
- package/dist/lib/ServiceResponseCodec.d.ts +0 -22
- package/dist/lib/ServiceResponseCodec.d.ts.map +0 -1
- package/dist/lib/ServiceResponseCodec.js +0 -50
- package/dist/providers/ServiceProxy.d.ts +0 -85
- package/dist/providers/ServiceProxy.d.ts.map +0 -1
- package/dist/providers/ServiceProxy.js +0 -124
- package/dist/types/SubscribeOptions.d.ts +0 -12
- package/dist/types/SubscribeOptions.d.ts.map +0 -1
- package/docs/large-payload-design.md +0 -199
- package/tsconfig.json +0 -8
- /package/dist/{interfaces → cjs/interfaces}/NatsClientOptions.js +0 -0
- /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
|
-
|
|
1
|
+
# @lakutata/nats
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
9
|
+
## Installation
|
|
6
10
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
13
|
-

|
|
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
|
-
|
|
21
|
+
```shell
|
|
22
|
+
docker run -d --name nats -p 4222:4222 nats:2.12-alpine -js
|
|
23
|
+
```
|
|
16
24
|
|
|
17
|
-
##
|
|
25
|
+
## Usage
|
|
18
26
|
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
+
```typescript
|
|
33
|
+
import {Application} from 'lakutata'
|
|
34
|
+
import {buildNatsClientOptions, type NATS} from '@lakutata/nats'
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|