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