@orion-js/echoes 4.4.1 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -32,28 +32,40 @@ __export(index_exports, {
32
32
  EchoEvent: () => EchoEvent,
33
33
  EchoRequest: () => EchoRequest,
34
34
  Echoes: () => Echoes,
35
+ EchoesUserError: () => EchoesUserError,
36
+ EchoesValidationError: () => EchoesValidationError,
37
+ cleanEchoesSchema: () => cleanEchoesSchema,
38
+ configureEchoesRuntime: () => configureEchoesRuntime,
35
39
  createEchoEvent: () => createEchoEvent,
36
40
  createEchoRequest: () => createEchoRequest,
41
+ createEchoesUserError: () => createEchoesUserError,
42
+ createEchoesValidationError: () => createEchoesValidationError,
37
43
  echo: () => echo,
44
+ getEchoesContext: () => getEchoesContext,
45
+ getEchoesLogger: () => getEchoesLogger,
46
+ getEchoesRuntime: () => getEchoesRuntime,
38
47
  getServiceEchoes: () => getServiceEchoes,
48
+ parseEchoesSchema: () => parseEchoesSchema,
39
49
  publish: () => publish,
40
50
  request: () => request,
51
+ runWithEchoesContext: () => runWithEchoesContext,
41
52
  startService: () => startService,
42
- stopService: () => stopService
53
+ stopService: () => stopService,
54
+ typedEchoesSchema: () => typedEchoesSchema
43
55
  });
44
56
  module.exports = __toCommonJS(index_exports);
45
57
 
46
- // src/startService/index.ts
47
- var import_http2 = require("@orion-js/http");
48
-
49
58
  // src/config.ts
50
59
  var config = {};
51
60
  var config_default = config;
52
61
 
53
- // src/startService/KafkaManager.ts
54
- var import_node_crypto = require("crypto");
55
- var import_logger = require("@orion-js/logger");
56
- var import_kafkajs = require("kafkajs");
62
+ // src/publish/index.ts
63
+ async function publish(options) {
64
+ if (!config_default.eventBus) {
65
+ throw new Error("You must initialize echoes configuration to use publish");
66
+ }
67
+ return await config_default.eventBus.publish(options);
68
+ }
57
69
 
58
70
  // src/echo/deserialize.ts
