@orion-js/echoes 4.4.0 → 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 +5 -5
- package/dist/errors.d.ts +26 -0
- package/dist/events/EventBus.d.ts +22 -0
- package/dist/events/EventTransport.d.ts +17 -0
- package/dist/events/PulseManager.d.ts +14 -0
- package/dist/events/createEventBus.d.ts +10 -0
- package/dist/index.cjs +744 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +731 -234
- package/dist/index.js.map +1 -1
- package/dist/publish/index.d.ts +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 +25 -19
- package/dist/types.d.ts +129 -16
- package/package.json +15 -10
package/dist/index.js
CHANGED
|
@@ -2,38 +2,175 @@
|
|
|
2
2
|
var config = {};
|
|
3
3
|
var config_default = config;
|
|
4
4
|
|
|
5
|
-
// src/
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
throw new Error(`Echo named ${method} not found in this service`);
|
|
5
|
+
// src/publish/index.ts
|
|
6
|
+
async function publish(options) {
|
|
7
|
+
if (!config_default.eventBus) {
|
|
8
|
+
throw new Error("You must initialize echoes configuration to use publish");
|
|
10
9
|
}
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
return await config_default.eventBus.publish(options);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/echo/deserialize.ts
|
|
14
|
+
function deserialize_default(serializedJavascript) {
|
|
15
|
+
try {
|
|
16
|
+
return eval("(" + serializedJavascript + ")");
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error("Error deserializing echo message");
|
|
13
19
|
}
|
|
14
|
-
return echo2;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
// src/publish/serialize.ts
|
|
18
23
|
import serialize from "serialize-javascript";
|
|
19
|
-
import { clone } from "@orion-js/helpers";
|
|
20
24
|
function serialize_default(data) {
|
|
21
|
-
const
|
|
22
|
-
const serialized = serialize(cloned, { ignoreFunction: true });
|
|
25
|
+
const serialized = serialize(data, { ignoreFunction: true });
|
|
23
26
|
return serialized;
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
// src/runtime.ts
|
|
30
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
31
|
+
import { randomUUID } from "crypto";
|
|
32
|
+
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
var EchoesUserError = class extends Error {
|
|
35
|
+
isEchoesError = true;
|
|
36
|
+
isOrionError = true;
|
|
37
|
+
isUserError = true;
|
|
38
|
+
code;
|
|
39
|
+
extra;
|
|
40
|
+
constructor(code, message, extra) {
|
|
41
|
+
if (!message) {
|
|
42
|
+
message = code;
|
|
43
|
+
code = "error";
|
|
44
|
+
}
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "EchoesUserError";
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.extra = extra;
|
|
49
|
+
}
|
|
50
|
+
getInfo() {
|
|
51
|
+
return { error: this.code, message: this.message, extra: this.extra };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var EchoesValidationError = class extends Error {
|
|
55
|
+
isEchoesError = true;
|
|
56
|
+
isOrionError = true;
|
|
57
|
+
isValidationError = true;
|
|
58
|
+
code = "validationError";
|
|
59
|
+
validationErrors;
|
|
60
|
+
labels;
|
|
61
|
+
constructor(validationErrors, labels = {}) {
|
|
62
|
+
const printableErrors = Object.entries(validationErrors).map(([key, value]) => `${key}: ${value}`).join(", ");
|
|
63
|
+
super(`Validation Error: {${printableErrors}}`);
|
|
64
|
+
this.name = "EchoesValidationError";
|
|
65
|
+
this.validationErrors = validationErrors;
|
|
66
|
+
this.labels = Object.fromEntries(
|
|
67
|
+
Object.keys(validationErrors).filter((key) => labels[key]).map((key) => [key, labels[key]])
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
getInfo() {
|
|
71
|
+
return {
|
|
72
|
+
error: this.code,
|
|
73
|
+
message: "Validation Error",
|
|
74
|
+
validationErrors: this.validationErrors,
|
|
75
|
+
labels: this.labels
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/runtime.ts
|
|
81
|
+
var defaultLogger = {
|
|
82
|
+
debug: (message, metadata) => metadata === void 0 ? console.debug(message) : console.debug(message, metadata),
|
|
83
|
+
info: (message, metadata) => metadata === void 0 ? console.info(message) : console.info(message, metadata),
|
|
84
|
+
warn: (message, metadata) => metadata === void 0 ? console.warn(message) : console.warn(message, metadata),
|
|
85
|
+
error: (message, metadata) => metadata === void 0 ? console.error(message) : console.error(message, metadata)
|
|
86
|
+
};
|
|
87
|
+
var runtime = {};
|
|
88
|
+
var contextStorage = new AsyncLocalStorage();
|
|
89
|
+
function configureEchoesRuntime(adapter) {
|
|
90
|
+
const previous = runtime;
|
|
91
|
+
runtime = { ...runtime, ...adapter };
|
|
92
|
+
return () => {
|
|
93
|
+
runtime = previous;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function getEchoesRuntime() {
|
|
97
|
+
return runtime;
|
|
98
|
+
}
|
|
99
|
+
function getEchoesLogger() {
|
|
100
|
+
return runtime.logger || defaultLogger;
|
|
101
|
+
}
|
|
102
|
+
function getEchoesContext() {
|
|
103
|
+
return contextStorage.getStore();
|
|
104
|
+
}
|
|
105
|
+
async function runWithEchoesContext(context, callback) {
|
|
106
|
+
const contextWithId = { contextId: context.contextId || randomUUID(), ...context };
|
|
107
|
+
return await contextStorage.run(contextWithId, async () => {
|
|
108
|
+
if (runtime.runWithContext) {
|
|
109
|
+
return await runtime.runWithContext(contextWithId, callback);
|
|
110
|
+
}
|
|
111
|
+
return await callback();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function isSimpleSchemaLike(schema) {
|
|
115
|
+
return !!schema && typeof schema === "object" && typeof schema.clean === "function" && typeof schema.validate === "function";
|
|
116
|
+
}
|
|
117
|
+
function cloneValue(value) {
|
|
118
|
+
if (value === null || typeof value !== "object") return value;
|
|
119
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
120
|
+
if (Buffer.isBuffer(value)) return Buffer.from(value);
|
|
121
|
+
if (Array.isArray(value)) return value.map(cloneValue);
|
|
122
|
+
const prototype = Object.getPrototypeOf(value);
|
|
123
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
124
|
+
const result = {};
|
|
125
|
+
for (const [key, child] of Object.entries(value)) {
|
|
126
|
+
result[key] = cloneValue(child);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
async function cleanSimpleSchema(schema, value) {
|
|
131
|
+
const cloned = cloneValue(value);
|
|
132
|
+
return await schema.clean(cloned, { mutate: false });
|
|
133
|
+
}
|
|
134
|
+
async function cleanEchoesSchema(schema, value) {
|
|
135
|
+
if (isSimpleSchemaLike(schema)) {
|
|
136
|
+
return await cleanSimpleSchema(schema, value);
|
|
137
|
+
}
|
|
138
|
+
if (runtime.schema) {
|
|
139
|
+
return await runtime.schema.clean(schema, value);
|
|
140
|
+
}
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Echoes received a schema it cannot execute. Use a SimpleSchema-compatible object or configure an Echoes schema adapter."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
async function parseEchoesSchema(schema, value) {
|
|
146
|
+
if (isSimpleSchemaLike(schema)) {
|
|
147
|
+
const cleaned = await cleanSimpleSchema(schema, value);
|
|
148
|
+
await schema.validate(cleaned);
|
|
149
|
+
return cleaned;
|
|
150
|
+
}
|
|
151
|
+
if (runtime.schema) {
|
|
152
|
+
return await runtime.schema.parse(schema, value);
|
|
153
|
+
}
|
|
154
|
+
throw new Error(
|
|
155
|
+
"Echoes received a schema it cannot execute. Use a SimpleSchema-compatible object or configure an Echoes schema adapter."
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
function createEchoesValidationError(info) {
|
|
159
|
+
return runtime.createValidationError ? runtime.createValidationError(info) : new EchoesValidationError(info.validationErrors || {}, info.labels);
|
|
160
|
+
}
|
|
161
|
+
function createEchoesUserError(info) {
|
|
162
|
+
return runtime.createUserError ? runtime.createUserError(info) : new EchoesUserError(info.error, info.message, info.extra);
|
|
163
|
+
}
|
|
164
|
+
|
|
26
165
|
// src/request/getSignature.ts
|
|
27
|
-
import
|
|
166
|
+
import { createHmac } from "crypto";
|
|
28
167
|
|
|
29
168
|
// src/request/getPassword.ts
|
|
30
|
-
import { logger } from "@orion-js/logger";
|
|
31
|
-
import { internalGetEnv } from "@orion-js/env";
|
|
32
169
|
function getEchoesPassword() {
|
|
33
170
|
var _a, _b;
|
|
34
|
-
const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) ||
|
|
171
|
+
const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) || process.env.echoes_password || process.env.ECHOES_PASSWORD;
|
|
35
172
|
if (!secret) {
|
|
36
|
-
|
|
173
|
+
getEchoesLogger().warn(
|
|
37
174
|
'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
|
|
38
175
|
);
|
|
39
176
|
}
|
|
@@ -41,89 +178,244 @@ function getEchoesPassword() {
|
|
|
41
178
|
}
|
|
42
179
|
|
|
43
180
|
// src/request/getSignature.ts
|
|
44
|
-
function getSignature_default(
|
|
181
|
+
function getSignature_default(_body) {
|
|
45
182
|
const password = getEchoesPassword();
|
|
46
|
-
|
|
47
|
-
shaObj.setHMACKey(password, "TEXT");
|
|
48
|
-
shaObj.update(body);
|
|
49
|
-
return shaObj.getHMAC("HEX");
|
|
183
|
+
return createHmac("sha1", password || "").update("").digest("hex");
|
|
50
184
|
}
|
|
51
185
|
|
|
52
|
-
// src/
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
186
|
+
// src/request/getURL.ts
|
|
187
|
+
function getURL_default(serviceName) {
|
|
188
|
+
var _a, _b, _c;
|
|
189
|
+
if (serviceName.startsWith("http")) return serviceName;
|
|
190
|
+
const url = (_c = (_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.services) == null ? void 0 : _c[serviceName];
|
|
191
|
+
if (!url) {
|
|
192
|
+
throw new Error(`No URL found in echoes config for service ${serviceName}`);
|
|
57
193
|
}
|
|
194
|
+
return url;
|
|
58
195
|
}
|
|
59
196
|
|
|
60
|
-
// src/
|
|
61
|
-
import
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
197
|
+
// src/request/makeRequest.ts
|
|
198
|
+
import http from "http";
|
|
199
|
+
import https from "https";
|
|
200
|
+
async function executeWithRetries(callback, retries = 0, delayMs = 200) {
|
|
201
|
+
try {
|
|
202
|
+
return await callback();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (retries <= 0) throw error;
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
206
|
+
return await executeWithRetries(callback, retries - 1, delayMs);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function postJSON(urlValue, data, timeout, redirectsRemaining = 5) {
|
|
210
|
+
const url = new URL(urlValue);
|
|
211
|
+
const body = JSON.stringify(data);
|
|
212
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
213
|
+
return await new Promise((resolve, reject) => {
|
|
214
|
+
const request2 = transport.request(
|
|
215
|
+
url,
|
|
216
|
+
{
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: {
|
|
219
|
+
"User-Agent": "Orionjs-Echoes/1.1",
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
"Content-Length": Buffer.byteLength(body)
|
|
79
222
|
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
223
|
+
},
|
|
224
|
+
(response) => {
|
|
225
|
+
const chunks = [];
|
|
226
|
+
const statusCode = response.statusCode || 0;
|
|
227
|
+
if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
|
|
228
|
+
response.resume();
|
|
229
|
+
if (redirectsRemaining <= 0) {
|
|
230
|
+
reject(new Error("Echoes request exceeded the redirect limit"));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
resolve(
|
|
234
|
+
postJSON(
|
|
235
|
+
new URL(response.headers.location, url).toString(),
|
|
236
|
+
data,
|
|
237
|
+
timeout,
|
|
238
|
+
redirectsRemaining - 1
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
244
|
+
response.on("end", () => {
|
|
245
|
+
try {
|
|
246
|
+
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
247
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
248
|
+
reject(new Error(`Request failed with status code ${statusCode}`));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
resolve({
|
|
252
|
+
statusCode,
|
|
253
|
+
data: responseBody ? JSON.parse(responseBody) : {}
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
reject(error);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
84
259
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
260
|
+
);
|
|
261
|
+
request2.on("error", reject);
|
|
262
|
+
if (timeout) {
|
|
263
|
+
request2.setTimeout(timeout, () => {
|
|
264
|
+
request2.destroy(new Error(`Echoes request timed out after ${timeout}ms`));
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
request2.end(body);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
var makeRequest = async (options) => {
|
|
271
|
+
return await executeWithRetries(
|
|
272
|
+
() => postJSON(options.url, options.data, options.timeout),
|
|
273
|
+
options.retries || 0
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/request/index.ts
|
|
278
|
+
async function request(options) {
|
|
279
|
+
var _a, _b;
|
|
280
|
+
const { method, service, params } = options;
|
|
281
|
+
const serializedParams = serialize_default(params);
|
|
282
|
+
const date = /* @__PURE__ */ new Date();
|
|
283
|
+
const body = { method, service, serializedParams, date };
|
|
284
|
+
const signature = getSignature_default(body);
|
|
285
|
+
try {
|
|
286
|
+
const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
|
|
287
|
+
const requestOptions = {
|
|
288
|
+
url: getURL_default(service),
|
|
289
|
+
retries: options.retries,
|
|
290
|
+
timeout: options.timeout,
|
|
291
|
+
data: {
|
|
292
|
+
body,
|
|
293
|
+
signature
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const result = await requestMaker(requestOptions);
|
|
297
|
+
if (result.statusCode !== 200) {
|
|
298
|
+
throw new Error(`Wrong status code ${result.statusCode}`);
|
|
299
|
+
}
|
|
300
|
+
const data = result.data;
|
|
301
|
+
if (data.error) {
|
|
302
|
+
const info = data.errorInfo;
|
|
303
|
+
if (info) {
|
|
304
|
+
if (data.isValidationError) {
|
|
305
|
+
throw createEchoesValidationError(info);
|
|
91
306
|
}
|
|
92
|
-
|
|
307
|
+
if (data.isUserError) {
|
|
308
|
+
throw createEchoesUserError(info);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
throw new Error(`${data.error}`);
|
|
93
312
|
}
|
|
313
|
+
const response = deserialize_default(data.result);
|
|
314
|
+
return response;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const caught = error;
|
|
317
|
+
if (caught.isOrionError || caught.isEchoesError) throw caught;
|
|
318
|
+
throw new Error(`Echoes request network error calling ${service}/${method}: ${caught.message}`);
|
|
94
319
|
}
|
|
95
|
-
}
|
|
320
|
+
}
|
|
96
321
|
|
|
97
322
|
// src/startService/KafkaManager.ts
|
|
98
|
-
import {
|
|
99
|
-
import { logger as logger2 } from "@orion-js/logger";
|
|
323
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
100
324
|
var HEARTBEAT_INTERVAL_SECONDS = 5;
|
|
101
325
|
var CHECK_JOIN_CONSUMER_INTERVAL_SECONDS = 30;
|
|
102
326
|
var DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY = 4;
|
|
103
327
|
var DEFAULT_MEMBERS_TO_PARTITIONS_RATIO = 1;
|
|
104
328
|
var KafkaManager = class {
|
|
329
|
+
name = "kafka";
|
|
105
330
|
kafka;
|
|
106
331
|
options;
|
|
107
332
|
producer;
|
|
108
333
|
consumer;
|
|
109
|
-
topics;
|
|
110
|
-
|
|
334
|
+
topics = [];
|
|
335
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
336
|
+
onEvent;
|
|
337
|
+
consumerStarted = false;
|
|
338
|
+
producerConnected = false;
|
|
111
339
|
interval;
|
|
112
340
|
constructor(options) {
|
|
113
|
-
this.kafka = new Kafka(options.client);
|
|
114
341
|
this.options = options;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
342
|
+
}
|
|
343
|
+
async start(options) {
|
|
344
|
+
var _a;
|
|
345
|
+
let kafkaModule;
|
|
346
|
+
try {
|
|
347
|
+
kafkaModule = await import("kafkajs");
|
|
348
|
+
} catch (error) {
|
|
349
|
+
const wrapped = new Error(
|
|
350
|
+
"Echoes Kafka transport requires kafkajs to be installed in the application"
|
|
351
|
+
);
|
|
352
|
+
wrapped.cause = error;
|
|
353
|
+
throw wrapped;
|
|
354
|
+
}
|
|
355
|
+
this.kafka = new kafkaModule.Kafka(this.options.client);
|
|
356
|
+
this.onEvent = options.onEvent;
|
|
357
|
+
this.subscriptions = new Map(
|
|
358
|
+
options.subscriptions.map((subscription) => [subscription.topic, subscription])
|
|
359
|
+
);
|
|
360
|
+
this.topics = options.subscriptions.map((subscription) => subscription.topic);
|
|
361
|
+
if (options.publish || options.consume) {
|
|
362
|
+
this.producer = this.kafka.producer(this.options.producer);
|
|
363
|
+
await this.producer.connect();
|
|
364
|
+
this.producerConnected = true;
|
|
365
|
+
}
|
|
366
|
+
if (!options.consume || this.topics.length === 0) return;
|
|
367
|
+
if (!((_a = this.options.consumer) == null ? void 0 : _a.groupId)) {
|
|
368
|
+
throw new Error("Echoes Kafka consumers require consumer.groupId");
|
|
369
|
+
}
|
|
370
|
+
this.consumer = this.kafka.consumer(this.options.consumer);
|
|
371
|
+
this.consumerStarted = await this.conditionalStart();
|
|
372
|
+
if (this.consumerStarted) return;
|
|
373
|
+
getEchoesLogger().info("Echoes: Delaying consumer group join, waiting for conditions to be met");
|
|
374
|
+
this.interval = setInterval(async () => {
|
|
375
|
+
this.consumerStarted = await this.conditionalStart();
|
|
376
|
+
if (this.consumerStarted) clearInterval(this.interval);
|
|
377
|
+
}, CHECK_JOIN_CONSUMER_INTERVAL_SECONDS * 1e3);
|
|
378
|
+
}
|
|
379
|
+
async publish(options) {
|
|
380
|
+
if (!this.producer || !this.producerConnected) {
|
|
381
|
+
throw new Error("Echoes Kafka producer is not connected");
|
|
382
|
+
}
|
|
383
|
+
return await this.producer.send({
|
|
384
|
+
acks: options.acks,
|
|
385
|
+
timeout: options.timeout,
|
|
386
|
+
topic: options.topic,
|
|
387
|
+
messages: [
|
|
388
|
+
{
|
|
389
|
+
value: serialize_default({ params: options.params }),
|
|
390
|
+
headers: {
|
|
391
|
+
"echoes-event-id": randomUUID2()
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async close() {
|
|
398
|
+
var _a, _b;
|
|
399
|
+
const logger = getEchoesLogger();
|
|
400
|
+
logger.warn("Echoes: Stopping Kafka transport");
|
|
401
|
+
if (this.interval) clearInterval(this.interval);
|
|
402
|
+
await Promise.all([
|
|
403
|
+
(_a = this.consumer) == null ? void 0 : _a.disconnect(),
|
|
404
|
+
this.producerConnected ? (_b = this.producer) == null ? void 0 : _b.disconnect() : void 0
|
|
405
|
+
]);
|
|
406
|
+
this.consumerStarted = false;
|
|
407
|
+
this.producerConnected = false;
|
|
118
408
|
}
|
|
119
409
|
async checkJoinConsumerGroupConditions() {
|
|
410
|
+
const logger = getEchoesLogger();
|
|
120
411
|
const admin = this.kafka.admin();
|
|
121
412
|
try {
|
|
122
413
|
await admin.connect();
|
|
123
|
-
const
|
|
414
|
+
const groupId = this.options.consumer.groupId;
|
|
415
|
+
const groupDescriptions = await admin.describeGroups([groupId]);
|
|
124
416
|
const group = groupDescriptions.groups[0];
|
|
125
417
|
if (group.state === "Empty") {
|
|
126
|
-
|
|
418
|
+
logger.info(`Echoes: Consumer group ${groupId} is empty, joining`);
|
|
127
419
|
return true;
|
|
128
420
|
}
|
|
129
421
|
const topicsMetadata = await admin.fetchTopicMetadata({ topics: this.topics });
|
|
@@ -131,23 +423,24 @@ var KafkaManager = class {
|
|
|
131
423
|
(acc, topic) => acc + topic.partitions.length,
|
|
132
424
|
0
|
|
133
425
|
);
|
|
134
|
-
|
|
135
|
-
`Echoes: Consumer group ${
|
|
426
|
+
logger.info(
|
|
427
|
+
`Echoes: Consumer group ${groupId} has ${group.members.length} members and ${totalPartitions} partitions`
|
|
136
428
|
);
|
|
137
429
|
const partitionsRatio = this.options.membersToPartitionsRatio || DEFAULT_MEMBERS_TO_PARTITIONS_RATIO;
|
|
138
430
|
const partitionsThreshold = Math.ceil(totalPartitions * partitionsRatio);
|
|
139
431
|
if (partitionsThreshold > group.members.length) {
|
|
140
|
-
|
|
141
|
-
`Echoes: Consumer group ${
|
|
432
|
+
logger.info(
|
|
433
|
+
`Echoes: Consumer group ${groupId} has room for more members ${group.members.length}/${partitionsThreshold}, joining`
|
|
142
434
|
);
|
|
143
435
|
return true;
|
|
144
436
|
}
|
|
437
|
+
return false;
|
|
145
438
|
} catch (error) {
|
|
146
|
-
|
|
439
|
+
logger.error("Echoes: Error checking consumer group conditions, join", { error });
|
|
147
440
|
return true;
|
|
148
441
|
} finally {
|
|
149
442
|
await admin.disconnect().catch((error) => {
|
|
150
|
-
|
|
443
|
+
logger.error("Echoes: Error disconnecting admin client", { error });
|
|
151
444
|
});
|
|
152
445
|
}
|
|
153
446
|
}
|
|
@@ -164,67 +457,83 @@ var KafkaManager = class {
|
|
|
164
457
|
await this.joinConsumerGroup();
|
|
165
458
|
return true;
|
|
166
459
|
}
|
|
167
|
-
|
|
168
|
-
async start() {
|
|
169
|
-
if (this.started) return;
|
|
170
|
-
await this.producer.connect();
|
|
171
|
-
this.started = await this.conditionalStart();
|
|
172
|
-
if (this.started) return;
|
|
173
|
-
logger2.info("Echoes: Delaying consumer group join, waiting for conditions to be met");
|
|
174
|
-
this.interval = setInterval(async () => {
|
|
175
|
-
this.started = await this.conditionalStart();
|
|
176
|
-
if (this.started) clearInterval(this.interval);
|
|
177
|
-
}, CHECK_JOIN_CONSUMER_INTERVAL_SECONDS * 1e3);
|
|
178
|
-
}
|
|
179
|
-
async stop() {
|
|
180
|
-
logger2.warn("Echoes: Stopping echoes");
|
|
181
|
-
if (this.interval) clearInterval(this.interval);
|
|
182
|
-
if (this.consumer) await this.consumer.disconnect();
|
|
183
|
-
if (this.producer) await this.producer.disconnect();
|
|
460
|
+
return false;
|
|
184
461
|
}
|
|
185
462
|
async handleMessage(params) {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
463
|
+
const logger = getEchoesLogger();
|
|
464
|
+
const subscription = this.subscriptions.get(params.topic);
|
|
465
|
+
if (!subscription) {
|
|
466
|
+
logger.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
|
|
189
467
|
return;
|
|
190
468
|
}
|
|
191
469
|
let intervalsCount = 0;
|
|
192
470
|
const heartbeatInterval = setInterval(async () => {
|
|
193
471
|
await params.heartbeat().catch((error) => {
|
|
194
|
-
|
|
472
|
+
logger.warn(`Echoes: Error sending heartbeat: ${error.message}`);
|
|
195
473
|
});
|
|
196
474
|
intervalsCount++;
|
|
197
475
|
if (intervalsCount * HEARTBEAT_INTERVAL_SECONDS % 30 === 0) {
|
|
198
|
-
|
|
476
|
+
logger.warn(
|
|
199
477
|
`Echoes: Event is taking too long to process: ${params.topic} ${intervalsCount * HEARTBEAT_INTERVAL_SECONDS}s`
|
|
200
478
|
);
|
|
201
479
|
}
|
|
202
480
|
}, HEARTBEAT_INTERVAL_SECONDS * 1e3);
|
|
203
481
|
try {
|
|
204
|
-
|
|
482
|
+
const event = this.createReceivedEvent(params);
|
|
483
|
+
await this.onEvent(event);
|
|
205
484
|
} catch (error) {
|
|
206
|
-
|
|
207
|
-
|
|
485
|
+
try {
|
|
486
|
+
await this.handleRetries(subscription, params, error);
|
|
487
|
+
} catch (retryError) {
|
|
488
|
+
logger.error("Echoes: error processing a message", {
|
|
489
|
+
error: retryError,
|
|
490
|
+
topic: params.topic
|
|
491
|
+
});
|
|
492
|
+
throw retryError;
|
|
493
|
+
}
|
|
208
494
|
} finally {
|
|
209
495
|
clearInterval(heartbeatInterval);
|
|
210
496
|
}
|
|
211
497
|
}
|
|
212
|
-
|
|
498
|
+
createReceivedEvent(params) {
|
|
499
|
+
var _a, _b, _c, _d;
|
|
500
|
+
const { message, topic, partition } = params;
|
|
501
|
+
if (!message.value) {
|
|
502
|
+
throw new Error(`Echoes received an empty Kafka message for ${topic}`);
|
|
503
|
+
}
|
|
504
|
+
const data = deserialize_default(message.value.toString());
|
|
505
|
+
const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
506
|
+
const timestamp = Number.parseInt(message.timestamp || "", 10);
|
|
507
|
+
return {
|
|
508
|
+
id: ((_d = (_c = message.headers) == null ? void 0 : _c["echoes-event-id"]) == null ? void 0 : _d.toString()) || `kafka:${topic}:${partition}:${message.offset}`,
|
|
509
|
+
topic,
|
|
510
|
+
data,
|
|
511
|
+
transport: "kafka",
|
|
512
|
+
headers: message.headers,
|
|
513
|
+
createdAt: Number.isFinite(timestamp) ? new Date(timestamp) : /* @__PURE__ */ new Date(),
|
|
514
|
+
attempt: retries + 1,
|
|
515
|
+
context: params
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
async handleRetries(subscription, params, error) {
|
|
213
519
|
var _a, _b;
|
|
520
|
+
const logger = getEchoesLogger();
|
|
214
521
|
const { message, topic } = params;
|
|
215
522
|
const retries = Number.parseInt(((_b = (_a = message == null ? void 0 : message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
216
|
-
if (
|
|
523
|
+
if (subscription.attemptsBeforeDeadLetter === void 0 || subscription.attemptsBeforeDeadLetter === null) {
|
|
217
524
|
throw error;
|
|
218
525
|
}
|
|
219
|
-
const maxRetries =
|
|
526
|
+
const maxRetries = subscription.attemptsBeforeDeadLetter || 0;
|
|
220
527
|
const exceededMaxRetries = retries >= maxRetries;
|
|
221
528
|
const nextTopic = exceededMaxRetries ? `DLQ-${topic}` : topic;
|
|
529
|
+
if (!message.value) throw error;
|
|
222
530
|
await this.producer.send({
|
|
223
531
|
topic: nextTopic,
|
|
224
532
|
messages: [
|
|
225
533
|
{
|
|
226
534
|
value: message.value.toString(),
|
|
227
535
|
headers: {
|
|
536
|
+
...message.headers,
|
|
228
537
|
retries: String(retries + 1),
|
|
229
538
|
error: error.message
|
|
230
539
|
}
|
|
@@ -232,180 +541,339 @@ var KafkaManager = class {
|
|
|
232
541
|
]
|
|
233
542
|
});
|
|
234
543
|
if (exceededMaxRetries) {
|
|
235
|
-
|
|
544
|
+
logger.error(
|
|
236
545
|
"Echoes: a message has reached the maximum number of retries, sending it to DLQ",
|
|
237
546
|
{ topic: nextTopic }
|
|
238
547
|
);
|
|
239
548
|
} else {
|
|
240
|
-
|
|
549
|
+
logger.warn("Echoes: a retryable message failed", { error, topic: nextTopic });
|
|
241
550
|
}
|
|
242
551
|
}
|
|
243
552
|
};
|
|
244
553
|
var KafkaManager_default = KafkaManager;
|
|
245
554
|
|
|
246
|
-
// src/
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
555
|
+
// src/events/EventBus.ts
|
|
556
|
+
var EventBus = class {
|
|
557
|
+
echoes;
|
|
558
|
+
transports;
|
|
559
|
+
consumeFrom;
|
|
560
|
+
publishTo;
|
|
561
|
+
startedTransports = [];
|
|
562
|
+
started = false;
|
|
563
|
+
closed = false;
|
|
564
|
+
constructor(options) {
|
|
565
|
+
this.echoes = options.echoes;
|
|
566
|
+
this.transports = options.transports;
|
|
567
|
+
this.consumeFrom = [...new Set(options.consumeFrom)];
|
|
568
|
+
this.publishTo = options.publishTo;
|
|
254
569
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
570
|
+
async start() {
|
|
571
|
+
if (this.started) return;
|
|
572
|
+
const selected = new Set(this.consumeFrom);
|
|
573
|
+
if (this.publishTo) selected.add(this.publishTo);
|
|
574
|
+
for (const name of selected) {
|
|
575
|
+
if (!this.transports[name]) {
|
|
576
|
+
throw new Error(`Echoes event transport "${name}" is selected but not configured`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const subscriptions = Object.entries(this.echoes).filter(([, echo2]) => echo2.type === "event").map(([topic, echo2]) => ({
|
|
580
|
+
topic,
|
|
581
|
+
attemptsBeforeDeadLetter: echo2.attemptsBeforeDeadLetter
|
|
582
|
+
}));
|
|
583
|
+
try {
|
|
584
|
+
for (const name of selected) {
|
|
585
|
+
const transport = this.transports[name];
|
|
586
|
+
this.startedTransports.push(transport);
|
|
587
|
+
await transport.start({
|
|
588
|
+
consume: this.consumeFrom.includes(name),
|
|
589
|
+
publish: this.publishTo === name,
|
|
590
|
+
subscriptions,
|
|
591
|
+
onEvent: (event) => this.handleEvent(event)
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
this.started = true;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
await this.close().catch(() => void 0);
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
260
599
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
600
|
+
async publish(options) {
|
|
601
|
+
if (!this.started) {
|
|
602
|
+
throw new Error("You must initialize echoes configuration to use publish");
|
|
603
|
+
}
|
|
604
|
+
if (!this.publishTo) {
|
|
605
|
+
throw new Error("Echoes does not have a publish transport configured");
|
|
606
|
+
}
|
|
607
|
+
return await this.transports[this.publishTo].publish(options);
|
|
267
608
|
}
|
|
268
|
-
|
|
609
|
+
async close() {
|
|
610
|
+
if (this.closed) return;
|
|
611
|
+
this.closed = true;
|
|
612
|
+
await Promise.all(this.startedTransports.map((transport) => transport.close()));
|
|
613
|
+
}
|
|
614
|
+
async handleEvent(event) {
|
|
615
|
+
const logger = getEchoesLogger();
|
|
616
|
+
const echo2 = this.echoes[event.topic];
|
|
617
|
+
if (!echo2 || echo2.type !== "event") {
|
|
618
|
+
logger.warn(`Echoes: Received a message for an unknown topic: ${event.topic}, ignoring it`);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (event.transport === "kafka" && event.context) {
|
|
622
|
+
await echo2.onMessage(event.context);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (echo2.onEvent) {
|
|
626
|
+
await echo2.onEvent(event);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
await echo2.resolve(event.data.params, {
|
|
630
|
+
...event.context || {},
|
|
631
|
+
transport: event.transport,
|
|
632
|
+
eventId: event.id,
|
|
633
|
+
topic: event.topic,
|
|
634
|
+
headers: event.headers,
|
|
635
|
+
createdAt: event.createdAt,
|
|
636
|
+
attempt: event.attempt,
|
|
637
|
+
data: event.data
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
};
|
|
269
641
|
|
|
270
|
-
// src/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
642
|
+
// src/events/PulseManager.ts
|
|
643
|
+
var PulseManager = class {
|
|
644
|
+
name = "pulse";
|
|
645
|
+
options;
|
|
646
|
+
pulse;
|
|
647
|
+
subscriptions = [];
|
|
648
|
+
constructor(options) {
|
|
649
|
+
this.options = options;
|
|
650
|
+
}
|
|
651
|
+
async start(options) {
|
|
652
|
+
let pulseModule;
|
|
653
|
+
try {
|
|
654
|
+
pulseModule = await import("@orion-js/pulse");
|
|
655
|
+
} catch (error) {
|
|
656
|
+
const wrapped = new Error(
|
|
657
|
+
"Echoes Pulse transport requires @orion-js/pulse to be installed in the application"
|
|
658
|
+
);
|
|
659
|
+
wrapped.cause = error;
|
|
660
|
+
throw wrapped;
|
|
661
|
+
}
|
|
662
|
+
const { subscription, ...connectOptions } = this.options;
|
|
663
|
+
this.pulse = pulseModule.connect(connectOptions);
|
|
664
|
+
await this.pulse.awaitConnection();
|
|
665
|
+
if (!options.consume) return;
|
|
666
|
+
this.subscriptions = await Promise.all(
|
|
667
|
+
options.subscriptions.map(
|
|
668
|
+
(definition) => this.pulse.subscribe(
|
|
669
|
+
definition.topic,
|
|
670
|
+
(event) => options.onEvent(this.createReceivedEvent(event)),
|
|
671
|
+
this.getSubscribeOptions(subscription, definition.attemptsBeforeDeadLetter)
|
|
672
|
+
)
|
|
673
|
+
)
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
async publish(options) {
|
|
677
|
+
if (!this.pulse) {
|
|
678
|
+
throw new Error("Echoes Pulse client is not connected");
|
|
679
|
+
}
|
|
680
|
+
return await this.pulse.publish({
|
|
681
|
+
topic: options.topic,
|
|
682
|
+
data: { params: options.params }
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
async close() {
|
|
686
|
+
var _a;
|
|
687
|
+
await ((_a = this.pulse) == null ? void 0 : _a.close());
|
|
688
|
+
this.subscriptions = [];
|
|
689
|
+
}
|
|
690
|
+
getSubscribeOptions(defaults = {}, attemptsBeforeDeadLetter) {
|
|
691
|
+
return {
|
|
692
|
+
...defaults,
|
|
693
|
+
...attemptsBeforeDeadLetter === void 0 ? {} : { maxRetries: attemptsBeforeDeadLetter }
|
|
694
|
+
};
|
|
274
695
|
}
|
|
275
|
-
|
|
276
|
-
|
|
696
|
+
createReceivedEvent(event) {
|
|
697
|
+
return {
|
|
698
|
+
id: event.id,
|
|
699
|
+
topic: event.topic,
|
|
700
|
+
data: event.data,
|
|
701
|
+
transport: "pulse",
|
|
702
|
+
headers: event.headers,
|
|
703
|
+
createdAt: event.createdAt,
|
|
704
|
+
attempt: event.attempt,
|
|
705
|
+
context: event
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// src/events/createEventBus.ts
|
|
711
|
+
function resolveEventsConfig(options) {
|
|
712
|
+
var _a, _b, _c, _d;
|
|
713
|
+
const legacyKafka = options.client ? {
|
|
714
|
+
client: options.client,
|
|
715
|
+
producer: options.producer,
|
|
716
|
+
consumer: options.consumer,
|
|
717
|
+
readTopicsFromBeginning: options.readTopicsFromBeginning,
|
|
718
|
+
partitionsConsumedConcurrently: options.partitionsConsumedConcurrently,
|
|
719
|
+
membersToPartitionsRatio: options.membersToPartitionsRatio
|
|
720
|
+
} : void 0;
|
|
721
|
+
const kafka = ((_a = options.events) == null ? void 0 : _a.kafka) || legacyKafka;
|
|
722
|
+
const pulse = (_b = options.events) == null ? void 0 : _b.pulse;
|
|
723
|
+
if (!kafka && !pulse && !options.events) return void 0;
|
|
724
|
+
const defaultTransport = kafka ? "kafka" : pulse ? "pulse" : void 0;
|
|
725
|
+
return {
|
|
726
|
+
kafka,
|
|
727
|
+
pulse,
|
|
728
|
+
consumeFrom: ((_c = options.events) == null ? void 0 : _c.consumeFrom) || (defaultTransport ? [defaultTransport] : []),
|
|
729
|
+
publishTo: ((_d = options.events) == null ? void 0 : _d.publishTo) || defaultTransport
|
|
277
730
|
};
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
731
|
+
}
|
|
732
|
+
function createEventBus(options) {
|
|
733
|
+
const resolved = resolveEventsConfig(options);
|
|
734
|
+
if (!resolved) return void 0;
|
|
735
|
+
const transports = {};
|
|
736
|
+
if (resolved.kafka) transports.kafka = new KafkaManager_default(resolved.kafka);
|
|
737
|
+
if (resolved.pulse) transports.pulse = new PulseManager(resolved.pulse);
|
|
738
|
+
return new EventBus({
|
|
739
|
+
echoes: options.echoes,
|
|
740
|
+
transports,
|
|
741
|
+
consumeFrom: resolved.consumeFrom,
|
|
742
|
+
publishTo: resolved.publishTo
|
|
287
743
|
});
|
|
288
744
|
}
|
|
289
745
|
|
|
290
|
-
// src/
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
if (!url) {
|
|
296
|
-
throw new Error(`No URL found in echoes config for service ${serviceName}`);
|
|
746
|
+
// src/requestsHandler/checkSignature.ts
|
|
747
|
+
function checkSignature_default(body, signature) {
|
|
748
|
+
const generatedSignature = getSignature_default(body);
|
|
749
|
+
if (generatedSignature !== signature) {
|
|
750
|
+
throw new Error("Echoes invalid signature");
|
|
297
751
|
}
|
|
298
|
-
return url;
|
|
299
752
|
}
|
|
300
753
|
|
|
301
|
-
// src/
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
throw new Error("Error deserializing echo message");
|
|
754
|
+
// src/requestsHandler/getEcho.ts
|
|
755
|
+
function getEcho_default(method) {
|
|
756
|
+
const echo2 = config_default.echoes[method];
|
|
757
|
+
if (!echo2) {
|
|
758
|
+
throw new Error(`Echo named ${method} not found in this service`);
|
|
307
759
|
}
|
|
760
|
+
if (echo2.type !== "request") {
|
|
761
|
+
throw new Error(`Echo named ${method} is not of type request`);
|
|
762
|
+
}
|
|
763
|
+
return echo2;
|
|
308
764
|
}
|
|
309
765
|
|
|
310
|
-
// src/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
200
|
|
328
|
-
);
|
|
329
|
-
return {
|
|
330
|
-
data: result.data,
|
|
331
|
-
statusCode: result.status
|
|
332
|
-
};
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
// src/request/index.ts
|
|
336
|
-
import { ValidationError } from "@orion-js/schema";
|
|
337
|
-
import { UserError } from "@orion-js/helpers";
|
|
338
|
-
async function request(options) {
|
|
339
|
-
var _a, _b;
|
|
340
|
-
const { method, service, params } = options;
|
|
341
|
-
const serializedParams = serialize_default(params);
|
|
342
|
-
const date = /* @__PURE__ */ new Date();
|
|
343
|
-
const body = { method, service, serializedParams, date };
|
|
344
|
-
const signature = getSignature_default(body);
|
|
345
|
-
try {
|
|
346
|
-
const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
|
|
347
|
-
const requestOptions = {
|
|
348
|
-
url: getURL_default(service),
|
|
349
|
-
retries: options.retries,
|
|
350
|
-
timeout: options.timeout,
|
|
351
|
-
data: {
|
|
352
|
-
body,
|
|
353
|
-
signature
|
|
766
|
+
// src/requestsHandler/index.ts
|
|
767
|
+
var requestsHandler_default = (options) => ({
|
|
768
|
+
method: "post",
|
|
769
|
+
path: options.requests.handlerPath || "/echoes-services",
|
|
770
|
+
bodyLimit: "10mb",
|
|
771
|
+
async handle(requestBody) {
|
|
772
|
+
try {
|
|
773
|
+
const { body, signature } = requestBody;
|
|
774
|
+
checkSignature_default(body, signature);
|
|
775
|
+
const { method, serializedParams } = body;
|
|
776
|
+
const echo2 = getEcho_default(method);
|
|
777
|
+
const result = await echo2.onRequest(serializedParams);
|
|
778
|
+
return { result: serialize_default(result) };
|
|
779
|
+
} catch (error) {
|
|
780
|
+
const caught = error;
|
|
781
|
+
if (!caught.getInfo) {
|
|
782
|
+
getEchoesLogger().error("Error at echo requests handler:", { error: caught });
|
|
354
783
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
784
|
+
return {
|
|
785
|
+
error: caught.message,
|
|
786
|
+
errorInfo: caught.getInfo ? caught.getInfo() : null,
|
|
787
|
+
isValidationError: !!caught.isValidationError,
|
|
788
|
+
isUserError: !!caught.isUserError
|
|
789
|
+
};
|
|
359
790
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
throw new Error(
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// src/startService/index.ts
|
|
795
|
+
var eventBus = null;
|
|
796
|
+
async function startService(options) {
|
|
797
|
+
config_default.echoes = options.echoes;
|
|
798
|
+
if (options.requests) {
|
|
799
|
+
config_default.requests = options.requests;
|
|
800
|
+
const registerHandler = options.requests.registerHandler || getEchoesRuntime().registerRequestHandler;
|
|
801
|
+
if (!registerHandler) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
"Echoes requests require requests.registerHandler in standalone servers. Orion applications can import @orion-js/echoes-orion once during startup."
|
|
804
|
+
);
|
|
372
805
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
806
|
+
await registerHandler(requestsHandler_default(options));
|
|
807
|
+
}
|
|
808
|
+
const nextEventBus = createEventBus(options);
|
|
809
|
+
if (nextEventBus) {
|
|
810
|
+
await nextEventBus.start();
|
|
811
|
+
eventBus = nextEventBus;
|
|
812
|
+
config_default.eventBus = eventBus;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async function stopService() {
|
|
816
|
+
if (eventBus) {
|
|
817
|
+
const logger = getEchoesLogger();
|
|
818
|
+
logger.info("Stopping Echoes...");
|
|
819
|
+
await eventBus.close();
|
|
820
|
+
eventBus = null;
|
|
821
|
+
config_default.eventBus = void 0;
|
|
822
|
+
logger.info("Echoes stopped");
|
|
378
823
|
}
|
|
379
824
|
}
|
|
380
|
-
|
|
381
|
-
// src/service/index.ts
|
|
382
|
-
import { runWithOrionAsyncContext } from "@orion-js/logger";
|
|
383
825
|
|
|
384
826
|
// src/echo/index.ts
|
|
385
|
-
import { clean, cleanAndValidate } from "@orion-js/schema";
|
|
386
827
|
var echo = function createNewEcho(options) {
|
|
387
828
|
const resolve = async (params, context) => {
|
|
388
|
-
const cleaned = options.params ? await
|
|
829
|
+
const cleaned = options.params ? await parseEchoesSchema(options.params, params) : params ?? {};
|
|
389
830
|
const result = await options.resolve(cleaned, context);
|
|
390
831
|
if (options.returns) {
|
|
391
|
-
return await
|
|
832
|
+
return await cleanEchoesSchema(options.returns, result);
|
|
392
833
|
}
|
|
393
834
|
return result;
|
|
394
835
|
};
|
|
836
|
+
const onEvent = async (event) => {
|
|
837
|
+
const context = {
|
|
838
|
+
...event.context || {},
|
|
839
|
+
transport: event.transport,
|
|
840
|
+
eventId: event.id,
|
|
841
|
+
topic: event.topic,
|
|
842
|
+
headers: event.headers,
|
|
843
|
+
createdAt: event.createdAt,
|
|
844
|
+
attempt: event.attempt,
|
|
845
|
+
data: event.data
|
|
846
|
+
};
|
|
847
|
+
await resolve(event.data.params, context);
|
|
848
|
+
};
|
|
395
849
|
return {
|
|
396
850
|
type: options.type,
|
|
397
851
|
params: options.params,
|
|
398
852
|
returns: options.returns,
|
|
399
853
|
attemptsBeforeDeadLetter: options.type === "event" ? options.attemptsBeforeDeadLetter : void 0,
|
|
400
854
|
resolve,
|
|
855
|
+
onEvent,
|
|
401
856
|
onMessage: async (messageData) => {
|
|
857
|
+
var _a, _b, _c, _d;
|
|
402
858
|
const { message } = messageData;
|
|
859
|
+
if (!message.value) {
|
|
860
|
+
throw new Error(`Echoes received an empty Kafka message for ${messageData.topic}`);
|
|
861
|
+
}
|
|
403
862
|
const data = deserialize_default(message.value.toString());
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
await
|
|
863
|
+
const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
864
|
+
const timestamp = Number(message.timestamp);
|
|
865
|
+
const createdAt = Number.isFinite(timestamp) ? new Date(timestamp) : /* @__PURE__ */ new Date();
|
|
866
|
+
const eventId = ((_d = (_c = message.headers) == null ? void 0 : _c["echoes-event-id"]) == null ? void 0 : _d.toString()) || `kafka:${messageData.topic}:${messageData.partition}:${message.offset}`;
|
|
867
|
+
await onEvent({
|
|
868
|
+
id: eventId,
|
|
869
|
+
topic: messageData.topic,
|
|
870
|
+
data,
|
|
871
|
+
transport: "kafka",
|
|
872
|
+
headers: message.headers,
|
|
873
|
+
createdAt,
|
|
874
|
+
attempt: retries + 1,
|
|
875
|
+
context: messageData
|
|
876
|
+
});
|
|
409
877
|
},
|
|
410
878
|
onRequest: async (serializedParams) => {
|
|
411
879
|
const context = {};
|
|
@@ -421,15 +889,21 @@ function createEchoEvent(options) {
|
|
|
421
889
|
return echo({ ...options, type: "event" });
|
|
422
890
|
}
|
|
423
891
|
|
|
892
|
+
// src/schema.ts
|
|
893
|
+
function typedEchoesSchema(schema) {
|
|
894
|
+
return schema;
|
|
895
|
+
}
|
|
896
|
+
|
|
424
897
|
// src/service/index.ts
|
|
425
|
-
import { getInstance, Service } from "@orion-js/services";
|
|
426
898
|
var serviceMetadata = /* @__PURE__ */ new WeakMap();
|
|
427
899
|
var echoesMetadata = /* @__PURE__ */ new WeakMap();
|
|
428
900
|
var echoEntriesByClass = /* @__PURE__ */ new Map();
|
|
901
|
+
var standaloneInstances = /* @__PURE__ */ new WeakMap();
|
|
429
902
|
var pendingEchoEntries = {};
|
|
430
903
|
function Echoes() {
|
|
431
904
|
return (target, context) => {
|
|
432
|
-
|
|
905
|
+
var _a, _b;
|
|
906
|
+
(_b = (_a = getEchoesRuntime()).decorateService) == null ? void 0 : _b.call(_a, target, context);
|
|
433
907
|
serviceMetadata.set(target, { _serviceType: "echoes" });
|
|
434
908
|
if (Object.keys(pendingEchoEntries).length > 0) {
|
|
435
909
|
echoEntriesByClass.set(target, pendingEchoEntries);
|
|
@@ -446,7 +920,7 @@ function EchoEvent(options = {}) {
|
|
|
446
920
|
return createEchoEvent({
|
|
447
921
|
...options,
|
|
448
922
|
resolve: async (params, contextData) => {
|
|
449
|
-
return await
|
|
923
|
+
return await runWithEchoesContext(
|
|
450
924
|
{
|
|
451
925
|
controllerType: "echo",
|
|
452
926
|
echoName: propertyKey,
|
|
@@ -475,7 +949,7 @@ function EchoRequest(options = {}) {
|
|
|
475
949
|
return createEchoRequest({
|
|
476
950
|
...options,
|
|
477
951
|
resolve: async (params, contextData) => {
|
|
478
|
-
return await
|
|
952
|
+
return await runWithEchoesContext(
|
|
479
953
|
{
|
|
480
954
|
controllerType: "echo",
|
|
481
955
|
echoName: propertyKey,
|
|
@@ -505,7 +979,18 @@ function initializeEchoesIfNeeded(instance) {
|
|
|
505
979
|
echoesMetadata.set(instance, echoes);
|
|
506
980
|
}
|
|
507
981
|
function getServiceEchoes(target) {
|
|
508
|
-
|
|
982
|
+
let instance;
|
|
983
|
+
if (typeof target !== "function") {
|
|
984
|
+
instance = target;
|
|
985
|
+
} else if (getEchoesRuntime().getInstance) {
|
|
986
|
+
instance = getEchoesRuntime().getInstance(target);
|
|
987
|
+
} else {
|
|
988
|
+
instance = standaloneInstances.get(target);
|
|
989
|
+
if (!instance) {
|
|
990
|
+
instance = new target();
|
|
991
|
+
standaloneInstances.set(target, instance);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
509
994
|
if (!serviceMetadata.has(instance.constructor)) {
|
|
510
995
|
throw new Error("You must pass a class decorated with @Echoes to getServiceEchoes");
|
|
511
996
|
}
|
|
@@ -521,13 +1006,25 @@ export {
|
|
|
521
1006
|
EchoEvent,
|
|
522
1007
|
EchoRequest,
|
|
523
1008
|
Echoes,
|
|
1009
|
+
EchoesUserError,
|
|
1010
|
+
EchoesValidationError,
|
|
1011
|
+
cleanEchoesSchema,
|
|
1012
|
+
configureEchoesRuntime,
|
|
524
1013
|
createEchoEvent,
|
|
525
1014
|
createEchoRequest,
|
|
1015
|
+
createEchoesUserError,
|
|
1016
|
+
createEchoesValidationError,
|
|
526
1017
|
echo,
|
|
1018
|
+
getEchoesContext,
|
|
1019
|
+
getEchoesLogger,
|
|
1020
|
+
getEchoesRuntime,
|
|
527
1021
|
getServiceEchoes,
|
|
1022
|
+
parseEchoesSchema,
|
|
528
1023
|
publish,
|
|
529
1024
|
request,
|
|
1025
|
+
runWithEchoesContext,
|
|
530
1026
|
startService,
|
|
531
|
-
stopService
|
|
1027
|
+
stopService,
|
|
1028
|
+
typedEchoesSchema
|
|
532
1029
|
};
|
|
533
1030
|
//# sourceMappingURL=index.js.map
|