@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.cjs
CHANGED
|
@@ -32,14 +32,26 @@ __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
|
|
|
@@ -47,38 +59,175 @@ module.exports = __toCommonJS(index_exports);
|
|
|
47
59
|
var config = {};
|
|
48
60
|
var config_default = config;
|
|
49
61
|
|
|
50
|
-
// src/
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
throw new Error(`Echo named ${method} not found in this service`);
|
|
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");
|
|
55
66
|
}
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
return await config_default.eventBus.publish(options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/echo/deserialize.ts
|
|
71
|
+
function deserialize_default(serializedJavascript) {
|
|
72
|
+
try {
|
|
73
|
+
return eval("(" + serializedJavascript + ")");
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error("Error deserializing echo message");
|
|
58
76
|
}
|
|
59
|
-
return echo2;
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
// src/publish/serialize.ts
|
|
63
80
|
var import_serialize_javascript = __toESM(require("serialize-javascript"), 1);
|
|
64
|
-
var import_helpers = require("@orion-js/helpers");
|
|
65
81
|
function serialize_default(data) {
|
|
66
|
-
const
|
|
67
|
-
const serialized = (0, import_serialize_javascript.default)(cloned, { ignoreFunction: true });
|
|
82
|
+
const serialized = (0, import_serialize_javascript.default)(data, { ignoreFunction: true });
|
|
68
83
|
return serialized;
|
|
69
84
|
}
|
|
70
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
|
+
|
|
71
222
|
// src/request/getSignature.ts
|
|
72
|
-
var
|
|
223
|
+
var import_node_crypto2 = require("crypto");
|
|
73
224
|
|
|
74
225
|
// src/request/getPassword.ts
|
|
75
|
-
var import_logger = require("@orion-js/logger");
|
|
76
|
-
var import_env = require("@orion-js/env");
|
|
77
226
|
function getEchoesPassword() {
|
|
78
227
|
var _a, _b;
|
|
79
|
-
const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) ||
|
|
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;
|
|
80
229
|
if (!secret) {
|
|
81
|
-
|
|
230
|
+
getEchoesLogger().warn(
|
|
82
231
|
'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
|
|
83
232
|
);
|
|
84
233
|
}
|
|
@@ -86,89 +235,244 @@ function getEchoesPassword() {
|
|
|
86
235
|
}
|
|
87
236
|
|
|
88
237
|
// src/request/getSignature.ts
|
|
89
|
-
function getSignature_default(
|
|
238
|
+
function getSignature_default(_body) {
|
|
90
239
|
const password = getEchoesPassword();
|
|
91
|
-
|
|
92
|
-
shaObj.setHMACKey(password, "TEXT");
|
|
93
|
-
shaObj.update(body);
|
|
94
|
-
return shaObj.getHMAC("HEX");
|
|
240
|
+
return (0, import_node_crypto2.createHmac)("sha1", password || "").update("").digest("hex");
|
|
95
241
|
}
|
|
96
242
|
|
|
97
|
-
// src/
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
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}`);
|
|
102
250
|
}
|
|
251
|
+
return url;
|
|
103
252
|
}
|
|
104
253
|
|
|
105
|
-
// src/
|
|
106
|
-
var
|
|
107
|
-
var
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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)
|
|
124
279
|
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
});
|
|
129
316
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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);
|
|
136
363
|
}
|
|
137
|
-
|
|
364
|
+
if (data.isUserError) {
|
|
365
|
+
throw createEchoesUserError(info);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`${data.error}`);
|
|
138
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}`);
|
|
139
376
|
}
|
|
140
|
-
}
|
|
377
|
+
}
|
|
141
378
|
|
|
142
379
|
// src/startService/KafkaManager.ts
|
|
143
|
-
var
|
|
144
|
-
var import_logger2 = require("@orion-js/logger");
|
|
380
|
+
var import_node_crypto3 = require("crypto");
|
|
145
381
|
var HEARTBEAT_INTERVAL_SECONDS = 5;
|
|
146
382
|
var CHECK_JOIN_CONSUMER_INTERVAL_SECONDS = 30;
|
|
147
383
|
var DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY = 4;
|
|
148
384
|
var DEFAULT_MEMBERS_TO_PARTITIONS_RATIO = 1;
|
|
149
385
|
var KafkaManager = class {
|
|
386
|
+
name = "kafka";
|
|
150
387
|
kafka;
|
|
151
388
|
options;
|
|
152
389
|
producer;
|
|
153
390
|
consumer;
|
|
154
|
-
topics;
|
|
155
|
-
|
|
391
|
+
topics = [];
|
|
392
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
393
|
+
onEvent;
|
|
394
|
+
consumerStarted = false;
|
|
395
|
+
producerConnected = false;
|
|
156
396
|
interval;
|
|
157
397
|
constructor(options) {
|
|
158
|
-
this.kafka = new import_kafkajs.Kafka(options.client);
|
|
159
398
|
this.options = options;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
399
|
+
}
|
|
400
|
+
async start(options) {
|
|
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);
|
|
413
|
+
this.onEvent = options.onEvent;
|
|
414
|
+
this.subscriptions = new Map(
|
|
415
|
+
options.subscriptions.map((subscription) => [subscription.topic, subscription])
|
|
416
|
+
);
|
|
417
|
+
this.topics = options.subscriptions.map((subscription) => subscription.topic);
|
|
418
|
+
if (options.publish || options.consume) {
|
|
419
|
+
this.producer = this.kafka.producer(this.options.producer);
|
|
420
|
+
await this.producer.connect();
|
|
421
|
+
this.producerConnected = true;
|
|
422
|
+
}
|
|
423
|
+
if (!options.consume || this.topics.length === 0) return;
|
|
424
|
+
if (!((_a = this.options.consumer) == null ? void 0 : _a.groupId)) {
|
|
425
|
+
throw new Error("Echoes Kafka consumers require consumer.groupId");
|
|
426
|
+
}
|
|
427
|
+
this.consumer = this.kafka.consumer(this.options.consumer);
|
|
428
|
+
this.consumerStarted = await this.conditionalStart();
|
|
429
|
+
if (this.consumerStarted) return;
|
|
430
|
+
getEchoesLogger().info("Echoes: Delaying consumer group join, waiting for conditions to be met");
|
|
431
|
+
this.interval = setInterval(async () => {
|
|
432
|
+
this.consumerStarted = await this.conditionalStart();
|
|
433
|
+
if (this.consumerStarted) clearInterval(this.interval);
|
|
434
|
+
}, CHECK_JOIN_CONSUMER_INTERVAL_SECONDS * 1e3);
|
|
435
|
+
}
|
|
436
|
+
async publish(options) {
|
|
437
|
+
if (!this.producer || !this.producerConnected) {
|
|
438
|
+
throw new Error("Echoes Kafka producer is not connected");
|
|
439
|
+
}
|
|
440
|
+
return await this.producer.send({
|
|
441
|
+
acks: options.acks,
|
|
442
|
+
timeout: options.timeout,
|
|
443
|
+
topic: options.topic,
|
|
444
|
+
messages: [
|
|
445
|
+
{
|
|
446
|
+
value: serialize_default({ params: options.params }),
|
|
447
|
+
headers: {
|
|
448
|
+
"echoes-event-id": (0, import_node_crypto3.randomUUID)()
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async close() {
|
|
455
|
+
var _a, _b;
|
|
456
|
+
const logger = getEchoesLogger();
|
|
457
|
+
logger.warn("Echoes: Stopping Kafka transport");
|
|
458
|
+
if (this.interval) clearInterval(this.interval);
|
|
459
|
+
await Promise.all([
|
|
460
|
+
(_a = this.consumer) == null ? void 0 : _a.disconnect(),
|
|
461
|
+
this.producerConnected ? (_b = this.producer) == null ? void 0 : _b.disconnect() : void 0
|
|
462
|
+
]);
|
|
463
|
+
this.consumerStarted = false;
|
|
464
|
+
this.producerConnected = false;
|
|
163
465
|
}
|
|
164
466
|
async checkJoinConsumerGroupConditions() {
|
|
467
|
+
const logger = getEchoesLogger();
|
|
165
468
|
const admin = this.kafka.admin();
|
|
166
469
|
try {
|
|
167
470
|
await admin.connect();
|
|
168
|
-
const
|
|
471
|
+
const groupId = this.options.consumer.groupId;
|
|
472
|
+
const groupDescriptions = await admin.describeGroups([groupId]);
|
|
169
473
|
const group = groupDescriptions.groups[0];
|
|
170
474
|
if (group.state === "Empty") {
|
|
171
|
-
|
|
475
|
+
logger.info(`Echoes: Consumer group ${groupId} is empty, joining`);
|
|
172
476
|
return true;
|
|
173
477
|
}
|
|
174
478
|
const topicsMetadata = await admin.fetchTopicMetadata({ topics: this.topics });
|
|
@@ -176,23 +480,24 @@ var KafkaManager = class {
|
|
|
176
480
|
(acc, topic) => acc + topic.partitions.length,
|
|
177
481
|
0
|
|
178
482
|
);
|
|
179
|
-
|
|
180
|
-
`Echoes: Consumer group ${
|
|
483
|
+
logger.info(
|
|
484
|
+
`Echoes: Consumer group ${groupId} has ${group.members.length} members and ${totalPartitions} partitions`
|
|
181
485
|
);
|
|
182
486
|
const partitionsRatio = this.options.membersToPartitionsRatio || DEFAULT_MEMBERS_TO_PARTITIONS_RATIO;
|
|
183
487
|
const partitionsThreshold = Math.ceil(totalPartitions * partitionsRatio);
|
|
184
488
|
if (partitionsThreshold > group.members.length) {
|
|
185
|
-
|
|
186
|
-
`Echoes: Consumer group ${
|
|
489
|
+
logger.info(
|
|
490
|
+
`Echoes: Consumer group ${groupId} has room for more members ${group.members.length}/${partitionsThreshold}, joining`
|
|
187
491
|
);
|
|
188
492
|
return true;
|
|
189
493
|
}
|
|
494
|
+
return false;
|
|
190
495
|
} catch (error) {
|
|
191
|
-
|
|
496
|
+
logger.error("Echoes: Error checking consumer group conditions, join", { error });
|
|
192
497
|
return true;
|
|
193
498
|
} finally {
|
|
194
499
|
await admin.disconnect().catch((error) => {
|
|
195
|
-
|
|
500
|
+
logger.error("Echoes: Error disconnecting admin client", { error });
|
|
196
501
|
});
|
|
197
502
|
}
|
|
198
503
|
}
|
|
@@ -209,67 +514,83 @@ var KafkaManager = class {
|
|
|
209
514
|
await this.joinConsumerGroup();
|
|
210
515
|
return true;
|
|
211
516
|
}
|
|
212
|
-
|
|
213
|
-
async start() {
|
|
214
|
-
if (this.started) return;
|
|
215
|
-
await this.producer.connect();
|
|
216
|
-
this.started = await this.conditionalStart();
|
|
217
|
-
if (this.started) return;
|
|
218
|
-
import_logger2.logger.info("Echoes: Delaying consumer group join, waiting for conditions to be met");
|
|
219
|
-
this.interval = setInterval(async () => {
|
|
220
|
-
this.started = await this.conditionalStart();
|
|
221
|
-
if (this.started) clearInterval(this.interval);
|
|
222
|
-
}, CHECK_JOIN_CONSUMER_INTERVAL_SECONDS * 1e3);
|
|
223
|
-
}
|
|
224
|
-
async stop() {
|
|
225
|
-
import_logger2.logger.warn("Echoes: Stopping echoes");
|
|
226
|
-
if (this.interval) clearInterval(this.interval);
|
|
227
|
-
if (this.consumer) await this.consumer.disconnect();
|
|
228
|
-
if (this.producer) await this.producer.disconnect();
|
|
517
|
+
return false;
|
|
229
518
|
}
|
|
230
519
|
async handleMessage(params) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
520
|
+
const logger = getEchoesLogger();
|
|
521
|
+
const subscription = this.subscriptions.get(params.topic);
|
|
522
|
+
if (!subscription) {
|
|
523
|
+
logger.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
|
|
234
524
|
return;
|
|
235
525
|
}
|
|
236
526
|
let intervalsCount = 0;
|
|
237
527
|
const heartbeatInterval = setInterval(async () => {
|
|
238
528
|
await params.heartbeat().catch((error) => {
|
|
239
|
-
|
|
529
|
+
logger.warn(`Echoes: Error sending heartbeat: ${error.message}`);
|
|
240
530
|
});
|
|
241
531
|
intervalsCount++;
|
|
242
532
|
if (intervalsCount * HEARTBEAT_INTERVAL_SECONDS % 30 === 0) {
|
|
243
|
-
|
|
533
|
+
logger.warn(
|
|
244
534
|
`Echoes: Event is taking too long to process: ${params.topic} ${intervalsCount * HEARTBEAT_INTERVAL_SECONDS}s`
|
|
245
535
|
);
|
|
246
536
|
}
|
|
247
537
|
}, HEARTBEAT_INTERVAL_SECONDS * 1e3);
|
|
248
538
|
try {
|
|
249
|
-
|
|
539
|
+
const event = this.createReceivedEvent(params);
|
|
540
|
+
await this.onEvent(event);
|
|
250
541
|
} catch (error) {
|
|
251
|
-
|
|
252
|
-
|
|
542
|
+
try {
|
|
543
|
+
await this.handleRetries(subscription, params, error);
|
|
544
|
+
} catch (retryError) {
|
|
545
|
+
logger.error("Echoes: error processing a message", {
|
|
546
|
+
error: retryError,
|
|
547
|
+
topic: params.topic
|
|
548
|
+
});
|
|
549
|
+
throw retryError;
|
|
550
|
+
}
|
|
253
551
|
} finally {
|
|
254
552
|
clearInterval(heartbeatInterval);
|
|
255
553
|
}
|
|
256
554
|
}
|
|
257
|
-
|
|
555
|
+
createReceivedEvent(params) {
|
|
556
|
+
var _a, _b, _c, _d;
|
|
557
|
+
const { message, topic, partition } = params;
|
|
558
|
+
if (!message.value) {
|
|
559
|
+
throw new Error(`Echoes received an empty Kafka message for ${topic}`);
|
|
560
|
+
}
|
|
561
|
+
const data = deserialize_default(message.value.toString());
|
|
562
|
+
const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
563
|
+
const timestamp = Number.parseInt(message.timestamp || "", 10);
|
|
564
|
+
return {
|
|
565
|
+
id: ((_d = (_c = message.headers) == null ? void 0 : _c["echoes-event-id"]) == null ? void 0 : _d.toString()) || `kafka:${topic}:${partition}:${message.offset}`,
|
|
566
|
+
topic,
|
|
567
|
+
data,
|
|
568
|
+
transport: "kafka",
|
|
569
|
+
headers: message.headers,
|
|
570
|
+
createdAt: Number.isFinite(timestamp) ? new Date(timestamp) : /* @__PURE__ */ new Date(),
|
|
571
|
+
attempt: retries + 1,
|
|
572
|
+
context: params
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
async handleRetries(subscription, params, error) {
|
|
258
576
|
var _a, _b;
|
|
577
|
+
const logger = getEchoesLogger();
|
|
259
578
|
const { message, topic } = params;
|
|
260
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);
|
|
261
|
-
if (
|
|
580
|
+
if (subscription.attemptsBeforeDeadLetter === void 0 || subscription.attemptsBeforeDeadLetter === null) {
|
|
262
581
|
throw error;
|
|
263
582
|
}
|
|
264
|
-
const maxRetries =
|
|
583
|
+
const maxRetries = subscription.attemptsBeforeDeadLetter || 0;
|
|
265
584
|
const exceededMaxRetries = retries >= maxRetries;
|
|
266
585
|
const nextTopic = exceededMaxRetries ? `DLQ-${topic}` : topic;
|
|
586
|
+
if (!message.value) throw error;
|
|
267
587
|
await this.producer.send({
|
|
268
588
|
topic: nextTopic,
|
|
269
589
|
messages: [
|
|
270
590
|
{
|
|
271
591
|
value: message.value.toString(),
|
|
272
592
|
headers: {
|
|
593
|
+
...message.headers,
|
|
273
594
|
retries: String(retries + 1),
|
|
274
595
|
error: error.message
|
|
275
596
|
}
|
|
@@ -277,180 +598,339 @@ var KafkaManager = class {
|
|
|
277
598
|
]
|
|
278
599
|
});
|
|
279
600
|
if (exceededMaxRetries) {
|
|
280
|
-
|
|
601
|
+
logger.error(
|
|
281
602
|
"Echoes: a message has reached the maximum number of retries, sending it to DLQ",
|
|
282
603
|
{ topic: nextTopic }
|
|
283
604
|
);
|
|
284
605
|
} else {
|
|
285
|
-
|
|
606
|
+
logger.warn("Echoes: a retryable message failed", { error, topic: nextTopic });
|
|
286
607
|
}
|
|
287
608
|
}
|
|
288
609
|
};
|
|
289
610
|
var KafkaManager_default = KafkaManager;
|
|
290
611
|
|
|
291
|
-
// src/
|
|
292
|
-
var
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
612
|
+
// src/events/EventBus.ts
|
|
613
|
+
var EventBus = class {
|
|
614
|
+
echoes;
|
|
615
|
+
transports;
|
|
616
|
+
consumeFrom;
|
|
617
|
+
publishTo;
|
|
618
|
+
startedTransports = [];
|
|
619
|
+
started = false;
|
|
620
|
+
closed = false;
|
|
621
|
+
constructor(options) {
|
|
622
|
+
this.echoes = options.echoes;
|
|
623
|
+
this.transports = options.transports;
|
|
624
|
+
this.consumeFrom = [...new Set(options.consumeFrom)];
|
|
625
|
+
this.publishTo = options.publishTo;
|
|
299
626
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
627
|
+
async start() {
|
|
628
|
+
if (this.started) return;
|
|
629
|
+
const selected = new Set(this.consumeFrom);
|
|
630
|
+
if (this.publishTo) selected.add(this.publishTo);
|
|
631
|
+
for (const name of selected) {
|
|
632
|
+
if (!this.transports[name]) {
|
|
633
|
+
throw new Error(`Echoes event transport "${name}" is selected but not configured`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const subscriptions = Object.entries(this.echoes).filter(([, echo2]) => echo2.type === "event").map(([topic, echo2]) => ({
|
|
637
|
+
topic,
|
|
638
|
+
attemptsBeforeDeadLetter: echo2.attemptsBeforeDeadLetter
|
|
639
|
+
}));
|
|
640
|
+
try {
|
|
641
|
+
for (const name of selected) {
|
|
642
|
+
const transport = this.transports[name];
|
|
643
|
+
this.startedTransports.push(transport);
|
|
644
|
+
await transport.start({
|
|
645
|
+
consume: this.consumeFrom.includes(name),
|
|
646
|
+
publish: this.publishTo === name,
|
|
647
|
+
subscriptions,
|
|
648
|
+
onEvent: (event) => this.handleEvent(event)
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
this.started = true;
|
|
652
|
+
} catch (error) {
|
|
653
|
+
await this.close().catch(() => void 0);
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
305
656
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
657
|
+
async publish(options) {
|
|
658
|
+
if (!this.started) {
|
|
659
|
+
throw new Error("You must initialize echoes configuration to use publish");
|
|
660
|
+
}
|
|
661
|
+
if (!this.publishTo) {
|
|
662
|
+
throw new Error("Echoes does not have a publish transport configured");
|
|
663
|
+
}
|
|
664
|
+
return await this.transports[this.publishTo].publish(options);
|
|
312
665
|
}
|
|
313
|
-
|
|
666
|
+
async close() {
|
|
667
|
+
if (this.closed) return;
|
|
668
|
+
this.closed = true;
|
|
669
|
+
await Promise.all(this.startedTransports.map((transport) => transport.close()));
|
|
670
|
+
}
|
|
671
|
+
async handleEvent(event) {
|
|
672
|
+
const logger = getEchoesLogger();
|
|
673
|
+
const echo2 = this.echoes[event.topic];
|
|
674
|
+
if (!echo2 || echo2.type !== "event") {
|
|
675
|
+
logger.warn(`Echoes: Received a message for an unknown topic: ${event.topic}, ignoring it`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (event.transport === "kafka" && event.context) {
|
|
679
|
+
await echo2.onMessage(event.context);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (echo2.onEvent) {
|
|
683
|
+
await echo2.onEvent(event);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
await echo2.resolve(event.data.params, {
|
|
687
|
+
...event.context || {},
|
|
688
|
+
transport: event.transport,
|
|
689
|
+
eventId: event.id,
|
|
690
|
+
topic: event.topic,
|
|
691
|
+
headers: event.headers,
|
|
692
|
+
createdAt: event.createdAt,
|
|
693
|
+
attempt: event.attempt,
|
|
694
|
+
data: event.data
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
};
|
|
314
698
|
|
|
315
|
-
// src/
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
699
|
+
// src/events/PulseManager.ts
|
|
700
|
+
var PulseManager = class {
|
|
701
|
+
name = "pulse";
|
|
702
|
+
options;
|
|
703
|
+
pulse;
|
|
704
|
+
subscriptions = [];
|
|
705
|
+
constructor(options) {
|
|
706
|
+
this.options = options;
|
|
707
|
+
}
|
|
708
|
+
async start(options) {
|
|
709
|
+
let pulseModule;
|
|
710
|
+
try {
|
|
711
|
+
pulseModule = await import("@orion-js/pulse");
|
|
712
|
+
} catch (error) {
|
|
713
|
+
const wrapped = new Error(
|
|
714
|
+
"Echoes Pulse transport requires @orion-js/pulse to be installed in the application"
|
|
715
|
+
);
|
|
716
|
+
wrapped.cause = error;
|
|
717
|
+
throw wrapped;
|
|
718
|
+
}
|
|
719
|
+
const { subscription, ...connectOptions } = this.options;
|
|
720
|
+
this.pulse = pulseModule.connect(connectOptions);
|
|
721
|
+
await this.pulse.awaitConnection();
|
|
722
|
+
if (!options.consume) return;
|
|
723
|
+
this.subscriptions = await Promise.all(
|
|
724
|
+
options.subscriptions.map(
|
|
725
|
+
(definition) => this.pulse.subscribe(
|
|
726
|
+
definition.topic,
|
|
727
|
+
(event) => options.onEvent(this.createReceivedEvent(event)),
|
|
728
|
+
this.getSubscribeOptions(subscription, definition.attemptsBeforeDeadLetter)
|
|
729
|
+
)
|
|
730
|
+
)
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
async publish(options) {
|
|
734
|
+
if (!this.pulse) {
|
|
735
|
+
throw new Error("Echoes Pulse client is not connected");
|
|
736
|
+
}
|
|
737
|
+
return await this.pulse.publish({
|
|
738
|
+
topic: options.topic,
|
|
739
|
+
data: { params: options.params }
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
async close() {
|
|
743
|
+
var _a;
|
|
744
|
+
await ((_a = this.pulse) == null ? void 0 : _a.close());
|
|
745
|
+
this.subscriptions = [];
|
|
746
|
+
}
|
|
747
|
+
getSubscribeOptions(defaults = {}, attemptsBeforeDeadLetter) {
|
|
748
|
+
return {
|
|
749
|
+
...defaults,
|
|
750
|
+
...attemptsBeforeDeadLetter === void 0 ? {} : { maxRetries: attemptsBeforeDeadLetter }
|
|
751
|
+
};
|
|
319
752
|
}
|
|
320
|
-
|
|
321
|
-
|
|
753
|
+
createReceivedEvent(event) {
|
|
754
|
+
return {
|
|
755
|
+
id: event.id,
|
|
756
|
+
topic: event.topic,
|
|
757
|
+
data: event.data,
|
|
758
|
+
transport: "pulse",
|
|
759
|
+
headers: event.headers,
|
|
760
|
+
createdAt: event.createdAt,
|
|
761
|
+
attempt: event.attempt,
|
|
762
|
+
context: event
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// src/events/createEventBus.ts
|
|
768
|
+
function resolveEventsConfig(options) {
|
|
769
|
+
var _a, _b, _c, _d;
|
|
770
|
+
const legacyKafka = options.client ? {
|
|
771
|
+
client: options.client,
|
|
772
|
+
producer: options.producer,
|
|
773
|
+
consumer: options.consumer,
|
|
774
|
+
readTopicsFromBeginning: options.readTopicsFromBeginning,
|
|
775
|
+
partitionsConsumedConcurrently: options.partitionsConsumedConcurrently,
|
|
776
|
+
membersToPartitionsRatio: options.membersToPartitionsRatio
|
|
777
|
+
} : void 0;
|
|
778
|
+
const kafka = ((_a = options.events) == null ? void 0 : _a.kafka) || legacyKafka;
|
|
779
|
+
const pulse = (_b = options.events) == null ? void 0 : _b.pulse;
|
|
780
|
+
if (!kafka && !pulse && !options.events) return void 0;
|
|
781
|
+
const defaultTransport = kafka ? "kafka" : pulse ? "pulse" : void 0;
|
|
782
|
+
return {
|
|
783
|
+
kafka,
|
|
784
|
+
pulse,
|
|
785
|
+
consumeFrom: ((_c = options.events) == null ? void 0 : _c.consumeFrom) || (defaultTransport ? [defaultTransport] : []),
|
|
786
|
+
publishTo: ((_d = options.events) == null ? void 0 : _d.publishTo) || defaultTransport
|
|
322
787
|
};
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
788
|
+
}
|
|
789
|
+
function createEventBus(options) {
|
|
790
|
+
const resolved = resolveEventsConfig(options);
|
|
791
|
+
if (!resolved) return void 0;
|
|
792
|
+
const transports = {};
|
|
793
|
+
if (resolved.kafka) transports.kafka = new KafkaManager_default(resolved.kafka);
|
|
794
|
+
if (resolved.pulse) transports.pulse = new PulseManager(resolved.pulse);
|
|
795
|
+
return new EventBus({
|
|
796
|
+
echoes: options.echoes,
|
|
797
|
+
transports,
|
|
798
|
+
consumeFrom: resolved.consumeFrom,
|
|
799
|
+
publishTo: resolved.publishTo
|
|
332
800
|
});
|
|
333
801
|
}
|
|
334
802
|
|
|
335
|
-
// src/
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
if (!url) {
|
|
341
|
-
throw new Error(`No URL found in echoes config for service ${serviceName}`);
|
|
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");
|
|
342
808
|
}
|
|
343
|
-
return url;
|
|
344
809
|
}
|
|
345
810
|
|
|
346
|
-
// src/
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
throw new Error("Error deserializing echo message");
|
|
811
|
+
// src/requestsHandler/getEcho.ts
|
|
812
|
+
function getEcho_default(method) {
|
|
813
|
+
const echo2 = config_default.echoes[method];
|
|
814
|
+
if (!echo2) {
|
|
815
|
+
throw new Error(`Echo named ${method} not found in this service`);
|
|
352
816
|
}
|
|
817
|
+
if (echo2.type !== "request") {
|
|
818
|
+
throw new Error(`Echo named ${method} is not of type request`);
|
|
819
|
+
}
|
|
820
|
+
return echo2;
|
|
353
821
|
}
|
|
354
822
|
|
|
355
|
-
// src/
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
200
|
|
373
|
-
);
|
|
374
|
-
return {
|
|
375
|
-
data: result.data,
|
|
376
|
-
statusCode: result.status
|
|
377
|
-
};
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
// src/request/index.ts
|
|
381
|
-
var import_schema = require("@orion-js/schema");
|
|
382
|
-
var import_helpers3 = require("@orion-js/helpers");
|
|
383
|
-
async function request(options) {
|
|
384
|
-
var _a, _b;
|
|
385
|
-
const { method, service, params } = options;
|
|
386
|
-
const serializedParams = serialize_default(params);
|
|
387
|
-
const date = /* @__PURE__ */ new Date();
|
|
388
|
-
const body = { method, service, serializedParams, date };
|
|
389
|
-
const signature = getSignature_default(body);
|
|
390
|
-
try {
|
|
391
|
-
const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
|
|
392
|
-
const requestOptions = {
|
|
393
|
-
url: getURL_default(service),
|
|
394
|
-
retries: options.retries,
|
|
395
|
-
timeout: options.timeout,
|
|
396
|
-
data: {
|
|
397
|
-
body,
|
|
398
|
-
signature
|
|
823
|
+
// src/requestsHandler/index.ts
|
|
824
|
+
var requestsHandler_default = (options) => ({
|
|
825
|
+
method: "post",
|
|
826
|
+
path: options.requests.handlerPath || "/echoes-services",
|
|
827
|
+
bodyLimit: "10mb",
|
|
828
|
+
async handle(requestBody) {
|
|
829
|
+
try {
|
|
830
|
+
const { body, signature } = requestBody;
|
|
831
|
+
checkSignature_default(body, signature);
|
|
832
|
+
const { method, serializedParams } = body;
|
|
833
|
+
const echo2 = getEcho_default(method);
|
|
834
|
+
const result = await echo2.onRequest(serializedParams);
|
|
835
|
+
return { result: serialize_default(result) };
|
|
836
|
+
} catch (error) {
|
|
837
|
+
const caught = error;
|
|
838
|
+
if (!caught.getInfo) {
|
|
839
|
+
getEchoesLogger().error("Error at echo requests handler:", { error: caught });
|
|
399
840
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
841
|
+
return {
|
|
842
|
+
error: caught.message,
|
|
843
|
+
errorInfo: caught.getInfo ? caught.getInfo() : null,
|
|
844
|
+
isValidationError: !!caught.isValidationError,
|
|
845
|
+
isUserError: !!caught.isUserError
|
|
846
|
+
};
|
|
404
847
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
throw new Error(
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// src/startService/index.ts
|
|
852
|
+
var eventBus = null;
|
|
853
|
+
async function startService(options) {
|
|
854
|
+
config_default.echoes = options.echoes;
|
|
855
|
+
if (options.requests) {
|
|
856
|
+
config_default.requests = options.requests;
|
|
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
|
+
);
|
|
417
862
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
863
|
+
await registerHandler(requestsHandler_default(options));
|
|
864
|
+
}
|
|
865
|
+
const nextEventBus = createEventBus(options);
|
|
866
|
+
if (nextEventBus) {
|
|
867
|
+
await nextEventBus.start();
|
|
868
|
+
eventBus = nextEventBus;
|
|
869
|
+
config_default.eventBus = eventBus;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
async function stopService() {
|
|
873
|
+
if (eventBus) {
|
|
874
|
+
const logger = getEchoesLogger();
|
|
875
|
+
logger.info("Stopping Echoes...");
|
|
876
|
+
await eventBus.close();
|
|
877
|
+
eventBus = null;
|
|
878
|
+
config_default.eventBus = void 0;
|
|
879
|
+
logger.info("Echoes stopped");
|
|
423
880
|
}
|
|
424
881
|
}
|
|
425
|
-
|
|
426
|
-
// src/service/index.ts
|
|
427
|
-
var import_logger3 = require("@orion-js/logger");
|
|
428
882
|
|
|
429
883
|
// src/echo/index.ts
|
|
430
|
-
var import_schema2 = require("@orion-js/schema");
|
|
431
884
|
var echo = function createNewEcho(options) {
|
|
432
885
|
const resolve = async (params, context) => {
|
|
433
|
-
const cleaned = options.params ? await (
|
|
886
|
+
const cleaned = options.params ? await parseEchoesSchema(options.params, params) : params ?? {};
|
|
434
887
|
const result = await options.resolve(cleaned, context);
|
|
435
888
|
if (options.returns) {
|
|
436
|
-
return await (
|
|
889
|
+
return await cleanEchoesSchema(options.returns, result);
|
|
437
890
|
}
|
|
438
891
|
return result;
|
|
439
892
|
};
|
|
893
|
+
const onEvent = async (event) => {
|
|
894
|
+
const context = {
|
|
895
|
+
...event.context || {},
|
|
896
|
+
transport: event.transport,
|
|
897
|
+
eventId: event.id,
|
|
898
|
+
topic: event.topic,
|
|
899
|
+
headers: event.headers,
|
|
900
|
+
createdAt: event.createdAt,
|
|
901
|
+
attempt: event.attempt,
|
|
902
|
+
data: event.data
|
|
903
|
+
};
|
|
904
|
+
await resolve(event.data.params, context);
|
|
905
|
+
};
|
|
440
906
|
return {
|
|
441
907
|
type: options.type,
|
|
442
908
|
params: options.params,
|
|
443
909
|
returns: options.returns,
|
|
444
910
|
attemptsBeforeDeadLetter: options.type === "event" ? options.attemptsBeforeDeadLetter : void 0,
|
|
445
911
|
resolve,
|
|
912
|
+
onEvent,
|
|
446
913
|
onMessage: async (messageData) => {
|
|
914
|
+
var _a, _b, _c, _d;
|
|
447
915
|
const { message } = messageData;
|
|
916
|
+
if (!message.value) {
|
|
917
|
+
throw new Error(`Echoes received an empty Kafka message for ${messageData.topic}`);
|
|
918
|
+
}
|
|
448
919
|
const data = deserialize_default(message.value.toString());
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
await
|
|
920
|
+
const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
921
|
+
const timestamp = Number(message.timestamp);
|
|
922
|
+
const createdAt = Number.isFinite(timestamp) ? new Date(timestamp) : /* @__PURE__ */ new Date();
|
|
923
|
+
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}`;
|
|
924
|
+
await onEvent({
|
|
925
|
+
id: eventId,
|
|
926
|
+
topic: messageData.topic,
|
|
927
|
+
data,
|
|
928
|
+
transport: "kafka",
|
|
929
|
+
headers: message.headers,
|
|
930
|
+
createdAt,
|
|
931
|
+
attempt: retries + 1,
|
|
932
|
+
context: messageData
|
|
933
|
+
});
|
|
454
934
|
},
|
|
455
935
|
onRequest: async (serializedParams) => {
|
|
456
936
|
const context = {};
|
|
@@ -466,15 +946,21 @@ function createEchoEvent(options) {
|
|
|
466
946
|
return echo({ ...options, type: "event" });
|
|
467
947
|
}
|
|
468
948
|
|
|
949
|
+
// src/schema.ts
|
|
950
|
+
function typedEchoesSchema(schema) {
|
|
951
|
+
return schema;
|
|
952
|
+
}
|
|
953
|
+
|
|
469
954
|
// src/service/index.ts
|
|
470
|
-
var import_services = require("@orion-js/services");
|
|
471
955
|
var serviceMetadata = /* @__PURE__ */ new WeakMap();
|
|
472
956
|
var echoesMetadata = /* @__PURE__ */ new WeakMap();
|
|
473
957
|
var echoEntriesByClass = /* @__PURE__ */ new Map();
|
|
958
|
+
var standaloneInstances = /* @__PURE__ */ new WeakMap();
|
|
474
959
|
var pendingEchoEntries = {};
|
|
475
960
|
function Echoes() {
|
|
476
961
|
return (target, context) => {
|
|
477
|
-
|
|
962
|
+
var _a, _b;
|
|
963
|
+
(_b = (_a = getEchoesRuntime()).decorateService) == null ? void 0 : _b.call(_a, target, context);
|
|
478
964
|
serviceMetadata.set(target, { _serviceType: "echoes" });
|
|
479
965
|
if (Object.keys(pendingEchoEntries).length > 0) {
|
|
480
966
|
echoEntriesByClass.set(target, pendingEchoEntries);
|
|
@@ -491,7 +977,7 @@ function EchoEvent(options = {}) {
|
|
|
491
977
|
return createEchoEvent({
|
|
492
978
|
...options,
|
|
493
979
|
resolve: async (params, contextData) => {
|
|
494
|
-
return await (
|
|
980
|
+
return await runWithEchoesContext(
|
|
495
981
|
{
|
|
496
982
|
controllerType: "echo",
|
|
497
983
|
echoName: propertyKey,
|
|
@@ -520,7 +1006,7 @@ function EchoRequest(options = {}) {
|
|
|
520
1006
|
return createEchoRequest({
|
|
521
1007
|
...options,
|
|
522
1008
|
resolve: async (params, contextData) => {
|
|
523
|
-
return await (
|
|
1009
|
+
return await runWithEchoesContext(
|
|
524
1010
|
{
|
|
525
1011
|
controllerType: "echo",
|
|
526
1012
|
echoName: propertyKey,
|
|
@@ -550,7 +1036,18 @@ function initializeEchoesIfNeeded(instance) {
|
|
|
550
1036
|
echoesMetadata.set(instance, echoes);
|
|
551
1037
|
}
|
|
552
1038
|
function getServiceEchoes(target) {
|
|
553
|
-
|
|
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
|
+
}
|
|
554
1051
|
if (!serviceMetadata.has(instance.constructor)) {
|
|
555
1052
|
throw new Error("You must pass a class decorated with @Echoes to getServiceEchoes");
|
|
556
1053
|
}
|
|
@@ -567,13 +1064,25 @@ function getServiceEchoes(target) {
|
|
|
567
1064
|
EchoEvent,
|
|
568
1065
|
EchoRequest,
|
|
569
1066
|
Echoes,
|
|
1067
|
+
EchoesUserError,
|
|
1068
|
+
EchoesValidationError,
|
|
1069
|
+
cleanEchoesSchema,
|
|
1070
|
+
configureEchoesRuntime,
|
|
570
1071
|
createEchoEvent,
|
|
571
1072
|
createEchoRequest,
|
|
1073
|
+
createEchoesUserError,
|
|
1074
|
+
createEchoesValidationError,
|
|
572
1075
|
echo,
|
|
1076
|
+
getEchoesContext,
|
|
1077
|
+
getEchoesLogger,
|
|
1078
|
+
getEchoesRuntime,
|
|
573
1079
|
getServiceEchoes,
|
|
1080
|
+
parseEchoesSchema,
|
|
574
1081
|
publish,
|
|
575
1082
|
request,
|
|
1083
|
+
runWithEchoesContext,
|
|
576
1084
|
startService,
|
|
577
|
-
stopService
|
|
1085
|
+
stopService,
|
|
1086
|
+
typedEchoesSchema
|
|
578
1087
|
});
|
|
579
1088
|
//# sourceMappingURL=index.cjs.map
|