59
71
  function deserialize_default(serializedJavascript) {
@@ -66,14 +78,426 @@ function deserialize_default(serializedJavascript) {
66
78
 
67
79
  // src/publish/serialize.ts
68
80
  var import_serialize_javascript = __toESM(require("serialize-javascript"), 1);
69
- var import_helpers = require("@orion-js/helpers");
81
+ function clonePropertyDescriptor(descriptor, references) {
82
+ const clonedDescriptor = { ...descriptor, configurable: true };
83
+ if ("value" in clonedDescriptor) {
84
+ clonedDescriptor.value = cloneForSerialization(clonedDescriptor.value, references);
85
+ }
86
+ return clonedDescriptor;
87
+ }
88
+ function copyOwnProperties(source, target, references, shouldCopy = () => true) {
89
+ for (const key of Reflect.ownKeys(source)) {
90
+ if (!shouldCopy(key)) continue;
91
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
92
+ if (!descriptor) continue;
93
+ Object.defineProperty(target, key, clonePropertyDescriptor(descriptor, references));
94
+ }
95
+ }
96
+ function preserveCustomToJSON(source, target, references) {
97
+ const toJSON = source.toJSON;
98
+ if (typeof toJSON !== "function") return;
99
+ Object.defineProperty(target, "toJSON", {
100
+ configurable: true,
101
+ enumerable: false,
102
+ writable: true,
103
+ value(key) {
104
+ return cloneForSerialization(toJSON.call(source, key), references);
105
+ }
106
+ });
107
+ }
108
+ function cloneArrayBufferView(value, references) {
109
+ const sourceBuffer = value.buffer;
110
+ let clonedBuffer = references.get(sourceBuffer);
111
+ const shouldCopyBufferProperties = !clonedBuffer;
112
+ if (!clonedBuffer) {
113
+ clonedBuffer = sourceBuffer.slice(0);
114
+ references.set(sourceBuffer, clonedBuffer);
115
+ }
116
+ const clone = value instanceof DataView ? new DataView(clonedBuffer, value.byteOffset, value.byteLength) : new value.constructor(clonedBuffer, value.byteOffset, value.length);
117
+ references.set(value, clone);
118
+ if (shouldCopyBufferProperties) {
119
+ copyOwnProperties(sourceBuffer, clonedBuffer, references);
120
+ }
121
+ copyOwnProperties(value, clone, references, (key) => {
122
+ if (typeof key !== "string") return true;
123
+ const index = Number(key);
124
+ return !(Number.isInteger(index) && index >= 0 && index < value.length && String(index) === key);
125
+ });
126
+ return clone;
127
+ }
128
+ function cloneForSerialization(value, references = /* @__PURE__ */ new WeakMap()) {
129
+ if (value === null || typeof value !== "object" && typeof value !== "function") return value;
130
+ if (typeof value === "function") return value;
131
+ const source = value;
132
+ if (references.has(source)) return references.get(source);
133
+ if (value instanceof Date) {
134
+ const clone2 = new Date(value.getTime());
135
+ Object.setPrototypeOf(clone2, Object.getPrototypeOf(value));
136
+ references.set(source, clone2);
137
+ return clone2;
138
+ }
139
+ if (value instanceof RegExp) {
140
+ const clone2 = new RegExp(value.source, value.flags);
141
+ clone2.lastIndex = value.lastIndex;
142
+ Object.setPrototypeOf(clone2, Object.getPrototypeOf(value));
143
+ references.set(source, clone2);
144
+ return clone2;
145
+ }
146
+ if (value instanceof Map) {
147
+ const clone2 = /* @__PURE__ */ new Map();
148
+ references.set(source, clone2);
149
+ for (const [key, entry] of value) {
150
+ Map.prototype.set.call(
151
+ clone2,
152
+ cloneForSerialization(key, references),
153
+ cloneForSerialization(entry, references)
154
+ );
155
+ }
156
+ Object.setPrototypeOf(clone2, Object.getPrototypeOf(value));
157
+ return clone2;
158
+ }
159
+ if (value instanceof Set) {
160
+ const clone2 = /* @__PURE__ */ new Set();
161
+ references.set(source, clone2);
162
+ for (const entry of value) {
163
+ Set.prototype.add.call(clone2, cloneForSerialization(entry, references));
164
+ }
165
+ Object.setPrototypeOf(clone2, Object.getPrototypeOf(value));
166
+ return clone2;
167
+ }
168
+ if (value instanceof URL) {
169
+ const clone2 = new URL(value.toString());
170
+ Object.setPrototypeOf(clone2, Object.getPrototypeOf(value));
171
+ references.set(source, clone2);
172
+ return clone2;
173
+ }
174
+ if (Buffer.isBuffer(value)) {
175
+ const clone2 = Buffer.from(value);
176
+ references.set(source, clone2);
177
+ return clone2;
178
+ }
179
+ if (ArrayBuffer.isView(value)) {
180
+ return cloneArrayBufferView(value, references);
181
+ }
182
+ if (value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer) {
183
+ const clone2 = value.slice(0);
184
+ references.set(source, clone2);
185
+ copyOwnProperties(value, clone2, references);
186
+ return clone2;
187
+ }
188
+ if (Array.isArray(value)) {
189
+ const clone2 = new Array(value.length);
190
+ Object.setPrototypeOf(clone2, Object.getPrototypeOf(value));
191
+ references.set(source, clone2);
192
+ copyOwnProperties(value, clone2, references, (key) => key !== "length");
193
+ return clone2;
194
+ }
195
+ const clone = Object.create(Object.getPrototypeOf(value));
196
+ references.set(source, clone);
197
+ copyOwnProperties(value, clone, references);
198
+ preserveCustomToJSON(source, clone, references);
199
+ return clone;
200
+ }
70
201
  function serialize_default(data) {
71
- const cloned = (0, import_helpers.clone)(data);
72
- const serialized = (0, import_serialize_javascript.default)(cloned, { ignoreFunction: true });
202
+ const serialized = (0, import_serialize_javascript.default)(cloneForSerialization(data), { ignoreFunction: true });
73
203
  return serialized;
74
204
  }
75
205
 
206
+ // src/runtime.ts
207
+ var import_node_async_hooks = require("async_hooks");
208
+ var import_node_crypto = require("crypto");
209
+
210
+ // src/errors.ts
211
+ var EchoesUserError = class extends Error {
212
+ isEchoesError = true;
213
+ isOrionError = true;
214
+ isUserError = true;
215
+ code;
216
+ extra;
217
+ constructor(code, message, extra) {
218
+ if (!message) {
219
+ message = code;
220
+ code = "error";
221
+ }
222
+ super(message);
223
+ this.name = "EchoesUserError";
224
+ this.code = code;
225
+ this.extra = extra;
226
+ }
227
+ getInfo() {
228
+ return { error: this.code, message: this.message, extra: this.extra };
229
+ }
230
+ };
231
+ var EchoesValidationError = class extends Error {
232
+ isEchoesError = true;
233
+ isOrionError = true;
234
+ isValidationError = true;
235
+ code = "validationError";
236
+ validationErrors;
237
+ labels;
238
+ constructor(validationErrors, labels = {}) {
239
+ const printableErrors = Object.entries(validationErrors).map(([key, value]) => `${key}: ${value}`).join(", ");
240
+ super(`Validation Error: {${printableErrors}}`);
241
+ this.name = "EchoesValidationError";
242
+ this.validationErrors = validationErrors;
243
+ this.labels = Object.fromEntries(
244
+ Object.keys(validationErrors).filter((key) => labels[key]).map((key) => [key, labels[key]])
245
+ );
246
+ }
247
+ getInfo() {
248
+ return {
249
+ error: this.code,
250
+ message: "Validation Error",
251
+ validationErrors: this.validationErrors,
252
+ labels: this.labels
253
+ };
254
+ }
255
+ };
256
+
257
+ // src/runtime.ts
258
+ var defaultLogger = {
259
+ debug: (message, metadata) => metadata === void 0 ? console.debug(message) : console.debug(message, metadata),
260
+ info: (message, metadata) => metadata === void 0 ? console.info(message) : console.info(message, metadata),
261
+ warn: (message, metadata) => metadata === void 0 ? console.warn(message) : console.warn(message, metadata),
262
+ error: (message, metadata) => metadata === void 0 ? console.error(message) : console.error(message, metadata)
263
+ };
264
+ var runtime = {};
265
+ var contextStorage = new import_node_async_hooks.AsyncLocalStorage();
266
+ function configureEchoesRuntime(adapter) {
267
+ const previous = runtime;
268
+ runtime = { ...runtime, ...adapter };
269
+ return () => {
270
+ runtime = previous;
271
+ };
272
+ }
273
+ function getEchoesRuntime() {
274
+ return runtime;
275
+ }
276
+ function getEchoesLogger() {
277
+ return runtime.logger || defaultLogger;
278
+ }
279
+ function getEchoesContext() {
280
+ return contextStorage.getStore();
281
+ }
282
+ async function runWithEchoesContext(context, callback) {
283
+ const contextWithId = { contextId: context.contextId || (0, import_node_crypto.randomUUID)(), ...context };
284
+ return await contextStorage.run(contextWithId, async () => {
285
+ if (runtime.runWithContext) {
286
+ return await runtime.runWithContext(contextWithId, callback);
287
+ }
288
+ return await callback();
289
+ });
290
+ }
291
+ function isSimpleSchemaLike(schema) {
292
+ return !!schema && typeof schema === "object" && typeof schema.clean === "function" && typeof schema.validate === "function";
293
+ }
294
+ function cloneValue(value) {
295
+ if (value === null || typeof value !== "object") return value;
296
+ if (value instanceof Date) return new Date(value.getTime());
297
+ if (Buffer.isBuffer(value)) return Buffer.from(value);
298
+ if (Array.isArray(value)) return value.map(cloneValue);
299
+ const prototype = Object.getPrototypeOf(value);
300
+ if (prototype !== Object.prototype && prototype !== null) return value;
301
+ const result = {};
302
+ for (const [key, child] of Object.entries(value)) {
303
+ result[key] = cloneValue(child);
304
+ }
305
+ return result;
306
+ }
307
+ async function cleanSimpleSchema(schema, value) {
308
+ const cloned = cloneValue(value);
309
+ return await schema.clean(cloned, { mutate: false });
310
+ }
311
+ async function cleanEchoesSchema(schema, value) {
312
+ if (isSimpleSchemaLike(schema)) {
313
+ return await cleanSimpleSchema(schema, value);
314
+ }
315
+ if (runtime.schema) {
316
+ return await runtime.schema.clean(schema, value);
317
+ }
318
+ throw new Error(
319
+ "Echoes received a schema it cannot execute. Use a SimpleSchema-compatible object or configure an Echoes schema adapter."
320
+ );
321
+ }
322
+ async function parseEchoesSchema(schema, value) {
323
+ if (isSimpleSchemaLike(schema)) {
324
+ const cleaned = await cleanSimpleSchema(schema, value);
325
+ await schema.validate(cleaned);
326
+ return cleaned;
327
+ }
328
+ if (runtime.schema) {
329
+ return await runtime.schema.parse(schema, value);
330
+ }
331
+ throw new Error(
332
+ "Echoes received a schema it cannot execute. Use a SimpleSchema-compatible object or configure an Echoes schema adapter."
333
+ );
334
+ }
335
+ function createEchoesValidationError(info) {
336
+ return runtime.createValidationError ? runtime.createValidationError(info) : new EchoesValidationError(info.validationErrors || {}, info.labels);
337
+ }
338
+ function createEchoesUserError(info) {
339
+ return runtime.createUserError ? runtime.createUserError(info) : new EchoesUserError(info.error, info.message, info.extra);
340
+ }
341
+
342
+ // src/request/getSignature.ts
343
+ var import_node_crypto2 = require("crypto");
344
+
345
+ // src/request/getPassword.ts
346
+ function getEchoesPassword() {
347
+ var _a, _b;
348
+ const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) || process.env.echoes_password || process.env.ECHOES_PASSWORD;
349
+ if (!secret) {
350
+ getEchoesLogger().warn(
351
+ 'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
352
+ );
353
+ }
354
+ return secret;
355
+ }
356
+
357
+ // src/request/getSignature.ts
358
+ function getSignature_default(_body) {
359
+ const password = getEchoesPassword();
360
+ return (0, import_node_crypto2.createHmac)("sha1", password || "").update("").digest("hex");
361
+ }
362
+
363
+ // src/request/getURL.ts
364
+ function getURL_default(serviceName) {
365
+ var _a, _b, _c;
366
+ if (serviceName.startsWith("http")) return serviceName;
367
+ const url = (_c = (_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.services) == null ? void 0 : _c[serviceName];
368
+ if (!url) {
369
+ throw new Error(`No URL found in echoes config for service ${serviceName}`);
370
+ }
371
+ return url;
372
+ }
373
+
374
+ // src/request/makeRequest.ts
375
+ var import_node_http = __toESM(require("http"), 1);
376
+ var import_node_https = __toESM(require("https"), 1);
377
+ async function executeWithRetries(callback, retries = 0, delayMs = 200) {
378
+ try {
379
+ return await callback();
380
+ } catch (error) {
381
+ if (retries <= 0) throw error;
382
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
383
+ return await executeWithRetries(callback, retries - 1, delayMs);
384
+ }
385
+ }
386
+ async function postJSON(urlValue, data, timeout, redirectsRemaining = 5) {
387
+ const url = new URL(urlValue);
388
+ const body = JSON.stringify(data);
389
+ const transport = url.protocol === "https:" ? import_node_https.default : import_node_http.default;
390
+ return await new Promise((resolve, reject) => {
391
+ const request2 = transport.request(
392
+ url,
393
+ {
394
+ method: "POST",
395
+ headers: {
396
+ "User-Agent": "Orionjs-Echoes/1.1",
397
+ "Content-Type": "application/json",
398
+ "Content-Length": Buffer.byteLength(body)
399
+ }
400
+ },
401
+ (response) => {
402
+ const chunks = [];
403
+ const statusCode = response.statusCode || 0;
404
+ if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
405
+ response.resume();
406
+ if (redirectsRemaining <= 0) {
407
+ reject(new Error("Echoes request exceeded the redirect limit"));
408
+ return;
409
+ }
410
+ resolve(
411
+ postJSON(
412
+ new URL(response.headers.location, url).toString(),
413
+ data,
414
+ timeout,
415
+ redirectsRemaining - 1
416
+ )
417
+ );
418
+ return;
419
+ }
420
+ response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
421
+ response.on("end", () => {
422
+ try {
423
+ const responseBody = Buffer.concat(chunks).toString("utf8");
424
+ if (statusCode < 200 || statusCode >= 300) {
425
+ reject(new Error(`Request failed with status code ${statusCode}`));
426
+ return;
427
+ }
428
+ resolve({
429
+ statusCode,
430
+ data: responseBody ? JSON.parse(responseBody) : {}
431
+ });
432
+ } catch (error) {
433
+ reject(error);
434
+ }
435
+ });
436
+ }
437
+ );
438
+ request2.on("error", reject);
439
+ if (timeout) {
440
+ request2.setTimeout(timeout, () => {
441
+ request2.destroy(new Error(`Echoes request timed out after ${timeout}ms`));
442
+ });
443
+ }
444
+ request2.end(body);
445
+ });
446
+ }
447
+ var makeRequest = async (options) => {
448
+ return await executeWithRetries(
449
+ () => postJSON(options.url, options.data, options.timeout),
450
+ options.retries || 0
451
+ );
452
+ };
453
+
454
+ // src/request/index.ts
455
+ async function request(options) {
456
+ var _a, _b;
457
+ const { method, service, params } = options;
458
+ const serializedParams = serialize_default(params);
459
+ const date = /* @__PURE__ */ new Date();
460
+ const body = { method, service, serializedParams, date };
461
+ const signature = getSignature_default(body);
462
+ try {
463
+ const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
464
+ const requestOptions = {
465
+ url: getURL_default(service),
466
+ retries: options.retries,
467
+ timeout: options.timeout,
468
+ data: {
469
+ body,
470
+ signature
471
+ }
472
+ };
473
+ const result = await requestMaker(requestOptions);
474
+ if (result.statusCode !== 200) {
475
+ throw new Error(`Wrong status code ${result.statusCode}`);
476
+ }
477
+ const data = result.data;
478
+ if (data.error) {
479
+ const info = data.errorInfo;
480
+ if (info) {
481
+ if (data.isValidationError) {
482
+ throw createEchoesValidationError(info);
483
+ }
484
+ if (data.isUserError) {
485
+ throw createEchoesUserError(info);
486
+ }
487
+ }
488
+ throw new Error(`${data.error}`);
489
+ }
490
+ const response = deserialize_default(data.result);
491
+ return response;
492
+ } catch (error) {
493
+ const caught = error;
494
+ if (caught.isOrionError || caught.isEchoesError) throw caught;
495
+ throw new Error(`Echoes request network error calling ${service}/${method}: ${caught.message}`);
496
+ }
497
+ }
498
+
76
499
  // src/startService/KafkaManager.ts
500
+ var import_node_crypto3 = require("crypto");
77
501
  var HEARTBEAT_INTERVAL_SECONDS = 5;
78
502
  var CHECK_JOIN_CONSUMER_INTERVAL_SECONDS = 30;
79
503
  var DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY = 4;
@@ -91,11 +515,21 @@ var KafkaManager = class {
91
515
  producerConnected = false;
92
516
  interval;
93
517
  constructor(options) {
94
- this.kafka = new import_kafkajs.Kafka(options.client);
95
518
  this.options = options;
96
519
  }
97
520
  async start(options) {
98
521
  var _a;
522
+ let kafkaModule;
523
+ try {
524
+ kafkaModule = await import("kafkajs");
525
+ } catch (error) {
526
+ const wrapped = new Error(
527
+ "Echoes Kafka transport requires kafkajs to be installed in the application"
528
+ );
529
+ wrapped.cause = error;
530
+ throw wrapped;
531
+ }
532
+ this.kafka = new kafkaModule.Kafka(this.options.client);
99
533
  this.onEvent = options.onEvent;
100
534
  this.subscriptions = new Map(
101
535
  options.subscriptions.map((subscription) => [subscription.topic, subscription])
@@ -113,7 +547,7 @@ var KafkaManager = class {
113
547
  this.consumer = this.kafka.consumer(this.options.consumer);
114
548
  this.consumerStarted = await this.conditionalStart();
115
549
  if (this.consumerStarted) return;
116
- import_logger.logger.info("Echoes: Delaying consumer group join, waiting for conditions to be met");
550
+ getEchoesLogger().info("Echoes: Delaying consumer group join, waiting for conditions to be met");
117
551
  this.interval = setInterval(async () => {
118
552
  this.consumerStarted = await this.conditionalStart();
119
553
  if (this.consumerStarted) clearInterval(this.interval);
@@ -131,7 +565,7 @@ var KafkaManager = class {
131
565
  {
132
566
  value: serialize_default({ params: options.params }),
133
567
  headers: {
134
- "echoes-event-id": (0, import_node_crypto.randomUUID)()
568
+ "echoes-event-id": (0, import_node_crypto3.randomUUID)()
135
569
  }
136
570
  }
137
571
  ]
@@ -139,7 +573,8 @@ var KafkaManager = class {
139
573
  }
140
574
  async close() {
141
575
  var _a, _b;
142
- import_logger.logger.warn("Echoes: Stopping Kafka transport");
576
+ const logger = getEchoesLogger();
577
+ logger.warn("Echoes: Stopping Kafka transport");
143
578
  if (this.interval) clearInterval(this.interval);
144
579
  await Promise.all([
145
580
  (_a = this.consumer) == null ? void 0 : _a.disconnect(),
@@ -149,6 +584,7 @@ var KafkaManager = class {
149
584
  this.producerConnected = false;
150
585
  }
151
586
  async checkJoinConsumerGroupConditions() {
587
+ const logger = getEchoesLogger();
152
588
  const admin = this.kafka.admin();
153
589
  try {
154
590
  await admin.connect();
@@ -156,7 +592,7 @@ var KafkaManager = class {
156
592
  const groupDescriptions = await admin.describeGroups([groupId]);
157
593
  const group = groupDescriptions.groups[0];
158
594
  if (group.state === "Empty") {
159
- import_logger.logger.info(`Echoes: Consumer group ${groupId} is empty, joining`);
595
+ logger.info(`Echoes: Consumer group ${groupId} is empty, joining`);
160
596
  return true;
161
597
  }
162
598
  const topicsMetadata = await admin.fetchTopicMetadata({ topics: this.topics });
@@ -164,24 +600,24 @@ var KafkaManager = class {
164
600
  (acc, topic) => acc + topic.partitions.length,
165
601
  0
166
602
  );
167
- import_logger.logger.info(
603
+ logger.info(
168
604
  `Echoes: Consumer group ${groupId} has ${group.members.length} members and ${totalPartitions} partitions`
169
605
  );
170
606
  const partitionsRatio = this.options.membersToPartitionsRatio || DEFAULT_MEMBERS_TO_PARTITIONS_RATIO;
171
607
  const partitionsThreshold = Math.ceil(totalPartitions * partitionsRatio);
172
608
  if (partitionsThreshold > group.members.length) {
173
- import_logger.logger.info(
609
+ logger.info(
174
610
  `Echoes: Consumer group ${groupId} has room for more members ${group.members.length}/${partitionsThreshold}, joining`
175
611
  );
176
612
  return true;
177
613
  }
178
614
  return false;
179
615
  } catch (error) {
180
- import_logger.logger.error("Echoes: Error checking consumer group conditions, join", { error });
616
+ logger.error("Echoes: Error checking consumer group conditions, join", { error });
181
617
  return true;
182
618
  } finally {
183
619
  await admin.disconnect().catch((error) => {
184
- import_logger.logger.error("Echoes: Error disconnecting admin client", { error });
620
+ logger.error("Echoes: Error disconnecting admin client", { error });
185
621
  });
186
622
  }
187
623
  }
@@ -201,19 +637,20 @@ var KafkaManager = class {
201
637
  return false;
202
638
  }
203
639
  async handleMessage(params) {
640
+ const logger = getEchoesLogger();
204
641
  const subscription = this.subscriptions.get(params.topic);
205
642
  if (!subscription) {
206
- import_logger.logger.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
643
+ logger.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
207
644
  return;
208
645
  }
209
646
  let intervalsCount = 0;
210
647
  const heartbeatInterval = setInterval(async () => {
211
648
  await params.heartbeat().catch((error) => {
212
- import_logger.logger.warn(`Echoes: Error sending heartbeat: ${error.message}`);
649
+ logger.warn(`Echoes: Error sending heartbeat: ${error.message}`);
213
650
  });
214
651
  intervalsCount++;
215
652
  if (intervalsCount * HEARTBEAT_INTERVAL_SECONDS % 30 === 0) {
216
- import_logger.logger.warn(
653
+ logger.warn(
217
654
  `Echoes: Event is taking too long to process: ${params.topic} ${intervalsCount * HEARTBEAT_INTERVAL_SECONDS}s`
218
655
  );
219
656
  }
@@ -225,7 +662,7 @@ var KafkaManager = class {
225
662
  try {
226
663
  await this.handleRetries(subscription, params, error);
227
664
  } catch (retryError) {
228
- import_logger.logger.error("Echoes: error processing a message", {
665
+ logger.error("Echoes: error processing a message", {
229
666
  error: retryError,
230
667
  topic: params.topic
231
668
  });
@@ -238,6 +675,9 @@ var KafkaManager = class {
238
675
  createReceivedEvent(params) {
239
676
  var _a, _b, _c, _d;
240
677
  const { message, topic, partition } = params;
678
+ if (!message.value) {
679
+ throw new Error(`Echoes received an empty Kafka message for ${topic}`);
680
+ }
241
681
  const data = deserialize_default(message.value.toString());
242
682
  const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
243
683
  const timestamp = Number.parseInt(message.timestamp || "", 10);
@@ -254,6 +694,7 @@ var KafkaManager = class {
254
694
  }
255
695
  async handleRetries(subscription, params, error) {
256
696
  var _a, _b;
697
+ const logger = getEchoesLogger();
257
698
  const { message, topic } = params;
258
699
  const retries = Number.parseInt(((_b = (_a = message == null ? void 0 : message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
259
700
  if (subscription.attemptsBeforeDeadLetter === void 0 || subscription.attemptsBeforeDeadLetter === null) {
@@ -262,6 +703,7 @@ var KafkaManager = class {
262
703
  const maxRetries = subscription.attemptsBeforeDeadLetter || 0;
263
704
  const exceededMaxRetries = retries >= maxRetries;
264
705
  const nextTopic = exceededMaxRetries ? `DLQ-${topic}` : topic;
706
+ if (!message.value) throw error;
265
707
  await this.producer.send({
266
708
  topic: nextTopic,
267
709
  messages: [
@@ -276,19 +718,18 @@ var KafkaManager = class {
276
718
  ]
277
719
  });
278
720
  if (exceededMaxRetries) {
279
- import_logger.logger.error(
721
+ logger.error(
280
722
  "Echoes: a message has reached the maximum number of retries, sending it to DLQ",
281
723
  { topic: nextTopic }
282
724
  );
283
725
  } else {
284
- import_logger.logger.warn("Echoes: a retryable message failed", { error, topic: nextTopic });
726
+ logger.warn("Echoes: a retryable message failed", { error, topic: nextTopic });
285
727
  }
286
728
  }
287
729
  };
288
730
  var KafkaManager_default = KafkaManager;
289
731
 
290
732
  // src/events/EventBus.ts
291
- var import_logger2 = require("@orion-js/logger");
292
733
  var EventBus = class {
293
734
  echoes;
294
735
  transports;
@@ -348,9 +789,10 @@ var EventBus = class {
348
789
  await Promise.all(this.startedTransports.map((transport) => transport.close()));
349
790
  }
350
791
  async handleEvent(event) {
792
+ const logger = getEchoesLogger();
351
793
  const echo2 = this.echoes[event.topic];
352
794
  if (!echo2 || echo2.type !== "event") {
353
- import_logger2.logger.warn(`Echoes: Received a message for an unknown topic: ${event.topic}, ignoring it`);
795
+ logger.warn(`Echoes: Received a message for an unknown topic: ${event.topic}, ignoring it`);
354
796
  return;
355
797
  }
356
798
  if (event.transport === "kafka" && event.context) {
@@ -478,6 +920,14 @@ function createEventBus(options) {
478
920
  });
479
921
  }
480
922
 
923
+ // src/requestsHandler/checkSignature.ts
924
+ function checkSignature_default(body, signature) {
925
+ const generatedSignature = getSignature_default(body);
926
+ if (generatedSignature !== signature) {
927
+ throw new Error("Echoes invalid signature");
928
+ }
929
+ }
930
+
481
931
  // src/requestsHandler/getEcho.ts
482
932
  function getEcho_default(method) {
483
933
  const echo2 = config_default.echoes[method];
@@ -490,72 +940,29 @@ function getEcho_default(method) {
490
940
  return echo2;
491
941
  }
492
942
 
493
- // src/request/getSignature.ts
494
- var import_jssha = __toESM(require("jssha"), 1);
495
-
496
- // src/request/getPassword.ts
497
- var import_logger3 = require("@orion-js/logger");
498
- var import_env = require("@orion-js/env");
499
- function getEchoesPassword() {
500
- var _a, _b;
501
- const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) || (0, import_env.internalGetEnv)("echoes_password", "ECHOES_PASSWORD");
502
- if (!secret) {
503
- import_logger3.logger.warn(
504
- 'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
505
- );
506
- }
507
- return secret;
508
- }
509
-
510
- // src/request/getSignature.ts
511
- function getSignature_default(body) {
512
- const password = getEchoesPassword();
513
- const shaObj = new import_jssha.default("SHA-1", "TEXT");
514
- shaObj.setHMACKey(password, "TEXT");
515
- shaObj.update(body);
516
- return shaObj.getHMAC("HEX");
517
- }
518
-
519
- // src/requestsHandler/checkSignature.ts
520
- function checkSignature_default(body, signature) {
521
- const generatedSignature = getSignature_default(body);
522
- if (generatedSignature !== signature) {
523
- throw new Error("Echoes invalid signature");
524
- }
525
- }
526
-
527
943
  // src/requestsHandler/index.ts
528
- var import_http = require("@orion-js/http");
529
- var requestsHandler_default = (options) => (0, import_http.route)({
944
+ var requestsHandler_default = (options) => ({
530
945
  method: "post",
531
946
  path: options.requests.handlerPath || "/echoes-services",
532
- bodyParser: "json",
533
- bodyParserOptions: {
534
- limit: "10mb"
535
- },
536
- async resolve(req) {
947
+ bodyLimit: "10mb",
948
+ async handle(requestBody) {
537
949
  try {
538
- const { body, signature } = req.body;
950
+ const { body, signature } = requestBody;
539
951
  checkSignature_default(body, signature);
540
952
  const { method, serializedParams } = body;
541
953
  const echo2 = getEcho_default(method);
542
954
  const result = await echo2.onRequest(serializedParams);
543
- return {
544
- body: {
545
- result: serialize_default(result)
546
- }
547
- };
955
+ return { result: serialize_default(result) };
548
956
  } catch (error) {
549
- if (!error.getInfo) {
550
- console.error("Error at echo requests handler:", error);
957
+ const caught = error;
958
+ if (!caught.getInfo) {
959
+ getEchoesLogger().error("Error at echo requests handler:", { error: caught });
551
960
  }
552
961
  return {
553
- body: {
554
- error: error.message,
555
- errorInfo: error.getInfo ? error.getInfo() : null,
556
- isValidationError: !!error.isValidationError,
557
- isUserError: !!error.isUserError
558
- }
962
+ error: caught.message,
963
+ errorInfo: caught.getInfo ? caught.getInfo() : null,
964
+ isValidationError: !!caught.isValidationError,
965
+ isUserError: !!caught.isUserError
559
966
  };
560
967
  }
561
968
  }
@@ -567,7 +974,13 @@ async function startService(options) {
567
974
  config_default.echoes = options.echoes;
568
975
  if (options.requests) {
569
976
  config_default.requests = options.requests;
570
- (0, import_http2.registerRoute)(requestsHandler_default(options));
977
+ const registerHandler = options.requests.registerHandler || getEchoesRuntime().registerRequestHandler;
978
+ if (!registerHandler) {
979
+ throw new Error(
980
+ "Echoes requests require requests.registerHandler in standalone servers. Orion applications can import @orion-js/echoes-orion once during startup."
981
+ );
982
+ }
983
+ await registerHandler(requestsHandler_default(options));
571
984
  }
572
985
  const nextEventBus = createEventBus(options);
573
986
  if (nextEventBus) {
@@ -578,115 +991,22 @@ async function startService(options) {
578
991
  }
579
992
  async function stopService() {
580
993
  if (eventBus) {
581
- console.info("Stoping echoes...");
994
+ const logger = getEchoesLogger();
995
+ logger.info("Stopping Echoes...");
582
996
  await eventBus.close();
583
997
  eventBus = null;
584
998
  config_default.eventBus = void 0;
585
- console.info("Echoes stopped");
586
- }
587
- }
588
-
589
- // src/publish/index.ts
590
- async function publish(options) {
591
- if (!config_default.eventBus) {
592
- throw new Error("You must initialize echoes configuration to use publish");
593
- }
594
- return await config_default.eventBus.publish(options);
595
- }
596
-
597
- // src/request/getURL.ts
598
- function getURL_default(serviceName) {
599
- var _a, _b;
600
- if (serviceName.startsWith("http")) return serviceName;
601
- const url = (_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.services[serviceName];
602
- if (!url) {
603
- throw new Error(`No URL found in echoes config for service ${serviceName}`);
604
- }
605
- return url;
606
- }
607
-
608
- // src/request/makeRequest.ts
609
- var import_axios = __toESM(require("axios"), 1);
610
- var import_helpers2 = require("@orion-js/helpers");
611
- var makeRequest = async (options) => {
612
- const result = await (0, import_helpers2.executeWithRetries)(
613
- async () => {
614
- return await (0, import_axios.default)({
615
- method: "post",
616
- url: options.url,
617
- timeout: options.timeout,
618
- headers: {
619
- "User-Agent": "Orionjs-Echoes/1.1"
620
- },
621
- data: options.data
622
- });
623
- },
624
- options.retries,
625
- 200
626
- );
627
- return {
628
- data: result.data,
629
- statusCode: result.status
630
- };
631
- };
632
-
633
- // src/request/index.ts
634
- var import_schema = require("@orion-js/schema");
635
- var import_helpers3 = require("@orion-js/helpers");
636
- async function request(options) {
637
- var _a, _b;
638
- const { method, service, params } = options;
639
- const serializedParams = serialize_default(params);
640
- const date = /* @__PURE__ */ new Date();
641
- const body = { method, service, serializedParams, date };
642
- const signature = getSignature_default(body);
643
- try {
644
- const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
645
- const requestOptions = {
646
- url: getURL_default(service),
647
- retries: options.retries,
648
- timeout: options.timeout,
649
- data: {
650
- body,
651
- signature
652
- }
653
- };
654
- const result = await requestMaker(requestOptions);
655
- if (result.statusCode !== 200) {
656
- throw new Error(`Wrong status code ${result.statusCode}`);
657
- }
658
- const data = result.data;
659
- if (data.error) {
660
- const info = data.errorInfo;
661
- if (info) {
662
- if (data.isValidationError) {
663
- throw new import_schema.ValidationError(info.validationErrors);
664
- }
665
- if (data.isUserError) {
666
- throw new import_helpers3.UserError(info.error, info.message, info.extra);
667
- }
668
- }
669
- throw new Error(`${data.error}`);
670
- }
671
- const response = deserialize_default(data.result);
672
- return response;
673
- } catch (error) {
674
- if (error.isOrionError) throw error;
675
- throw new Error(`Echoes request network error calling ${service}/${method}: ${error.message}`);
999
+ logger.info("Echoes stopped");
676
1000
  }
677
1001
  }
678
1002
 
679
- // src/service/index.ts
680
- var import_logger4 = require("@orion-js/logger");
681
-
682
1003
  // src/echo/index.ts
683
- var import_schema2 = require("@orion-js/schema");
684
1004
  var echo = function createNewEcho(options) {
685
1005
  const resolve = async (params, context) => {
686
- const cleaned = options.params ? await (0, import_schema2.cleanAndValidate)(options.params, params) : params ?? {};
1006
+ const cleaned = options.params ? await parseEchoesSchema(options.params, params) : params ?? {};
687
1007
  const result = await options.resolve(cleaned, context);
688
1008
  if (options.returns) {
689
- return await (0, import_schema2.clean)(options.returns, result);
1009
+ return await cleanEchoesSchema(options.returns, result);
690
1010
  }
691
1011
  return result;
692
1012
  };
@@ -713,6 +1033,9 @@ var echo = function createNewEcho(options) {
713
1033
  onMessage: async (messageData) => {
714
1034
  var _a, _b, _c, _d;
715
1035
  const { message } = messageData;
1036
+ if (!message.value) {
1037
+ throw new Error(`Echoes received an empty Kafka message for ${messageData.topic}`);
1038
+ }
716
1039
  const data = deserialize_default(message.value.toString());
717
1040
  const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
718
1041
  const timestamp = Number(message.timestamp);
@@ -743,15 +1066,21 @@ function createEchoEvent(options) {
743
1066
  return echo({ ...options, type: "event" });
744
1067
  }
745
1068
 
1069
+ // src/schema.ts
1070
+ function typedEchoesSchema(schema) {
1071
+ return schema;
1072
+ }
1073
+
746
1074
  // src/service/index.ts
747
- var import_services = require("@orion-js/services");
748
1075
  var serviceMetadata = /* @__PURE__ */ new WeakMap();
749
1076
  var echoesMetadata = /* @__PURE__ */ new WeakMap();
750
1077
  var echoEntriesByClass = /* @__PURE__ */ new Map();
1078
+ var standaloneInstances = /* @__PURE__ */ new WeakMap();
751
1079
  var pendingEchoEntries = {};
752
1080
  function Echoes() {
753
1081
  return (target, context) => {
754
- (0, import_services.Service)()(target, context);
1082
+ var _a, _b;
1083
+ (_b = (_a = getEchoesRuntime()).decorateService) == null ? void 0 : _b.call(_a, target, context);
755
1084
  serviceMetadata.set(target, { _serviceType: "echoes" });
756
1085
  if (Object.keys(pendingEchoEntries).length > 0) {
757
1086
  echoEntriesByClass.set(target, pendingEchoEntries);
@@ -768,7 +1097,7 @@ function EchoEvent(options = {}) {
768
1097
  return createEchoEvent({
769
1098
  ...options,
770
1099
  resolve: async (params, contextData) => {
771
- return await (0, import_logger4.runWithOrionAsyncContext)(
1100
+ return await runWithEchoesContext(
772
1101
  {
773
1102
  controllerType: "echo",
774
1103
  echoName: propertyKey,
@@ -797,7 +1126,7 @@ function EchoRequest(options = {}) {
797
1126
  return createEchoRequest({
798
1127
  ...options,
799
1128
  resolve: async (params, contextData) => {
800
- return await (0, import_logger4.runWithOrionAsyncContext)(
1129
+ return await runWithEchoesContext(
801
1130
  {
802
1131
  controllerType: "echo",
803
1132
  echoName: propertyKey,
@@ -827,7 +1156,18 @@ function initializeEchoesIfNeeded(instance) {
827
1156
  echoesMetadata.set(instance, echoes);
828
1157
  }
829
1158
  function getServiceEchoes(target) {
830
- const instance = (0, import_services.getInstance)(target);
1159
+ let instance;
1160
+ if (typeof target !== "function") {
1161
+ instance = target;
1162
+ } else if (getEchoesRuntime().getInstance) {
1163
+ instance = getEchoesRuntime().getInstance(target);
1164
+ } else {
1165
+ instance = standaloneInstances.get(target);
1166
+ if (!instance) {
1167
+ instance = new target();
1168
+ standaloneInstances.set(target, instance);
1169
+ }
1170
+ }
831
1171
  if (!serviceMetadata.has(instance.constructor)) {
832
1172
  throw new Error("You must pass a class decorated with @Echoes to getServiceEchoes");
833
1173
  }
@@ -844,13 +1184,25 @@ function getServiceEchoes(target) {
844
1184
  EchoEvent,
845
1185
  EchoRequest,
846
1186
  Echoes,
1187
+ EchoesUserError,
1188
+ EchoesValidationError,
1189
+ cleanEchoesSchema,
1190
+ configureEchoesRuntime,
847
1191
  createEchoEvent,
848
1192
  createEchoRequest,
1193
+ createEchoesUserError,
1194
+ createEchoesValidationError,
849
1195
  echo,
1196
+ getEchoesContext,
1197
+ getEchoesLogger,
1198
+ getEchoesRuntime,
850
1199
  getServiceEchoes,
1200
+ parseEchoesSchema,
851
1201
  publish,
852
1202
  request,
1203
+ runWithEchoesContext,
853
1204
  startService,
854
- stopService
1205
+ stopService,
1206
+ typedEchoesSchema
855
1207
  });
856
1208
  //# sourceMappingURL=index.cjs.map