@riaskov/nevo-messaging 1.0.1 → 1.1.1
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 +1 -1
- package/README.md +241 -56
- package/dist/common/access-control.d.ts +15 -0
- package/dist/common/access-control.js +94 -0
- package/dist/common/base.client.d.ts +7 -2
- package/dist/common/base.client.js +16 -2
- package/dist/common/base.controller.d.ts +6 -1
- package/dist/common/base.controller.js +68 -4
- package/dist/common/constants.d.ts +4 -0
- package/dist/common/constants.js +7 -0
- package/dist/common/discovery.d.ts +8 -0
- package/dist/common/discovery.js +35 -0
- package/dist/common/error-code.d.ts +2 -1
- package/dist/common/error-code.js +1 -0
- package/dist/common/error-messages.js +2 -1
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +3 -0
- package/dist/common/service-utils.d.ts +2 -0
- package/dist/common/service-utils.js +8 -0
- package/dist/common/types.d.ts +62 -0
- package/dist/signal-router.utils.d.ts +3 -1
- package/dist/signal-router.utils.js +70 -6
- package/dist/transports/http/http.client-base.d.ts +13 -0
- package/dist/transports/http/http.client-base.js +33 -0
- package/dist/transports/http/http.config.d.ts +8 -0
- package/dist/transports/http/http.config.js +16 -0
- package/dist/transports/http/http.signal-router.decorator.d.ts +3 -0
- package/dist/transports/http/http.signal-router.decorator.js +18 -0
- package/dist/transports/http/http.transport.controller.d.ts +21 -0
- package/dist/transports/http/http.transport.controller.js +114 -0
- package/dist/transports/http/index.d.ts +5 -0
- package/dist/transports/http/index.js +21 -0
- package/dist/transports/http/nevo-http.client.d.ts +54 -0
- package/dist/transports/http/nevo-http.client.js +280 -0
- package/dist/transports/index.d.ts +3 -0
- package/dist/transports/index.js +3 -0
- package/dist/transports/kafka/kafka.client-base.d.ts +5 -0
- package/dist/transports/kafka/kafka.client-base.js +15 -0
- package/dist/transports/kafka/kafka.config.d.ts +7 -0
- package/dist/transports/kafka/kafka.config.js +48 -2
- package/dist/transports/kafka/kafka.signal-router.decorator.js +2 -1
- package/dist/transports/kafka/nevo-kafka.client.d.ts +42 -0
- package/dist/transports/kafka/nevo-kafka.client.js +210 -4
- package/dist/transports/nats/index.d.ts +4 -0
- package/dist/transports/nats/index.js +20 -0
- package/dist/transports/nats/nats.client-base.d.ts +13 -0
- package/dist/transports/nats/nats.client-base.js +33 -0
- package/dist/transports/nats/nats.config.d.ts +8 -0
- package/dist/transports/nats/nats.config.js +16 -0
- package/dist/transports/nats/nats.signal-router.decorator.d.ts +6 -0
- package/dist/transports/nats/nats.signal-router.decorator.js +49 -0
- package/dist/transports/nats/nevo-nats.client.d.ts +55 -0
- package/dist/transports/nats/nevo-nats.client.js +210 -0
- package/dist/transports/socket-io/index.d.ts +4 -0
- package/dist/transports/socket-io/index.js +20 -0
- package/dist/transports/socket-io/nevo-socket.client.d.ts +50 -0
- package/dist/transports/socket-io/nevo-socket.client.js +202 -0
- package/dist/transports/socket-io/socket.client-base.d.ts +13 -0
- package/dist/transports/socket-io/socket.client-base.js +33 -0
- package/dist/transports/socket-io/socket.config.d.ts +8 -0
- package/dist/transports/socket-io/socket.config.js +16 -0
- package/dist/transports/socket-io/socket.signal-router.decorator.d.ts +13 -0
- package/dist/transports/socket-io/socket.signal-router.decorator.js +109 -0
- package/package.json +11 -5
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Subscription, SubscriptionContext, SubscriptionOptions } from "../../common";
|
|
2
|
+
export interface NevoHttpClientOptions {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
debug?: boolean;
|
|
5
|
+
serviceName?: string;
|
|
6
|
+
authToken?: string;
|
|
7
|
+
discoveryUrl?: string;
|
|
8
|
+
backoff?: {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
baseMs?: number;
|
|
11
|
+
maxMs?: number;
|
|
12
|
+
maxAttempts?: number;
|
|
13
|
+
jitter?: boolean;
|
|
14
|
+
};
|
|
15
|
+
discovery?: {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
heartbeatIntervalMs?: number;
|
|
18
|
+
ttlMs?: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare class NevoHttpClient {
|
|
22
|
+
private readonly serviceUrls;
|
|
23
|
+
private readonly timeoutMs;
|
|
24
|
+
private readonly debug;
|
|
25
|
+
private readonly serviceName?;
|
|
26
|
+
private readonly authToken?;
|
|
27
|
+
private readonly backoffEnabled;
|
|
28
|
+
private readonly backoffBaseMs;
|
|
29
|
+
private readonly backoffMaxMs;
|
|
30
|
+
private readonly backoffMaxAttempts;
|
|
31
|
+
private readonly backoffJitter;
|
|
32
|
+
private readonly inFlight;
|
|
33
|
+
private readonly discoveryRegistry;
|
|
34
|
+
private readonly discoveryEnabled;
|
|
35
|
+
private readonly discoveryHeartbeatIntervalMs;
|
|
36
|
+
private readonly discoveryTtlMs;
|
|
37
|
+
private readonly discoveryUrl?;
|
|
38
|
+
private discoveryTimer?;
|
|
39
|
+
private discoveryAbort?;
|
|
40
|
+
constructor(serviceUrls: Record<string, string>, options?: NevoHttpClientOptions);
|
|
41
|
+
private createMessagePayload;
|
|
42
|
+
private waitForInFlightSlot;
|
|
43
|
+
private getServiceUrl;
|
|
44
|
+
private postJson;
|
|
45
|
+
query<T = any>(serviceName: string, method: string, params: any): Promise<T>;
|
|
46
|
+
emit(serviceName: string, method: string, params: any): Promise<void>;
|
|
47
|
+
publish(serviceName: string, method: string, params: any): Promise<void>;
|
|
48
|
+
broadcast(method: string, params: any): Promise<void>;
|
|
49
|
+
subscribe<T = any>(serviceName: string, method: string, options: SubscriptionOptions | undefined, handler: (data: T, context: SubscriptionContext) => Promise<void> | void): Promise<Subscription>;
|
|
50
|
+
getAvailableServices(): string[];
|
|
51
|
+
getDiscoveredServices(): import("../../common").DiscoveryEntry[];
|
|
52
|
+
isServiceAvailable(serviceName: string): boolean;
|
|
53
|
+
private initDiscovery;
|
|
54
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NevoHttpClient = void 0;
|
|
4
|
+
const common_1 = require("../../common");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
class NevoHttpClient {
|
|
7
|
+
constructor(serviceUrls, options) {
|
|
8
|
+
this.inFlight = new Set();
|
|
9
|
+
this.discoveryRegistry = new common_1.DiscoveryRegistry();
|
|
10
|
+
this.serviceUrls = new Map(Object.entries(serviceUrls).map(([k, v]) => [k.toLowerCase(), v]));
|
|
11
|
+
this.timeoutMs = options?.timeoutMs || 20000;
|
|
12
|
+
this.debug = options?.debug || false;
|
|
13
|
+
this.serviceName = options?.serviceName;
|
|
14
|
+
this.authToken = options?.authToken;
|
|
15
|
+
this.backoffEnabled = options?.backoff?.enabled !== false;
|
|
16
|
+
this.backoffBaseMs = options?.backoff?.baseMs || 100;
|
|
17
|
+
this.backoffMaxMs = options?.backoff?.maxMs || 2000;
|
|
18
|
+
this.backoffMaxAttempts = options?.backoff?.maxAttempts || 0;
|
|
19
|
+
this.backoffJitter = options?.backoff?.jitter !== false;
|
|
20
|
+
this.discoveryEnabled = options?.discovery?.enabled === true && !!options?.discoveryUrl;
|
|
21
|
+
this.discoveryHeartbeatIntervalMs = options?.discovery?.heartbeatIntervalMs || 5000;
|
|
22
|
+
this.discoveryTtlMs = options?.discovery?.ttlMs || 15000;
|
|
23
|
+
this.discoveryUrl = options?.discoveryUrl;
|
|
24
|
+
if (this.discoveryEnabled) {
|
|
25
|
+
void this.initDiscovery();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
createMessagePayload(method, params, type) {
|
|
29
|
+
const uuid = (0, node_crypto_1.randomUUID)();
|
|
30
|
+
const meta = {
|
|
31
|
+
type,
|
|
32
|
+
service: this.serviceName,
|
|
33
|
+
ts: Date.now(),
|
|
34
|
+
auth: { token: this.authToken }
|
|
35
|
+
};
|
|
36
|
+
return { uuid, method, params, meta };
|
|
37
|
+
}
|
|
38
|
+
async waitForInFlightSlot(key) {
|
|
39
|
+
if (!this.backoffEnabled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let attempt = 0;
|
|
43
|
+
let delay = this.backoffBaseMs;
|
|
44
|
+
while (this.inFlight.has(key)) {
|
|
45
|
+
attempt++;
|
|
46
|
+
if (this.backoffMaxAttempts > 0 && attempt > this.backoffMaxAttempts) {
|
|
47
|
+
throw new common_1.MessagingError(common_1.ErrorCode.UNKNOWN, {
|
|
48
|
+
message: `Backoff exceeded for ${key}`
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const jitter = this.backoffJitter ? Math.floor(Math.random() * delay * 0.2) : 0;
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
53
|
+
delay = Math.min(this.backoffMaxMs, delay * 2);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
getServiceUrl(serviceName) {
|
|
57
|
+
const normalized = serviceName.toLowerCase();
|
|
58
|
+
const url = this.serviceUrls.get(normalized);
|
|
59
|
+
if (!url) {
|
|
60
|
+
throw new common_1.MessagingError(common_1.ErrorCode.UNKNOWN, {
|
|
61
|
+
message: `Service "${serviceName}" is not registered in nevo http client`,
|
|
62
|
+
availableServices: [...this.serviceUrls.keys()]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return url.replace(/\/+$/, "");
|
|
66
|
+
}
|
|
67
|
+
async postJson(url, payload) {
|
|
68
|
+
const fetchFn = globalThis.fetch;
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetchFn(url, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": "application/json"
|
|
76
|
+
},
|
|
77
|
+
body: (0, common_1.stringifyWithBigInt)(payload),
|
|
78
|
+
signal: controller.signal
|
|
79
|
+
});
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
if (!text) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return (0, common_1.parseWithBigInt)(text);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error?.name === "AbortError") {
|
|
88
|
+
throw new common_1.MessagingError(common_1.ErrorCode.UNKNOWN, {
|
|
89
|
+
message: `HTTP request timed out after ${this.timeoutMs}ms`
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async query(serviceName, method, params) {
|
|
99
|
+
const url = this.getServiceUrl(serviceName);
|
|
100
|
+
const endpoint = `${url}/${serviceName.toLowerCase()}${common_1.DEFAULT_EVENTS_SUFFIX}`;
|
|
101
|
+
const payload = this.createMessagePayload(method, params, "query");
|
|
102
|
+
const inFlightKey = `${serviceName.toLowerCase()}:${method}`;
|
|
103
|
+
if (this.debug) {
|
|
104
|
+
console.log(`[NevoHttpClient] Sending query to ${endpoint}:`, { method, params });
|
|
105
|
+
}
|
|
106
|
+
let inFlightAcquired = false;
|
|
107
|
+
await this.waitForInFlightSlot(inFlightKey);
|
|
108
|
+
this.inFlight.add(inFlightKey);
|
|
109
|
+
inFlightAcquired = true;
|
|
110
|
+
try {
|
|
111
|
+
const response = await this.postJson(endpoint, payload);
|
|
112
|
+
if (response?.params?.result === "error" && response?.params?.error) {
|
|
113
|
+
const errorData = response.params.error;
|
|
114
|
+
const error = new common_1.MessagingError(errorData.code, errorData.details, errorData.service || serviceName);
|
|
115
|
+
if (process.env["MODE"] !== "production" && errorData.stack) {
|
|
116
|
+
error.stack = errorData.stack;
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
return response?.params?.result;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (inFlightAcquired) {
|
|
124
|
+
this.inFlight.delete(inFlightKey);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async emit(serviceName, method, params) {
|
|
129
|
+
const url = this.getServiceUrl(serviceName);
|
|
130
|
+
const endpoint = `${url}/${serviceName.toLowerCase()}${common_1.DEFAULT_EVENTS_SUFFIX}`;
|
|
131
|
+
const payload = this.createMessagePayload(method, params, "emit");
|
|
132
|
+
if (this.debug) {
|
|
133
|
+
console.log(`[NevoHttpClient] Emitting to ${endpoint}:`, { method, params });
|
|
134
|
+
}
|
|
135
|
+
await this.postJson(endpoint, payload);
|
|
136
|
+
}
|
|
137
|
+
async publish(serviceName, method, params) {
|
|
138
|
+
const url = this.getServiceUrl(serviceName);
|
|
139
|
+
const endpoint = `${url}/__nevo/publish`;
|
|
140
|
+
const payload = this.createMessagePayload(method, params, "sub");
|
|
141
|
+
const body = {
|
|
142
|
+
serviceName,
|
|
143
|
+
...payload
|
|
144
|
+
};
|
|
145
|
+
if (this.debug) {
|
|
146
|
+
console.log(`[NevoHttpClient] Publishing to ${endpoint}:`, { method, params });
|
|
147
|
+
}
|
|
148
|
+
await this.postJson(endpoint, body);
|
|
149
|
+
}
|
|
150
|
+
async broadcast(method, params) {
|
|
151
|
+
const url = this.discoveryUrl || [...this.serviceUrls.values()][0];
|
|
152
|
+
if (!url) {
|
|
153
|
+
throw new common_1.MessagingError(common_1.ErrorCode.UNKNOWN, { message: "No base URL available for broadcast" });
|
|
154
|
+
}
|
|
155
|
+
const endpoint = `${url.replace(/\/+$/, "")}/${common_1.DEFAULT_BROADCAST_TOPIC}`;
|
|
156
|
+
const payload = this.createMessagePayload(method, params, "broadcast");
|
|
157
|
+
if (this.debug) {
|
|
158
|
+
console.log(`[NevoHttpClient] Broadcasting to ${endpoint}:`, { method, params });
|
|
159
|
+
}
|
|
160
|
+
await this.postJson(endpoint, payload);
|
|
161
|
+
}
|
|
162
|
+
async subscribe(serviceName, method, options, handler) {
|
|
163
|
+
const normalized = serviceName.toLowerCase();
|
|
164
|
+
const isBroadcast = normalized === common_1.DEFAULT_BROADCAST_TOPIC;
|
|
165
|
+
const baseUrl = isBroadcast ? this.discoveryUrl || [...this.serviceUrls.values()][0] : this.getServiceUrl(serviceName);
|
|
166
|
+
if (!baseUrl) {
|
|
167
|
+
throw new common_1.MessagingError(common_1.ErrorCode.UNKNOWN, { message: "No base URL available for subscription" });
|
|
168
|
+
}
|
|
169
|
+
const endpoint = isBroadcast
|
|
170
|
+
? `${baseUrl.replace(/\/+$/, "")}/${common_1.DEFAULT_BROADCAST_TOPIC}`
|
|
171
|
+
: `${baseUrl.replace(/\/+$/, "")}/__nevo/subscribe?service=${encodeURIComponent(serviceName)}`;
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const fetchFn = globalThis.fetch;
|
|
174
|
+
const response = await fetchFn(endpoint, {
|
|
175
|
+
headers: { Accept: "text/event-stream" },
|
|
176
|
+
signal: controller.signal
|
|
177
|
+
});
|
|
178
|
+
if (!response.ok || !response.body) {
|
|
179
|
+
throw new common_1.MessagingError(common_1.ErrorCode.UNKNOWN, { message: `Failed to subscribe via SSE: ${response.status}` });
|
|
180
|
+
}
|
|
181
|
+
const reader = response.body.getReader();
|
|
182
|
+
const decoder = new TextDecoder();
|
|
183
|
+
let buffer = "";
|
|
184
|
+
const readLoop = async () => {
|
|
185
|
+
while (true) {
|
|
186
|
+
const { value, done } = await reader.read();
|
|
187
|
+
if (done) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
buffer += decoder.decode(value);
|
|
191
|
+
const parts = buffer.split("\n\n");
|
|
192
|
+
buffer = parts.pop() || "";
|
|
193
|
+
for (const part of parts) {
|
|
194
|
+
const line = part.split("\n").find((l) => l.startsWith("data:"));
|
|
195
|
+
if (!line)
|
|
196
|
+
continue;
|
|
197
|
+
const raw = line.replace(/^data:\s*/, "");
|
|
198
|
+
const payload = (0, common_1.parseWithBigInt)(raw);
|
|
199
|
+
if (method && payload.method !== method) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const context = {
|
|
203
|
+
meta: payload.meta || {},
|
|
204
|
+
ack: async () => {
|
|
205
|
+
return;
|
|
206
|
+
},
|
|
207
|
+
nack: async () => {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
await handler(payload.params, context);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
void readLoop();
|
|
216
|
+
return {
|
|
217
|
+
unsubscribe: async () => {
|
|
218
|
+
controller.abort();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
getAvailableServices() {
|
|
223
|
+
return [...this.serviceUrls.keys()];
|
|
224
|
+
}
|
|
225
|
+
getDiscoveredServices() {
|
|
226
|
+
this.discoveryRegistry.prune(this.discoveryTtlMs);
|
|
227
|
+
return this.discoveryRegistry.list();
|
|
228
|
+
}
|
|
229
|
+
isServiceAvailable(serviceName) {
|
|
230
|
+
return this.discoveryRegistry.isAvailable(serviceName, this.discoveryTtlMs);
|
|
231
|
+
}
|
|
232
|
+
async initDiscovery() {
|
|
233
|
+
if (!this.discoveryUrl) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this.discoveryAbort = new AbortController();
|
|
237
|
+
const discoveryEndpoint = `${this.discoveryUrl.replace(/\/+$/, "")}/${common_1.DEFAULT_DISCOVERY_TOPIC}`;
|
|
238
|
+
const fetchFn = globalThis.fetch;
|
|
239
|
+
const response = await fetchFn(discoveryEndpoint, {
|
|
240
|
+
headers: { Accept: "text/event-stream" },
|
|
241
|
+
signal: this.discoveryAbort.signal
|
|
242
|
+
});
|
|
243
|
+
if (response.ok && response.body) {
|
|
244
|
+
const reader = response.body.getReader();
|
|
245
|
+
const decoder = new TextDecoder();
|
|
246
|
+
let buffer = "";
|
|
247
|
+
(async () => {
|
|
248
|
+
while (true) {
|
|
249
|
+
const { value, done } = await reader.read();
|
|
250
|
+
if (done) {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
buffer += decoder.decode(value);
|
|
254
|
+
const parts = buffer.split("\n\n");
|
|
255
|
+
buffer = parts.pop() || "";
|
|
256
|
+
for (const part of parts) {
|
|
257
|
+
const line = part.split("\n").find((l) => l.startsWith("data:"));
|
|
258
|
+
if (!line)
|
|
259
|
+
continue;
|
|
260
|
+
const raw = line.replace(/^data:\s*/, "");
|
|
261
|
+
const payload = (0, common_1.parseWithBigInt)(raw);
|
|
262
|
+
if (payload?.serviceName) {
|
|
263
|
+
this.discoveryRegistry.update(payload);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})();
|
|
268
|
+
}
|
|
269
|
+
this.discoveryTimer = setInterval(() => {
|
|
270
|
+
const announcement = {
|
|
271
|
+
serviceName: this.serviceName || "unknown",
|
|
272
|
+
clientId: this.serviceName,
|
|
273
|
+
transport: "http",
|
|
274
|
+
ts: Date.now()
|
|
275
|
+
};
|
|
276
|
+
void this.postJson(discoveryEndpoint, announcement);
|
|
277
|
+
}, this.discoveryHeartbeatIntervalMs);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.NevoHttpClient = NevoHttpClient;
|
package/dist/transports/index.js
CHANGED
|
@@ -15,3 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./kafka"), exports);
|
|
18
|
+
__exportStar(require("./nats"), exports);
|
|
19
|
+
__exportStar(require("./http"), exports);
|
|
20
|
+
__exportStar(require("./socket-io"), exports);
|
|
@@ -4,5 +4,10 @@ export declare abstract class KafkaClientBase {
|
|
|
4
4
|
protected constructor(universalClient: NevoKafkaClient);
|
|
5
5
|
protected query<T = any>(serviceName: string, method: string, params: any): Promise<T>;
|
|
6
6
|
protected emit(serviceName: string, method: string, params: any): Promise<void>;
|
|
7
|
+
protected publish(serviceName: string, method: string, params: any): Promise<void>;
|
|
8
|
+
protected broadcast(method: string, params: any): Promise<void>;
|
|
9
|
+
protected subscribe<T = any>(serviceName: string, method: string, options: Parameters<NevoKafkaClient["subscribe"]>[2], handler: Parameters<NevoKafkaClient["subscribe"]>[3]): Promise<import("../..").Subscription>;
|
|
7
10
|
protected getAvailableServices(): string[];
|
|
11
|
+
protected getDiscoveredServices(): import("../..").DiscoveryEntry[];
|
|
12
|
+
protected isServiceAvailable(serviceName: string): boolean;
|
|
8
13
|
}
|
|
@@ -11,8 +11,23 @@ class KafkaClientBase {
|
|
|
11
11
|
async emit(serviceName, method, params) {
|
|
12
12
|
return this.universalClient.emit(serviceName, method, params);
|
|
13
13
|
}
|
|
14
|
+
async publish(serviceName, method, params) {
|
|
15
|
+
return this.universalClient.publish(serviceName, method, params);
|
|
16
|
+
}
|
|
17
|
+
async broadcast(method, params) {
|
|
18
|
+
return this.universalClient.broadcast(method, params);
|
|
19
|
+
}
|
|
20
|
+
async subscribe(serviceName, method, options, handler) {
|
|
21
|
+
return this.universalClient.subscribe(serviceName, method, options, handler);
|
|
22
|
+
}
|
|
14
23
|
getAvailableServices() {
|
|
15
24
|
return this.universalClient.getAvailableServices();
|
|
16
25
|
}
|
|
26
|
+
getDiscoveredServices() {
|
|
27
|
+
return this.universalClient.getDiscoveredServices();
|
|
28
|
+
}
|
|
29
|
+
isServiceAvailable(serviceName) {
|
|
30
|
+
return this.universalClient.isServiceAvailable(serviceName);
|
|
31
|
+
}
|
|
17
32
|
}
|
|
18
33
|
exports.KafkaClientBase = KafkaClientBase;
|
|
@@ -2,6 +2,7 @@ import { NevoKafkaClient } from "./nevo-kafka.client";
|
|
|
2
2
|
export interface KafkaClientFactoryOptions {
|
|
3
3
|
clientIdPrefix: string;
|
|
4
4
|
groupIdPrefix?: string;
|
|
5
|
+
serviceName?: string;
|
|
5
6
|
sessionTimeout?: number;
|
|
6
7
|
allowAutoTopicCreation?: boolean;
|
|
7
8
|
retryAttempts?: number;
|
|
@@ -9,6 +10,12 @@ export interface KafkaClientFactoryOptions {
|
|
|
9
10
|
kafkaHost?: string;
|
|
10
11
|
kafkaPort?: string;
|
|
11
12
|
debug?: boolean;
|
|
13
|
+
authToken?: string;
|
|
14
|
+
discovery?: {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
heartbeatIntervalMs?: number;
|
|
17
|
+
ttlMs?: number;
|
|
18
|
+
};
|
|
12
19
|
}
|
|
13
20
|
export declare const createNevoKafkaClient: (serviceNames: string[], options: KafkaClientFactoryOptions) => {
|
|
14
21
|
provide: string;
|
|
@@ -37,6 +37,7 @@ exports.createNevoKafkaClient = void 0;
|
|
|
37
37
|
const microservices_1 = require("@nestjs/microservices");
|
|
38
38
|
const kafkajs_1 = require("kafkajs");
|
|
39
39
|
const nevo_kafka_client_1 = require("./nevo-kafka.client");
|
|
40
|
+
const common_1 = require("../../common");
|
|
40
41
|
async function createKafkaTopics(serviceNames, options) {
|
|
41
42
|
const host = process.env[options.kafkaHost || "KAFKA_HOST"] || "localhost";
|
|
42
43
|
const port = parseInt(process.env[options.kafkaPort || "KAFKA_PORT"] || "9092");
|
|
@@ -62,6 +63,7 @@ async function createKafkaTopics(serviceNames, options) {
|
|
|
62
63
|
const normalizedServiceName = serviceName.toLowerCase();
|
|
63
64
|
const eventsTopic = `${normalizedServiceName}-events`;
|
|
64
65
|
const replyTopic = `${eventsTopic}.reply`;
|
|
66
|
+
const subscriptionTopic = `${normalizedServiceName}${common_1.DEFAULT_SUBSCRIPTION_SUFFIX}`;
|
|
65
67
|
const topics = [];
|
|
66
68
|
if (!existingTopics.includes(eventsTopic)) {
|
|
67
69
|
topics.push({
|
|
@@ -87,8 +89,43 @@ async function createKafkaTopics(serviceNames, options) {
|
|
|
87
89
|
]
|
|
88
90
|
});
|
|
89
91
|
}
|
|
92
|
+
if (!existingTopics.includes(subscriptionTopic)) {
|
|
93
|
+
topics.push({
|
|
94
|
+
topic: subscriptionTopic,
|
|
95
|
+
numPartitions: 3,
|
|
96
|
+
replicationFactor: 1,
|
|
97
|
+
configEntries: [
|
|
98
|
+
{ name: "cleanup.policy", value: "delete" },
|
|
99
|
+
{ name: "retention.ms", value: "86400000" },
|
|
100
|
+
{ name: "max.message.bytes", value: "1000012" }
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
90
104
|
return topics;
|
|
91
105
|
});
|
|
106
|
+
if (!existingTopics.includes(common_1.DEFAULT_BROADCAST_TOPIC)) {
|
|
107
|
+
topicsToCreate.push({
|
|
108
|
+
topic: common_1.DEFAULT_BROADCAST_TOPIC,
|
|
109
|
+
numPartitions: 3,
|
|
110
|
+
replicationFactor: 1,
|
|
111
|
+
configEntries: [
|
|
112
|
+
{ name: "cleanup.policy", value: "delete" },
|
|
113
|
+
{ name: "retention.ms", value: "86400000" },
|
|
114
|
+
{ name: "max.message.bytes", value: "1000012" }
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (!existingTopics.includes(common_1.DEFAULT_DISCOVERY_TOPIC)) {
|
|
119
|
+
topicsToCreate.push({
|
|
120
|
+
topic: common_1.DEFAULT_DISCOVERY_TOPIC,
|
|
121
|
+
numPartitions: 1,
|
|
122
|
+
replicationFactor: 1,
|
|
123
|
+
configEntries: [
|
|
124
|
+
{ name: "cleanup.policy", value: "compact" },
|
|
125
|
+
{ name: "retention.ms", value: "86400000" }
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
92
129
|
if (topicsToCreate.length > 0) {
|
|
93
130
|
console.log(`[KafkaAdmin] Creating ${topicsToCreate.length} topics:`, topicsToCreate.map((t) => t.topic));
|
|
94
131
|
await admin.createTopics({
|
|
@@ -141,7 +178,12 @@ const createNevoKafkaClient = (serviceNames, options) => {
|
|
|
141
178
|
retryAttempts: 3,
|
|
142
179
|
brokerRetryTimeout: 1000,
|
|
143
180
|
debug: false,
|
|
144
|
-
timeoutMs: 20000
|
|
181
|
+
timeoutMs: 20000,
|
|
182
|
+
discovery: {
|
|
183
|
+
enabled: true,
|
|
184
|
+
heartbeatIntervalMs: 5000,
|
|
185
|
+
ttlMs: 15000
|
|
186
|
+
}
|
|
145
187
|
};
|
|
146
188
|
const mergedOptions = { ...defaultOptions, ...options };
|
|
147
189
|
return {
|
|
@@ -202,7 +244,11 @@ const createNevoKafkaClient = (serviceNames, options) => {
|
|
|
202
244
|
console.log(`[NevoKafkaClient] Created client for services: ${serviceNames.join(", ")}`);
|
|
203
245
|
return new nevo_kafka_client_1.NevoKafkaClient(kafkaClient, serviceNames, {
|
|
204
246
|
timeoutMs: mergedOptions.timeoutMs,
|
|
205
|
-
debug: mergedOptions.debug
|
|
247
|
+
debug: mergedOptions.debug,
|
|
248
|
+
serviceName: mergedOptions.serviceName || mergedOptions.clientIdPrefix,
|
|
249
|
+
authToken: mergedOptions.authToken,
|
|
250
|
+
brokers: [`${host}:${port}`],
|
|
251
|
+
discovery: mergedOptions.discovery
|
|
206
252
|
});
|
|
207
253
|
}
|
|
208
254
|
};
|
|
@@ -19,7 +19,8 @@ function KafkaSignalRouter(serviceType, options) {
|
|
|
19
19
|
return {
|
|
20
20
|
method: messageData.method,
|
|
21
21
|
params: messageData.params,
|
|
22
|
-
uuid: messageData.uuid
|
|
22
|
+
uuid: messageData.uuid,
|
|
23
|
+
meta: messageData.meta
|
|
23
24
|
};
|
|
24
25
|
}, (target, eventPattern, handlerName) => {
|
|
25
26
|
const originalMethod = target.prototype[handlerName];
|
|
@@ -1,16 +1,58 @@
|
|
|
1
1
|
import { ClientKafka } from "@nestjs/microservices";
|
|
2
|
+
import { Subscription, SubscriptionContext, SubscriptionOptions } from "../../common";
|
|
2
3
|
export interface NevoKafkaClientOptions {
|
|
3
4
|
timeoutMs?: number;
|
|
4
5
|
debug?: boolean;
|
|
6
|
+
serviceName?: string;
|
|
7
|
+
authToken?: string;
|
|
8
|
+
brokers?: string[];
|
|
9
|
+
backoff?: {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
baseMs?: number;
|
|
12
|
+
maxMs?: number;
|
|
13
|
+
maxAttempts?: number;
|
|
14
|
+
jitter?: boolean;
|
|
15
|
+
};
|
|
16
|
+
discovery?: {
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
heartbeatIntervalMs?: number;
|
|
19
|
+
ttlMs?: number;
|
|
20
|
+
};
|
|
5
21
|
}
|
|
6
22
|
export declare class NevoKafkaClient {
|
|
7
23
|
private readonly kafkaClient;
|
|
8
24
|
private readonly serviceNames;
|
|
9
25
|
private readonly timeoutMs;
|
|
10
26
|
private readonly debug;
|
|
27
|
+
private readonly serviceName?;
|
|
28
|
+
private readonly authToken?;
|
|
29
|
+
private readonly brokers;
|
|
30
|
+
private readonly backoffEnabled;
|
|
31
|
+
private readonly backoffBaseMs;
|
|
32
|
+
private readonly backoffMaxMs;
|
|
33
|
+
private readonly backoffMaxAttempts;
|
|
34
|
+
private readonly backoffJitter;
|
|
35
|
+
private readonly inFlight;
|
|
36
|
+
private readonly discoveryTopic;
|
|
37
|
+
private readonly broadcastTopic;
|
|
38
|
+
private readonly discoveryRegistry;
|
|
39
|
+
private readonly discoveryEnabled;
|
|
40
|
+
private readonly discoveryHeartbeatIntervalMs;
|
|
41
|
+
private readonly discoveryTtlMs;
|
|
42
|
+
private discoveryProducer?;
|
|
43
|
+
private discoveryConsumer?;
|
|
44
|
+
private discoveryTimer?;
|
|
45
|
+
private readonly subscriptionConsumers;
|
|
11
46
|
constructor(kafkaClient: ClientKafka, serviceNames: string[], options?: NevoKafkaClientOptions);
|
|
12
47
|
private createMessagePayload;
|
|
48
|
+
private waitForInFlightSlot;
|
|
13
49
|
query<T = any>(serviceName: string, method: string, params: any): Promise<T>;
|
|
14
50
|
emit(serviceName: string, method: string, params: any): Promise<void>;
|
|
15
51
|
getAvailableServices(): string[];
|
|
52
|
+
publish(serviceName: string, method: string, params: any): Promise<void>;
|
|
53
|
+
broadcast(method: string, params: any): Promise<void>;
|
|
54
|
+
subscribe<T = any>(serviceName: string, method: string, options: SubscriptionOptions | undefined, handler: (data: T, context: SubscriptionContext) => Promise<void> | void): Promise<Subscription>;
|
|
55
|
+
getDiscoveredServices(): import("../../common").DiscoveryEntry[];
|
|
56
|
+
isServiceAvailable(serviceName: string): boolean;
|
|
57
|
+
private initDiscovery;
|
|
16
58
|
}
|