@orion-js/echoes 4.4.1 → 4.5.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/dist/echo/index.d.ts +4 -4
- package/dist/errors.d.ts +26 -0
- package/dist/events/PulseManager.d.ts +2 -2
- package/dist/index.cjs +420 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +395 -175
- package/dist/index.js.map +1 -1
- package/dist/request/getSignature.d.ts +1 -1
- package/dist/requestsHandler/index.d.ts +18 -1
- package/dist/runtime.d.ts +49 -0
- package/dist/schema.d.ts +42 -0
- package/dist/startService/KafkaManager.d.ts +2 -2
- package/dist/types.d.ts +48 -19
- package/package.json +8 -12
package/dist/index.cjs
CHANGED
|
@@ -32,28 +32,40 @@ __export(index_exports, {
|
|
|
32
32
|
EchoEvent: () => EchoEvent,
|
|
33
33
|
EchoRequest: () => EchoRequest,
|
|
34
34
|
Echoes: () => Echoes,
|
|
35
|
+
EchoesUserError: () => EchoesUserError,
|
|
36
|
+
EchoesValidationError: () => EchoesValidationError,
|
|
37
|
+
cleanEchoesSchema: () => cleanEchoesSchema,
|
|
38
|
+
configureEchoesRuntime: () => configureEchoesRuntime,
|
|
35
39
|
createEchoEvent: () => createEchoEvent,
|
|
36
40
|
createEchoRequest: () => createEchoRequest,
|
|
41
|
+
createEchoesUserError: () => createEchoesUserError,
|
|
42
|
+
createEchoesValidationError: () => createEchoesValidationError,
|
|
37
43
|
echo: () => echo,
|
|
44
|
+
getEchoesContext: () => getEchoesContext,
|
|
45
|
+
getEchoesLogger: () => getEchoesLogger,
|
|
46
|
+
getEchoesRuntime: () => getEchoesRuntime,
|
|
38
47
|
getServiceEchoes: () => getServiceEchoes,
|
|
48
|
+
parseEchoesSchema: () => parseEchoesSchema,
|
|
39
49
|
publish: () => publish,
|
|
40
50
|
request: () => request,
|
|
51
|
+
runWithEchoesContext: () => runWithEchoesContext,
|
|
41
52
|
startService: () => startService,
|
|
42
|
-
stopService: () => stopService
|
|
53
|
+
stopService: () => stopService,
|
|
54
|
+
typedEchoesSchema: () => typedEchoesSchema
|
|
43
55
|
});
|
|
44
56
|
module.exports = __toCommonJS(index_exports);
|
|
45
57
|
|
|
46
|
-
// src/startService/index.ts
|
|
47
|
-
var import_http2 = require("@orion-js/http");
|
|
48
|
-
|
|
49
58
|
// src/config.ts
|
|
50
59
|
var config = {};
|
|
51
60
|
var config_default = config;
|
|
52
61
|
|
|
53
|
-
// src/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
// src/publish/index.ts
|
|
63
|
+
async function publish(options) {
|
|
64
|
+
if (!config_default.eventBus) {
|
|
65
|
+
throw new Error("You must initialize echoes configuration to use publish");
|
|
66
|
+
}
|
|
67
|
+
return await config_default.eventBus.publish(options);
|
|
68
|
+
}
|
|
57
69
|
|
|
58
70
|
// src/echo/deserialize.ts
|
|
59
71
|
function deserialize_default(serializedJavascript) {
|
|
@@ -66,14 +78,306 @@ function deserialize_default(serializedJavascript) {
|
|
|
66
78
|
|
|
67
79
|
// src/publish/serialize.ts
|
|
68
80
|
var import_serialize_javascript = __toESM(require("serialize-javascript"), 1);
|
|
69
|
-
var import_helpers = require("@orion-js/helpers");
|
|
70
81
|
function serialize_default(data) {
|
|
71
|
-
const
|
|
72
|
-
const serialized = (0, import_serialize_javascript.default)(cloned, { ignoreFunction: true });
|
|
82
|
+
const serialized = (0, import_serialize_javascript.default)(data, { ignoreFunction: true });
|
|
73
83
|
return serialized;
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
// src/runtime.ts
|
|
87
|
+
var import_node_async_hooks = require("async_hooks");
|
|
88
|
+
var import_node_crypto = require("crypto");
|
|
89
|
+
|
|
90
|
+
// src/errors.ts
|
|
91
|
+
var EchoesUserError = class extends Error {
|
|
92
|
+
isEchoesError = true;
|
|
93
|
+
isOrionError = true;
|
|
94
|
+
isUserError = true;
|
|
95
|
+
code;
|
|
96
|
+
extra;
|
|
97
|
+
constructor(code, message, extra) {
|
|
98
|
+
if (!message) {
|
|
99
|
+
message = code;
|
|
100
|
+
code = "error";
|
|
101
|
+
}
|
|
102
|
+
super(message);
|
|
103
|
+
this.name = "EchoesUserError";
|
|
104
|
+
this.code = code;
|
|
105
|
+
this.extra = extra;
|
|
106
|
+
}
|
|
107
|
+
getInfo() {
|
|
108
|
+
return { error: this.code, message: this.message, extra: this.extra };
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var EchoesValidationError = class extends Error {
|
|
112
|
+
isEchoesError = true;
|
|
113
|
+
isOrionError = true;
|
|
114
|
+
isValidationError = true;
|
|
115
|
+
code = "validationError";
|
|
116
|
+
validationErrors;
|
|
117
|
+
labels;
|
|
118
|
+
constructor(validationErrors, labels = {}) {
|
|
119
|
+
const printableErrors = Object.entries(validationErrors).map(([key, value]) => `${key}: ${value}`).join(", ");
|
|
120
|
+
super(`Validation Error: {${printableErrors}}`);
|
|
121
|
+
this.name = "EchoesValidationError";
|
|
122
|
+
this.validationErrors = validationErrors;
|
|
123
|
+
this.labels = Object.fromEntries(
|
|
124
|
+
Object.keys(validationErrors).filter((key) => labels[key]).map((key) => [key, labels[key]])
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
getInfo() {
|
|
128
|
+
return {
|
|
129
|
+
error: this.code,
|
|
130
|
+
message: "Validation Error",
|
|
131
|
+
validationErrors: this.validationErrors,
|
|
132
|
+
labels: this.labels
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/runtime.ts
|
|
138
|
+
var defaultLogger = {
|
|
139
|
+
debug: (message, metadata) => metadata === void 0 ? console.debug(message) : console.debug(message, metadata),
|
|
140
|
+
info: (message, metadata) => metadata === void 0 ? console.info(message) : console.info(message, metadata),
|
|
141
|
+
warn: (message, metadata) => metadata === void 0 ? console.warn(message) : console.warn(message, metadata),
|
|
142
|
+
error: (message, metadata) => metadata === void 0 ? console.error(message) : console.error(message, metadata)
|
|
143
|
+
};
|
|
144
|
+
var runtime = {};
|
|
145
|
+
var contextStorage = new import_node_async_hooks.AsyncLocalStorage();
|
|
146
|
+
function configureEchoesRuntime(adapter) {
|
|
147
|
+
const previous = runtime;
|
|
148
|
+
runtime = { ...runtime, ...adapter };
|
|
149
|
+
return () => {
|
|
150
|
+
runtime = previous;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function getEchoesRuntime() {
|
|
154
|
+
return runtime;
|
|
155
|
+
}
|
|
156
|
+
function getEchoesLogger() {
|
|
157
|
+
return runtime.logger || defaultLogger;
|
|
158
|
+
}
|
|
159
|
+
function getEchoesContext() {
|
|
160
|
+
return contextStorage.getStore();
|
|
161
|
+
}
|
|
162
|
+
async function runWithEchoesContext(context, callback) {
|
|
163
|
+
const contextWithId = { contextId: context.contextId || (0, import_node_crypto.randomUUID)(), ...context };
|
|
164
|
+
return await contextStorage.run(contextWithId, async () => {
|
|
165
|
+
if (runtime.runWithContext) {
|
|
166
|
+
return await runtime.runWithContext(contextWithId, callback);
|
|
167
|
+
}
|
|
168
|
+
return await callback();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function isSimpleSchemaLike(schema) {
|
|
172
|
+
return !!schema && typeof schema === "object" && typeof schema.clean === "function" && typeof schema.validate === "function";
|
|
173
|
+
}
|
|
174
|
+
function cloneValue(value) {
|
|
175
|
+
if (value === null || typeof value !== "object") return value;
|
|
176
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
177
|
+
if (Buffer.isBuffer(value)) return Buffer.from(value);
|
|
178
|
+
if (Array.isArray(value)) return value.map(cloneValue);
|
|
179
|
+
const prototype = Object.getPrototypeOf(value);
|
|
180
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
181
|
+
const result = {};
|
|
182
|
+
for (const [key, child] of Object.entries(value)) {
|
|
183
|
+
result[key] = cloneValue(child);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
async function cleanSimpleSchema(schema, value) {
|
|
188
|
+
const cloned = cloneValue(value);
|
|
189
|
+
return await schema.clean(cloned, { mutate: false });
|
|
190
|
+
}
|
|
191
|
+
async function cleanEchoesSchema(schema, value) {
|
|
192
|
+
if (isSimpleSchemaLike(schema)) {
|
|
193
|
+
return await cleanSimpleSchema(schema, value);
|
|
194
|
+
}
|
|
195
|
+
if (runtime.schema) {
|
|
196
|
+
return await runtime.schema.clean(schema, value);
|
|
197
|
+
}
|
|
198
|
+
throw new Error(
|
|
199
|
+
"Echoes received a schema it cannot execute. Use a SimpleSchema-compatible object or configure an Echoes schema adapter."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
async function parseEchoesSchema(schema, value) {
|
|
203
|
+
if (isSimpleSchemaLike(schema)) {
|
|
204
|
+
const cleaned = await cleanSimpleSchema(schema, value);
|
|
205
|
+
await schema.validate(cleaned);
|
|
206
|
+
return cleaned;
|
|
207
|
+
}
|
|
208
|
+
if (runtime.schema) {
|
|
209
|
+
return await runtime.schema.parse(schema, value);
|
|
210
|
+
}
|
|
211
|
+
throw new Error(
|
|
212
|
+
"Echoes received a schema it cannot execute. Use a SimpleSchema-compatible object or configure an Echoes schema adapter."
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function createEchoesValidationError(info) {
|
|
216
|
+
return runtime.createValidationError ? runtime.createValidationError(info) : new EchoesValidationError(info.validationErrors || {}, info.labels);
|
|
217
|
+
}
|
|
218
|
+
function createEchoesUserError(info) {
|
|
219
|
+
return runtime.createUserError ? runtime.createUserError(info) : new EchoesUserError(info.error, info.message, info.extra);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/request/getSignature.ts
|
|
223
|
+
var import_node_crypto2 = require("crypto");
|
|
224
|
+
|
|
225
|
+
// src/request/getPassword.ts
|
|
226
|
+
function getEchoesPassword() {
|
|
227
|
+
var _a, _b;
|
|
228
|
+
const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) || process.env.echoes_password || process.env.ECHOES_PASSWORD;
|
|
229
|
+
if (!secret) {
|
|
230
|
+
getEchoesLogger().warn(
|
|
231
|
+
'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return secret;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/request/getSignature.ts
|
|
238
|
+
function getSignature_default(_body) {
|
|
239
|
+
const password = getEchoesPassword();
|
|
240
|
+
return (0, import_node_crypto2.createHmac)("sha1", password || "").update("").digest("hex");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/request/getURL.ts
|
|
244
|
+
function getURL_default(serviceName) {
|
|
245
|
+
var _a, _b, _c;
|
|
246
|
+
if (serviceName.startsWith("http")) return serviceName;
|
|
247
|
+
const url = (_c = (_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.services) == null ? void 0 : _c[serviceName];
|
|
248
|
+
if (!url) {
|
|
249
|
+
throw new Error(`No URL found in echoes config for service ${serviceName}`);
|
|
250
|
+
}
|
|
251
|
+
return url;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/request/makeRequest.ts
|
|
255
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
256
|
+
var import_node_https = __toESM(require("https"), 1);
|
|
257
|
+
async function executeWithRetries(callback, retries = 0, delayMs = 200) {
|
|
258
|
+
try {
|
|
259
|
+
return await callback();
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (retries <= 0) throw error;
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
263
|
+
return await executeWithRetries(callback, retries - 1, delayMs);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function postJSON(urlValue, data, timeout, redirectsRemaining = 5) {
|
|
267
|
+
const url = new URL(urlValue);
|
|
268
|
+
const body = JSON.stringify(data);
|
|
269
|
+
const transport = url.protocol === "https:" ? import_node_https.default : import_node_http.default;
|
|
270
|
+
return await new Promise((resolve, reject) => {
|
|
271
|
+
const request2 = transport.request(
|
|
272
|
+
url,
|
|
273
|
+
{
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: {
|
|
276
|
+
"User-Agent": "Orionjs-Echoes/1.1",
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
"Content-Length": Buffer.byteLength(body)
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
(response) => {
|
|
282
|
+
const chunks = [];
|
|
283
|
+
const statusCode = response.statusCode || 0;
|
|
284
|
+
if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
|
|
285
|
+
response.resume();
|
|
286
|
+
if (redirectsRemaining <= 0) {
|
|
287
|
+
reject(new Error("Echoes request exceeded the redirect limit"));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
resolve(
|
|
291
|
+
postJSON(
|
|
292
|
+
new URL(response.headers.location, url).toString(),
|
|
293
|
+
data,
|
|
294
|
+
timeout,
|
|
295
|
+
redirectsRemaining - 1
|
|
296
|
+
)
|
|
297
|
+
);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
301
|
+
response.on("end", () => {
|
|
302
|
+
try {
|
|
303
|
+
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
304
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
305
|
+
reject(new Error(`Request failed with status code ${statusCode}`));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
resolve({
|
|
309
|
+
statusCode,
|
|
310
|
+
data: responseBody ? JSON.parse(responseBody) : {}
|
|
311
|
+
});
|
|
312
|
+
} catch (error) {
|
|
313
|
+
reject(error);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
request2.on("error", reject);
|
|
319
|
+
if (timeout) {
|
|
320
|
+
request2.setTimeout(timeout, () => {
|
|
321
|
+
request2.destroy(new Error(`Echoes request timed out after ${timeout}ms`));
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
request2.end(body);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
var makeRequest = async (options) => {
|
|
328
|
+
return await executeWithRetries(
|
|
329
|
+
() => postJSON(options.url, options.data, options.timeout),
|
|
330
|
+
options.retries || 0
|
|
331
|
+
);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// src/request/index.ts
|
|
335
|
+
async function request(options) {
|
|
336
|
+
var _a, _b;
|
|
337
|
+
const { method, service, params } = options;
|
|
338
|
+
const serializedParams = serialize_default(params);
|
|
339
|
+
const date = /* @__PURE__ */ new Date();
|
|
340
|
+
const body = { method, service, serializedParams, date };
|
|
341
|
+
const signature = getSignature_default(body);
|
|
342
|
+
try {
|
|
343
|
+
const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
|
|
344
|
+
const requestOptions = {
|
|
345
|
+
url: getURL_default(service),
|
|
346
|
+
retries: options.retries,
|
|
347
|
+
timeout: options.timeout,
|
|
348
|
+
data: {
|
|
349
|
+
body,
|
|
350
|
+
signature
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const result = await requestMaker(requestOptions);
|
|
354
|
+
if (result.statusCode !== 200) {
|
|
355
|
+
throw new Error(`Wrong status code ${result.statusCode}`);
|
|
356
|
+
}
|
|
357
|
+
const data = result.data;
|
|
358
|
+
if (data.error) {
|
|
359
|
+
const info = data.errorInfo;
|
|
360
|
+
if (info) {
|
|
361
|
+
if (data.isValidationError) {
|
|
362
|
+
throw createEchoesValidationError(info);
|
|
363
|
+
}
|
|
364
|
+
if (data.isUserError) {
|
|
365
|
+
throw createEchoesUserError(info);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`${data.error}`);
|
|
369
|
+
}
|
|
370
|
+
const response = deserialize_default(data.result);
|
|
371
|
+
return response;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
const caught = error;
|
|
374
|
+
if (caught.isOrionError || caught.isEchoesError) throw caught;
|
|
375
|
+
throw new Error(`Echoes request network error calling ${service}/${method}: ${caught.message}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
76
379
|
// src/startService/KafkaManager.ts
|
|
380
|
+
var import_node_crypto3 = require("crypto");
|
|
77
381
|
var HEARTBEAT_INTERVAL_SECONDS = 5;
|
|
78
382
|
var CHECK_JOIN_CONSUMER_INTERVAL_SECONDS = 30;
|
|
79
383
|
var DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY = 4;
|
|
@@ -91,11 +395,21 @@ var KafkaManager = class {
|
|
|
91
395
|
producerConnected = false;
|
|
92
396
|
interval;
|
|
93
397
|
constructor(options) {
|
|
94
|
-
this.kafka = new import_kafkajs.Kafka(options.client);
|
|
95
398
|
this.options = options;
|
|
96
399
|
}
|
|
97
400
|
async start(options) {
|
|
98
401
|
var _a;
|
|
402
|
+
let kafkaModule;
|
|
403
|
+
try {
|
|
404
|
+
kafkaModule = await import("kafkajs");
|
|
405
|
+
} catch (error) {
|
|
406
|
+
const wrapped = new Error(
|
|
407
|
+
"Echoes Kafka transport requires kafkajs to be installed in the application"
|
|
408
|
+
);
|
|
409
|
+
wrapped.cause = error;
|
|
410
|
+
throw wrapped;
|
|
411
|
+
}
|
|
412
|
+
this.kafka = new kafkaModule.Kafka(this.options.client);
|
|
99
413
|
this.onEvent = options.onEvent;
|
|
100
414
|
this.subscriptions = new Map(
|
|
101
415
|
options.subscriptions.map((subscription) => [subscription.topic, subscription])
|
|
@@ -113,7 +427,7 @@ var KafkaManager = class {
|
|
|
113
427
|
this.consumer = this.kafka.consumer(this.options.consumer);
|
|
114
428
|
this.consumerStarted = await this.conditionalStart();
|
|
115
429
|
if (this.consumerStarted) return;
|
|
116
|
-
|
|
430
|
+
getEchoesLogger().info("Echoes: Delaying consumer group join, waiting for conditions to be met");
|
|
117
431
|
this.interval = setInterval(async () => {
|
|
118
432
|
this.consumerStarted = await this.conditionalStart();
|
|
119
433
|
if (this.consumerStarted) clearInterval(this.interval);
|
|
@@ -131,7 +445,7 @@ var KafkaManager = class {
|
|
|
131
445
|
{
|
|
132
446
|
value: serialize_default({ params: options.params }),
|
|
133
447
|
headers: {
|
|
134
|
-
"echoes-event-id": (0,
|
|
448
|
+
"echoes-event-id": (0, import_node_crypto3.randomUUID)()
|
|
135
449
|
}
|
|
136
450
|
}
|
|
137
451
|
]
|
|
@@ -139,7 +453,8 @@ var KafkaManager = class {
|
|
|
139
453
|
}
|
|
140
454
|
async close() {
|
|
141
455
|
var _a, _b;
|
|
142
|
-
|
|
456
|
+
const logger = getEchoesLogger();
|
|
457
|
+
logger.warn("Echoes: Stopping Kafka transport");
|
|
143
458
|
if (this.interval) clearInterval(this.interval);
|
|
144
459
|
await Promise.all([
|
|
145
460
|
(_a = this.consumer) == null ? void 0 : _a.disconnect(),
|
|
@@ -149,6 +464,7 @@ var KafkaManager = class {
|
|
|
149
464
|
this.producerConnected = false;
|
|
150
465
|
}
|
|
151
466
|
async checkJoinConsumerGroupConditions() {
|
|
467
|
+
const logger = getEchoesLogger();
|
|
152
468
|
const admin = this.kafka.admin();
|
|
153
469
|
try {
|
|
154
470
|
await admin.connect();
|
|
@@ -156,7 +472,7 @@ var KafkaManager = class {
|
|
|
156
472
|
const groupDescriptions = await admin.describeGroups([groupId]);
|
|
157
473
|
const group = groupDescriptions.groups[0];
|
|
158
474
|
if (group.state === "Empty") {
|
|
159
|
-
|
|
475
|
+
logger.info(`Echoes: Consumer group ${groupId} is empty, joining`);
|
|
160
476
|
return true;
|
|
161
477
|
}
|
|
162
478
|
const topicsMetadata = await admin.fetchTopicMetadata({ topics: this.topics });
|
|
@@ -164,24 +480,24 @@ var KafkaManager = class {
|
|
|
164
480
|
(acc, topic) => acc + topic.partitions.length,
|
|
165
481
|
0
|
|
166
482
|
);
|
|
167
|
-
|
|
483
|
+
logger.info(
|
|
168
484
|
`Echoes: Consumer group ${groupId} has ${group.members.length} members and ${totalPartitions} partitions`
|
|
169
485
|
);
|
|
170
486
|
const partitionsRatio = this.options.membersToPartitionsRatio || DEFAULT_MEMBERS_TO_PARTITIONS_RATIO;
|
|
171
487
|
const partitionsThreshold = Math.ceil(totalPartitions * partitionsRatio);
|
|
172
488
|
if (partitionsThreshold > group.members.length) {
|
|
173
|
-
|
|
489
|
+
logger.info(
|
|
174
490
|
`Echoes: Consumer group ${groupId} has room for more members ${group.members.length}/${partitionsThreshold}, joining`
|
|
175
491
|
);
|
|
176
492
|
return true;
|
|
177
493
|
}
|
|
178
494
|
return false;
|
|
179
495
|
} catch (error) {
|
|
180
|
-
|
|
496
|
+
logger.error("Echoes: Error checking consumer group conditions, join", { error });
|
|
181
497
|
return true;
|
|
182
498
|
} finally {
|
|
183
499
|
await admin.disconnect().catch((error) => {
|
|
184
|
-
|
|
500
|
+
logger.error("Echoes: Error disconnecting admin client", { error });
|
|
185
501
|
});
|
|
186
502
|
}
|
|
187
503
|
}
|
|
@@ -201,19 +517,20 @@ var KafkaManager = class {
|
|
|
201
517
|
return false;
|
|
202
518
|
}
|
|
203
519
|
async handleMessage(params) {
|
|
520
|
+
const logger = getEchoesLogger();
|
|
204
521
|
const subscription = this.subscriptions.get(params.topic);
|
|
205
522
|
if (!subscription) {
|
|
206
|
-
|
|
523
|
+
logger.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
|
|
207
524
|
return;
|
|
208
525
|
}
|
|
209
526
|
let intervalsCount = 0;
|
|
210
527
|
const heartbeatInterval = setInterval(async () => {
|
|
211
528
|
await params.heartbeat().catch((error) => {
|
|
212
|
-
|
|
529
|
+
logger.warn(`Echoes: Error sending heartbeat: ${error.message}`);
|
|
213
530
|
});
|
|
214
531
|
intervalsCount++;
|
|
215
532
|
if (intervalsCount * HEARTBEAT_INTERVAL_SECONDS % 30 === 0) {
|
|
216
|
-
|
|
533
|
+
logger.warn(
|
|
217
534
|
`Echoes: Event is taking too long to process: ${params.topic} ${intervalsCount * HEARTBEAT_INTERVAL_SECONDS}s`
|
|
218
535
|
);
|
|
219
536
|
}
|
|
@@ -225,7 +542,7 @@ var KafkaManager = class {
|
|
|
225
542
|
try {
|
|
226
543
|
await this.handleRetries(subscription, params, error);
|
|
227
544
|
} catch (retryError) {
|
|
228
|
-
|
|
545
|
+
logger.error("Echoes: error processing a message", {
|
|
229
546
|
error: retryError,
|
|
230
547
|
topic: params.topic
|
|
231
548
|
});
|
|
@@ -238,6 +555,9 @@ var KafkaManager = class {
|
|
|
238
555
|
createReceivedEvent(params) {
|
|
239
556
|
var _a, _b, _c, _d;
|
|
240
557
|
const { message, topic, partition } = params;
|
|
558
|
+
if (!message.value) {
|
|
559
|
+
throw new Error(`Echoes received an empty Kafka message for ${topic}`);
|
|
560
|
+
}
|
|
241
561
|
const data = deserialize_default(message.value.toString());
|
|
242
562
|
const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
243
563
|
const timestamp = Number.parseInt(message.timestamp || "", 10);
|
|
@@ -254,6 +574,7 @@ var KafkaManager = class {
|
|
|
254
574
|
}
|
|
255
575
|
async handleRetries(subscription, params, error) {
|
|
256
576
|
var _a, _b;
|
|
577
|
+
const logger = getEchoesLogger();
|
|
257
578
|
const { message, topic } = params;
|
|
258
579
|
const retries = Number.parseInt(((_b = (_a = message == null ? void 0 : message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
259
580
|
if (subscription.attemptsBeforeDeadLetter === void 0 || subscription.attemptsBeforeDeadLetter === null) {
|
|
@@ -262,6 +583,7 @@ var KafkaManager = class {
|
|
|
262
583
|
const maxRetries = subscription.attemptsBeforeDeadLetter || 0;
|
|
263
584
|
const exceededMaxRetries = retries >= maxRetries;
|
|
264
585
|
const nextTopic = exceededMaxRetries ? `DLQ-${topic}` : topic;
|
|
586
|
+
if (!message.value) throw error;
|
|
265
587
|
await this.producer.send({
|
|
266
588
|
topic: nextTopic,
|
|
267
589
|
messages: [
|
|
@@ -276,19 +598,18 @@ var KafkaManager = class {
|
|
|
276
598
|
]
|
|
277
599
|
});
|
|
278
600
|
if (exceededMaxRetries) {
|
|
279
|
-
|
|
601
|
+
logger.error(
|
|
280
602
|
"Echoes: a message has reached the maximum number of retries, sending it to DLQ",
|
|
281
603
|
{ topic: nextTopic }
|
|
282
604
|
);
|
|
283
605
|
} else {
|
|
284
|
-
|
|
606
|
+
logger.warn("Echoes: a retryable message failed", { error, topic: nextTopic });
|
|
285
607
|
}
|
|
286
608
|
}
|
|
287
609
|
};
|
|
288
610
|
var KafkaManager_default = KafkaManager;
|
|
289
611
|
|
|
290
612
|
// src/events/EventBus.ts
|
|
291
|
-
var import_logger2 = require("@orion-js/logger");
|
|
292
613
|
var EventBus = class {
|
|
293
614
|
echoes;
|
|
294
615
|
transports;
|
|
@@ -348,9 +669,10 @@ var EventBus = class {
|
|
|
348
669
|
await Promise.all(this.startedTransports.map((transport) => transport.close()));
|
|
349
670
|
}
|
|
350
671
|
async handleEvent(event) {
|
|
672
|
+
const logger = getEchoesLogger();
|
|
351
673
|
const echo2 = this.echoes[event.topic];
|
|
352
674
|
if (!echo2 || echo2.type !== "event") {
|
|
353
|
-
|
|
675
|
+
logger.warn(`Echoes: Received a message for an unknown topic: ${event.topic}, ignoring it`);
|
|
354
676
|
return;
|
|
355
677
|
}
|
|
356
678
|
if (event.transport === "kafka" && event.context) {
|
|
@@ -478,6 +800,14 @@ function createEventBus(options) {
|
|
|
478
800
|
});
|
|
479
801
|
}
|
|
480
802
|
|
|
803
|
+
// src/requestsHandler/checkSignature.ts
|
|
804
|
+
function checkSignature_default(body, signature) {
|
|
805
|
+
const generatedSignature = getSignature_default(body);
|
|
806
|
+
if (generatedSignature !== signature) {
|
|
807
|
+
throw new Error("Echoes invalid signature");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
481
811
|
// src/requestsHandler/getEcho.ts
|
|
482
812
|
function getEcho_default(method) {
|
|
483
813
|
const echo2 = config_default.echoes[method];
|
|
@@ -490,72 +820,29 @@ function getEcho_default(method) {
|
|
|
490
820
|
return echo2;
|
|
491
821
|
}
|
|
492
822
|
|
|
493
|
-
// src/request/getSignature.ts
|
|
494
|
-
var import_jssha = __toESM(require("jssha"), 1);
|
|
495
|
-
|
|
496
|
-
// src/request/getPassword.ts
|
|
497
|
-
var import_logger3 = require("@orion-js/logger");
|
|
498
|
-
var import_env = require("@orion-js/env");
|
|
499
|
-
function getEchoesPassword() {
|
|
500
|
-
var _a, _b;
|
|
501
|
-
const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) || (0, import_env.internalGetEnv)("echoes_password", "ECHOES_PASSWORD");
|
|
502
|
-
if (!secret) {
|
|
503
|
-
import_logger3.logger.warn(
|
|
504
|
-
'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
return secret;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// src/request/getSignature.ts
|
|
511
|
-
function getSignature_default(body) {
|
|
512
|
-
const password = getEchoesPassword();
|
|
513
|
-
const shaObj = new import_jssha.default("SHA-1", "TEXT");
|
|
514
|
-
shaObj.setHMACKey(password, "TEXT");
|
|
515
|
-
shaObj.update(body);
|
|
516
|
-
return shaObj.getHMAC("HEX");
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// src/requestsHandler/checkSignature.ts
|
|
520
|
-
function checkSignature_default(body, signature) {
|
|
521
|
-
const generatedSignature = getSignature_default(body);
|
|
522
|
-
if (generatedSignature !== signature) {
|
|
523
|
-
throw new Error("Echoes invalid signature");
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
823
|
// src/requestsHandler/index.ts
|
|
528
|
-
var
|
|
529
|
-
var requestsHandler_default = (options) => (0, import_http.route)({
|
|
824
|
+
var requestsHandler_default = (options) => ({
|
|
530
825
|
method: "post",
|
|
531
826
|
path: options.requests.handlerPath || "/echoes-services",
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
limit: "10mb"
|
|
535
|
-
},
|
|
536
|
-
async resolve(req) {
|
|
827
|
+
bodyLimit: "10mb",
|
|
828
|
+
async handle(requestBody) {
|
|
537
829
|
try {
|
|
538
|
-
const { body, signature } =
|
|
830
|
+
const { body, signature } = requestBody;
|
|
539
831
|
checkSignature_default(body, signature);
|
|
540
832
|
const { method, serializedParams } = body;
|
|
541
833
|
const echo2 = getEcho_default(method);
|
|
542
834
|
const result = await echo2.onRequest(serializedParams);
|
|
543
|
-
return {
|
|
544
|
-
body: {
|
|
545
|
-
result: serialize_default(result)
|
|
546
|
-
}
|
|
547
|
-
};
|
|
835
|
+
return { result: serialize_default(result) };
|
|
548
836
|
} catch (error) {
|
|
549
|
-
|
|
550
|
-
|
|
837
|
+
const caught = error;
|
|
838
|
+
if (!caught.getInfo) {
|
|
839
|
+
getEchoesLogger().error("Error at echo requests handler:", { error: caught });
|
|
551
840
|
}
|
|
552
841
|
return {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
isUserError: !!error.isUserError
|
|
558
|
-
}
|
|
842
|
+
error: caught.message,
|
|
843
|
+
errorInfo: caught.getInfo ? caught.getInfo() : null,
|
|
844
|
+
isValidationError: !!caught.isValidationError,
|
|
845
|
+
isUserError: !!caught.isUserError
|
|
559
846
|
};
|
|
560
847
|
}
|
|
561
848
|
}
|
|
@@ -567,7 +854,13 @@ async function startService(options) {
|
|
|
567
854
|
config_default.echoes = options.echoes;
|
|
568
855
|
if (options.requests) {
|
|
569
856
|
config_default.requests = options.requests;
|
|
570
|
-
|
|
857
|
+
const registerHandler = options.requests.registerHandler || getEchoesRuntime().registerRequestHandler;
|
|
858
|
+
if (!registerHandler) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
"Echoes requests require requests.registerHandler in standalone servers. Orion applications can import @orion-js/echoes-orion once during startup."
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
await registerHandler(requestsHandler_default(options));
|
|
571
864
|
}
|
|
572
865
|
const nextEventBus = createEventBus(options);
|
|
573
866
|
if (nextEventBus) {
|
|
@@ -578,115 +871,22 @@ async function startService(options) {
|
|
|
578
871
|
}
|
|
579
872
|
async function stopService() {
|
|
580
873
|
if (eventBus) {
|
|
581
|
-
|
|
874
|
+
const logger = getEchoesLogger();
|
|
875
|
+
logger.info("Stopping Echoes...");
|
|
582
876
|
await eventBus.close();
|
|
583
877
|
eventBus = null;
|
|
584
878
|
config_default.eventBus = void 0;
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// src/publish/index.ts
|
|
590
|
-
async function publish(options) {
|
|
591
|
-
if (!config_default.eventBus) {
|
|
592
|
-
throw new Error("You must initialize echoes configuration to use publish");
|
|
593
|
-
}
|
|
594
|
-
return await config_default.eventBus.publish(options);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// src/request/getURL.ts
|
|
598
|
-
function getURL_default(serviceName) {
|
|
599
|
-
var _a, _b;
|
|
600
|
-
if (serviceName.startsWith("http")) return serviceName;
|
|
601
|
-
const url = (_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.services[serviceName];
|
|
602
|
-
if (!url) {
|
|
603
|
-
throw new Error(`No URL found in echoes config for service ${serviceName}`);
|
|
604
|
-
}
|
|
605
|
-
return url;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// src/request/makeRequest.ts
|
|
609
|
-
var import_axios = __toESM(require("axios"), 1);
|
|
610
|
-
var import_helpers2 = require("@orion-js/helpers");
|
|
611
|
-
var makeRequest = async (options) => {
|
|
612
|
-
const result = await (0, import_helpers2.executeWithRetries)(
|
|
613
|
-
async () => {
|
|
614
|
-
return await (0, import_axios.default)({
|
|
615
|
-
method: "post",
|
|
616
|
-
url: options.url,
|
|
617
|
-
timeout: options.timeout,
|
|
618
|
-
headers: {
|
|
619
|
-
"User-Agent": "Orionjs-Echoes/1.1"
|
|
620
|
-
},
|
|
621
|
-
data: options.data
|
|
622
|
-
});
|
|
623
|
-
},
|
|
624
|
-
options.retries,
|
|
625
|
-
200
|
|
626
|
-
);
|
|
627
|
-
return {
|
|
628
|
-
data: result.data,
|
|
629
|
-
statusCode: result.status
|
|
630
|
-
};
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
// src/request/index.ts
|
|
634
|
-
var import_schema = require("@orion-js/schema");
|
|
635
|
-
var import_helpers3 = require("@orion-js/helpers");
|
|
636
|
-
async function request(options) {
|
|
637
|
-
var _a, _b;
|
|
638
|
-
const { method, service, params } = options;
|
|
639
|
-
const serializedParams = serialize_default(params);
|
|
640
|
-
const date = /* @__PURE__ */ new Date();
|
|
641
|
-
const body = { method, service, serializedParams, date };
|
|
642
|
-
const signature = getSignature_default(body);
|
|
643
|
-
try {
|
|
644
|
-
const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
|
|
645
|
-
const requestOptions = {
|
|
646
|
-
url: getURL_default(service),
|
|
647
|
-
retries: options.retries,
|
|
648
|
-
timeout: options.timeout,
|
|
649
|
-
data: {
|
|
650
|
-
body,
|
|
651
|
-
signature
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
const result = await requestMaker(requestOptions);
|
|
655
|
-
if (result.statusCode !== 200) {
|
|
656
|
-
throw new Error(`Wrong status code ${result.statusCode}`);
|
|
657
|
-
}
|
|
658
|
-
const data = result.data;
|
|
659
|
-
if (data.error) {
|
|
660
|
-
const info = data.errorInfo;
|
|
661
|
-
if (info) {
|
|
662
|
-
if (data.isValidationError) {
|
|
663
|
-
throw new import_schema.ValidationError(info.validationErrors);
|
|
664
|
-
}
|
|
665
|
-
if (data.isUserError) {
|
|
666
|
-
throw new import_helpers3.UserError(info.error, info.message, info.extra);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
throw new Error(`${data.error}`);
|
|
670
|
-
}
|
|
671
|
-
const response = deserialize_default(data.result);
|
|
672
|
-
return response;
|
|
673
|
-
} catch (error) {
|
|
674
|
-
if (error.isOrionError) throw error;
|
|
675
|
-
throw new Error(`Echoes request network error calling ${service}/${method}: ${error.message}`);
|
|
879
|
+
logger.info("Echoes stopped");
|
|
676
880
|
}
|
|
677
881
|
}
|
|
678
882
|
|
|
679
|
-
// src/service/index.ts
|
|
680
|
-
var import_logger4 = require("@orion-js/logger");
|
|
681
|
-
|
|
682
883
|
// src/echo/index.ts
|
|
683
|
-
var import_schema2 = require("@orion-js/schema");
|
|
684
884
|
var echo = function createNewEcho(options) {
|
|
685
885
|
const resolve = async (params, context) => {
|
|
686
|
-
const cleaned = options.params ? await (
|
|
886
|
+
const cleaned = options.params ? await parseEchoesSchema(options.params, params) : params ?? {};
|
|
687
887
|
const result = await options.resolve(cleaned, context);
|
|
688
888
|
if (options.returns) {
|
|
689
|
-
return await (
|
|
889
|
+
return await cleanEchoesSchema(options.returns, result);
|
|
690
890
|
}
|
|
691
891
|
return result;
|
|
692
892
|
};
|
|
@@ -713,6 +913,9 @@ var echo = function createNewEcho(options) {
|
|
|
713
913
|
onMessage: async (messageData) => {
|
|
714
914
|
var _a, _b, _c, _d;
|
|
715
915
|
const { message } = messageData;
|
|
916
|
+
if (!message.value) {
|
|
917
|
+
throw new Error(`Echoes received an empty Kafka message for ${messageData.topic}`);
|
|
918
|
+
}
|
|
716
919
|
const data = deserialize_default(message.value.toString());
|
|
717
920
|
const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
718
921
|
const timestamp = Number(message.timestamp);
|
|
@@ -743,15 +946,21 @@ function createEchoEvent(options) {
|
|
|
743
946
|
return echo({ ...options, type: "event" });
|
|
744
947
|
}
|
|
745
948
|
|
|
949
|
+
// src/schema.ts
|
|
950
|
+
function typedEchoesSchema(schema) {
|
|
951
|
+
return schema;
|
|
952
|
+
}
|
|
953
|
+
|
|
746
954
|
// src/service/index.ts
|
|
747
|
-
var import_services = require("@orion-js/services");
|
|
748
955
|
var serviceMetadata = /* @__PURE__ */ new WeakMap();
|
|
749
956
|
var echoesMetadata = /* @__PURE__ */ new WeakMap();
|
|
750
957
|
var echoEntriesByClass = /* @__PURE__ */ new Map();
|
|
958
|
+
var standaloneInstances = /* @__PURE__ */ new WeakMap();
|
|
751
959
|
var pendingEchoEntries = {};
|
|
752
960
|
function Echoes() {
|
|
753
961
|
return (target, context) => {
|
|
754
|
-
|
|
962
|
+
var _a, _b;
|
|
963
|
+
(_b = (_a = getEchoesRuntime()).decorateService) == null ? void 0 : _b.call(_a, target, context);
|
|
755
964
|
serviceMetadata.set(target, { _serviceType: "echoes" });
|
|
756
965
|
if (Object.keys(pendingEchoEntries).length > 0) {
|
|
757
966
|
echoEntriesByClass.set(target, pendingEchoEntries);
|
|
@@ -768,7 +977,7 @@ function EchoEvent(options = {}) {
|
|
|
768
977
|
return createEchoEvent({
|
|
769
978
|
...options,
|
|
770
979
|
resolve: async (params, contextData) => {
|
|
771
|
-
return await (
|
|
980
|
+
return await runWithEchoesContext(
|
|
772
981
|
{
|
|
773
982
|
controllerType: "echo",
|
|
774
983
|
echoName: propertyKey,
|
|
@@ -797,7 +1006,7 @@ function EchoRequest(options = {}) {
|
|
|
797
1006
|
return createEchoRequest({
|
|
798
1007
|
...options,
|
|
799
1008
|
resolve: async (params, contextData) => {
|
|
800
|
-
return await (
|
|
1009
|
+
return await runWithEchoesContext(
|
|
801
1010
|
{
|
|
802
1011
|
controllerType: "echo",
|
|
803
1012
|
echoName: propertyKey,
|
|
@@ -827,7 +1036,18 @@ function initializeEchoesIfNeeded(instance) {
|
|
|
827
1036
|
echoesMetadata.set(instance, echoes);
|
|
828
1037
|
}
|
|
829
1038
|
function getServiceEchoes(target) {
|
|
830
|
-
|
|
1039
|
+
let instance;
|
|
1040
|
+
if (typeof target !== "function") {
|
|
1041
|
+
instance = target;
|
|
1042
|
+
} else if (getEchoesRuntime().getInstance) {
|
|
1043
|
+
instance = getEchoesRuntime().getInstance(target);
|
|
1044
|
+
} else {
|
|
1045
|
+
instance = standaloneInstances.get(target);
|
|
1046
|
+
if (!instance) {
|
|
1047
|
+
instance = new target();
|
|
1048
|
+
standaloneInstances.set(target, instance);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
831
1051
|
if (!serviceMetadata.has(instance.constructor)) {
|
|
832
1052
|
throw new Error("You must pass a class decorated with @Echoes to getServiceEchoes");
|
|
833
1053
|
}
|
|
@@ -844,13 +1064,25 @@ function getServiceEchoes(target) {
|
|
|
844
1064
|
EchoEvent,
|
|
845
1065
|
EchoRequest,
|
|
846
1066
|
Echoes,
|
|
1067
|
+
EchoesUserError,
|
|
1068
|
+
EchoesValidationError,
|
|
1069
|
+
cleanEchoesSchema,
|
|
1070
|
+
configureEchoesRuntime,
|
|
847
1071
|
createEchoEvent,
|
|
848
1072
|
createEchoRequest,
|
|
1073
|
+
createEchoesUserError,
|
|
1074
|
+
createEchoesValidationError,
|
|
849
1075
|
echo,
|
|
1076
|
+
getEchoesContext,
|
|
1077
|
+
getEchoesLogger,
|
|
1078
|
+
getEchoesRuntime,
|
|
850
1079
|
getServiceEchoes,
|
|
1080
|
+
parseEchoesSchema,
|
|
851
1081
|
publish,
|
|
852
1082
|
request,
|
|
1083
|
+
runWithEchoesContext,
|
|
853
1084
|
startService,
|
|
854
|
-
stopService
|
|
1085
|
+
stopService,
|
|
1086
|
+
typedEchoesSchema
|
|
855
1087
|
});
|
|
856
1088
|
//# sourceMappingURL=index.cjs.map
|