@orion-js/echoes 4.4.0 → 4.4.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/echo/index.d.ts +1 -1
- 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 +443 -166
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +440 -163
- package/dist/index.js.map +1 -1
- package/dist/publish/index.d.ts +1 -1
- package/dist/startService/KafkaManager.d.ts +25 -19
- package/dist/types.d.ts +85 -1
- package/package.json +11 -2
package/dist/index.cjs
CHANGED
|
@@ -43,20 +43,25 @@ __export(index_exports, {
|
|
|
43
43
|
});
|
|
44
44
|
module.exports = __toCommonJS(index_exports);
|
|
45
45
|
|
|
46
|
+
// src/startService/index.ts
|
|
47
|
+
var import_http2 = require("@orion-js/http");
|
|
48
|
+
|
|
46
49
|
// src/config.ts
|
|
47
50
|
var config = {};
|
|
48
51
|
var config_default = config;
|
|
49
52
|
|
|
50
|
-
// src/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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");
|
|
57
|
+
|
|
58
|
+
// src/echo/deserialize.ts
|
|
59
|
+
function deserialize_default(serializedJavascript) {
|
|
60
|
+
try {
|
|
61
|
+
return eval("(" + serializedJavascript + ")");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error("Error deserializing echo message");
|
|
58
64
|
}
|
|
59
|
-
return echo2;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
// src/publish/serialize.ts
|
|
@@ -68,107 +73,90 @@ function serialize_default(data) {
|
|
|
68
73
|
return serialized;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
// src/request/getSignature.ts
|
|
72
|
-
var import_jssha = __toESM(require("jssha"), 1);
|
|
73
|
-
|
|
74
|
-
// src/request/getPassword.ts
|
|
75
|
-
var import_logger = require("@orion-js/logger");
|
|
76
|
-
var import_env = require("@orion-js/env");
|
|
77
|
-
function getEchoesPassword() {
|
|
78
|
-
var _a, _b;
|
|
79
|
-
const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) || (0, import_env.internalGetEnv)("echoes_password", "ECHOES_PASSWORD");
|
|
80
|
-
if (!secret) {
|
|
81
|
-
import_logger.logger.warn(
|
|
82
|
-
'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
return secret;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// src/request/getSignature.ts
|
|
89
|
-
function getSignature_default(body) {
|
|
90
|
-
const password = getEchoesPassword();
|
|
91
|
-
const shaObj = new import_jssha.default("SHA-1", "TEXT");
|
|
92
|
-
shaObj.setHMACKey(password, "TEXT");
|
|
93
|
-
shaObj.update(body);
|
|
94
|
-
return shaObj.getHMAC("HEX");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// src/requestsHandler/checkSignature.ts
|
|
98
|
-
function checkSignature_default(body, signature) {
|
|
99
|
-
const generatedSignature = getSignature_default(body);
|
|
100
|
-
if (generatedSignature !== signature) {
|
|
101
|
-
throw new Error("Echoes invalid signature");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// src/requestsHandler/index.ts
|
|
106
|
-
var import_http = require("@orion-js/http");
|
|
107
|
-
var requestsHandler_default = (options) => (0, import_http.route)({
|
|
108
|
-
method: "post",
|
|
109
|
-
path: options.requests.handlerPath || "/echoes-services",
|
|
110
|
-
bodyParser: "json",
|
|
111
|
-
bodyParserOptions: {
|
|
112
|
-
limit: "10mb"
|
|
113
|
-
},
|
|
114
|
-
async resolve(req) {
|
|
115
|
-
try {
|
|
116
|
-
const { body, signature } = req.body;
|
|
117
|
-
checkSignature_default(body, signature);
|
|
118
|
-
const { method, serializedParams } = body;
|
|
119
|
-
const echo2 = getEcho_default(method);
|
|
120
|
-
const result = await echo2.onRequest(serializedParams);
|
|
121
|
-
return {
|
|
122
|
-
body: {
|
|
123
|
-
result: serialize_default(result)
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
} catch (error) {
|
|
127
|
-
if (!error.getInfo) {
|
|
128
|
-
console.error("Error at echo requests handler:", error);
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
body: {
|
|
132
|
-
error: error.message,
|
|
133
|
-
errorInfo: error.getInfo ? error.getInfo() : null,
|
|
134
|
-
isValidationError: !!error.isValidationError,
|
|
135
|
-
isUserError: !!error.isUserError
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
76
|
// src/startService/KafkaManager.ts
|
|
143
|
-
var import_kafkajs = require("kafkajs");
|
|
144
|
-
var import_logger2 = require("@orion-js/logger");
|
|
145
77
|
var HEARTBEAT_INTERVAL_SECONDS = 5;
|
|
146
78
|
var CHECK_JOIN_CONSUMER_INTERVAL_SECONDS = 30;
|
|
147
79
|
var DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY = 4;
|
|
148
80
|
var DEFAULT_MEMBERS_TO_PARTITIONS_RATIO = 1;
|
|
149
81
|
var KafkaManager = class {
|
|
82
|
+
name = "kafka";
|
|
150
83
|
kafka;
|
|
151
84
|
options;
|
|
152
85
|
producer;
|
|
153
86
|
consumer;
|
|
154
|
-
topics;
|
|
155
|
-
|
|
87
|
+
topics = [];
|
|
88
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
89
|
+
onEvent;
|
|
90
|
+
consumerStarted = false;
|
|
91
|
+
producerConnected = false;
|
|
156
92
|
interval;
|
|
157
93
|
constructor(options) {
|
|
158
94
|
this.kafka = new import_kafkajs.Kafka(options.client);
|
|
159
95
|
this.options = options;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
96
|
+
}
|
|
97
|
+
async start(options) {
|
|
98
|
+
var _a;
|
|
99
|
+
this.onEvent = options.onEvent;
|
|
100
|
+
this.subscriptions = new Map(
|
|
101
|
+
options.subscriptions.map((subscription) => [subscription.topic, subscription])
|
|
102
|
+
);
|
|
103
|
+
this.topics = options.subscriptions.map((subscription) => subscription.topic);
|
|
104
|
+
if (options.publish || options.consume) {
|
|
105
|
+
this.producer = this.kafka.producer(this.options.producer);
|
|
106
|
+
await this.producer.connect();
|
|
107
|
+
this.producerConnected = true;
|
|
108
|
+
}
|
|
109
|
+
if (!options.consume || this.topics.length === 0) return;
|
|
110
|
+
if (!((_a = this.options.consumer) == null ? void 0 : _a.groupId)) {
|
|
111
|
+
throw new Error("Echoes Kafka consumers require consumer.groupId");
|
|
112
|
+
}
|
|
113
|
+
this.consumer = this.kafka.consumer(this.options.consumer);
|
|
114
|
+
this.consumerStarted = await this.conditionalStart();
|
|
115
|
+
if (this.consumerStarted) return;
|
|
116
|
+
import_logger.logger.info("Echoes: Delaying consumer group join, waiting for conditions to be met");
|
|
117
|
+
this.interval = setInterval(async () => {
|
|
118
|
+
this.consumerStarted = await this.conditionalStart();
|
|
119
|
+
if (this.consumerStarted) clearInterval(this.interval);
|
|
120
|
+
}, CHECK_JOIN_CONSUMER_INTERVAL_SECONDS * 1e3);
|
|
121
|
+
}
|
|
122
|
+
async publish(options) {
|
|
123
|
+
if (!this.producer || !this.producerConnected) {
|
|
124
|
+
throw new Error("Echoes Kafka producer is not connected");
|
|
125
|
+
}
|
|
126
|
+
return await this.producer.send({
|
|
127
|
+
acks: options.acks,
|
|
128
|
+
timeout: options.timeout,
|
|
129
|
+
topic: options.topic,
|
|
130
|
+
messages: [
|
|
131
|
+
{
|
|
132
|
+
value: serialize_default({ params: options.params }),
|
|
133
|
+
headers: {
|
|
134
|
+
"echoes-event-id": (0, import_node_crypto.randomUUID)()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async close() {
|
|
141
|
+
var _a, _b;
|
|
142
|
+
import_logger.logger.warn("Echoes: Stopping Kafka transport");
|
|
143
|
+
if (this.interval) clearInterval(this.interval);
|
|
144
|
+
await Promise.all([
|
|
145
|
+
(_a = this.consumer) == null ? void 0 : _a.disconnect(),
|
|
146
|
+
this.producerConnected ? (_b = this.producer) == null ? void 0 : _b.disconnect() : void 0
|
|
147
|
+
]);
|
|
148
|
+
this.consumerStarted = false;
|
|
149
|
+
this.producerConnected = false;
|
|
163
150
|
}
|
|
164
151
|
async checkJoinConsumerGroupConditions() {
|
|
165
152
|
const admin = this.kafka.admin();
|
|
166
153
|
try {
|
|
167
154
|
await admin.connect();
|
|
168
|
-
const
|
|
155
|
+
const groupId = this.options.consumer.groupId;
|
|
156
|
+
const groupDescriptions = await admin.describeGroups([groupId]);
|
|
169
157
|
const group = groupDescriptions.groups[0];
|
|
170
158
|
if (group.state === "Empty") {
|
|
171
|
-
|
|
159
|
+
import_logger.logger.info(`Echoes: Consumer group ${groupId} is empty, joining`);
|
|
172
160
|
return true;
|
|
173
161
|
}
|
|
174
162
|
const topicsMetadata = await admin.fetchTopicMetadata({ topics: this.topics });
|
|
@@ -176,23 +164,24 @@ var KafkaManager = class {
|
|
|
176
164
|
(acc, topic) => acc + topic.partitions.length,
|
|
177
165
|
0
|
|
178
166
|
);
|
|
179
|
-
|
|
180
|
-
`Echoes: Consumer group ${
|
|
167
|
+
import_logger.logger.info(
|
|
168
|
+
`Echoes: Consumer group ${groupId} has ${group.members.length} members and ${totalPartitions} partitions`
|
|
181
169
|
);
|
|
182
170
|
const partitionsRatio = this.options.membersToPartitionsRatio || DEFAULT_MEMBERS_TO_PARTITIONS_RATIO;
|
|
183
171
|
const partitionsThreshold = Math.ceil(totalPartitions * partitionsRatio);
|
|
184
172
|
if (partitionsThreshold > group.members.length) {
|
|
185
|
-
|
|
186
|
-
`Echoes: Consumer group ${
|
|
173
|
+
import_logger.logger.info(
|
|
174
|
+
`Echoes: Consumer group ${groupId} has room for more members ${group.members.length}/${partitionsThreshold}, joining`
|
|
187
175
|
);
|
|
188
176
|
return true;
|
|
189
177
|
}
|
|
178
|
+
return false;
|
|
190
179
|
} catch (error) {
|
|
191
|
-
|
|
180
|
+
import_logger.logger.error("Echoes: Error checking consumer group conditions, join", { error });
|
|
192
181
|
return true;
|
|
193
182
|
} finally {
|
|
194
183
|
await admin.disconnect().catch((error) => {
|
|
195
|
-
|
|
184
|
+
import_logger.logger.error("Echoes: Error disconnecting admin client", { error });
|
|
196
185
|
});
|
|
197
186
|
}
|
|
198
187
|
}
|
|
@@ -209,59 +198,68 @@ var KafkaManager = class {
|
|
|
209
198
|
await this.joinConsumerGroup();
|
|
210
199
|
return true;
|
|
211
200
|
}
|
|
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();
|
|
201
|
+
return false;
|
|
229
202
|
}
|
|
230
203
|
async handleMessage(params) {
|
|
231
|
-
const
|
|
232
|
-
if (!
|
|
233
|
-
|
|
204
|
+
const subscription = this.subscriptions.get(params.topic);
|
|
205
|
+
if (!subscription) {
|
|
206
|
+
import_logger.logger.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
|
|
234
207
|
return;
|
|
235
208
|
}
|
|
236
209
|
let intervalsCount = 0;
|
|
237
210
|
const heartbeatInterval = setInterval(async () => {
|
|
238
211
|
await params.heartbeat().catch((error) => {
|
|
239
|
-
|
|
212
|
+
import_logger.logger.warn(`Echoes: Error sending heartbeat: ${error.message}`);
|
|
240
213
|
});
|
|
241
214
|
intervalsCount++;
|
|
242
215
|
if (intervalsCount * HEARTBEAT_INTERVAL_SECONDS % 30 === 0) {
|
|
243
|
-
|
|
216
|
+
import_logger.logger.warn(
|
|
244
217
|
`Echoes: Event is taking too long to process: ${params.topic} ${intervalsCount * HEARTBEAT_INTERVAL_SECONDS}s`
|
|
245
218
|
);
|
|
246
219
|
}
|
|
247
220
|
}, HEARTBEAT_INTERVAL_SECONDS * 1e3);
|
|
248
221
|
try {
|
|
249
|
-
|
|
222
|
+
const event = this.createReceivedEvent(params);
|
|
223
|
+
await this.onEvent(event);
|
|
250
224
|
} catch (error) {
|
|
251
|
-
|
|
252
|
-
|
|
225
|
+
try {
|
|
226
|
+
await this.handleRetries(subscription, params, error);
|
|
227
|
+
} catch (retryError) {
|
|
228
|
+
import_logger.logger.error("Echoes: error processing a message", {
|
|
229
|
+
error: retryError,
|
|
230
|
+
topic: params.topic
|
|
231
|
+
});
|
|
232
|
+
throw retryError;
|
|
233
|
+
}
|
|
253
234
|
} finally {
|
|
254
235
|
clearInterval(heartbeatInterval);
|
|
255
236
|
}
|
|
256
237
|
}
|
|
257
|
-
|
|
238
|
+
createReceivedEvent(params) {
|
|
239
|
+
var _a, _b, _c, _d;
|
|
240
|
+
const { message, topic, partition } = params;
|
|
241
|
+
const data = deserialize_default(message.value.toString());
|
|
242
|
+
const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
243
|
+
const timestamp = Number.parseInt(message.timestamp || "", 10);
|
|
244
|
+
return {
|
|
245
|
+
id: ((_d = (_c = message.headers) == null ? void 0 : _c["echoes-event-id"]) == null ? void 0 : _d.toString()) || `kafka:${topic}:${partition}:${message.offset}`,
|
|
246
|
+
topic,
|
|
247
|
+
data,
|
|
248
|
+
transport: "kafka",
|
|
249
|
+
headers: message.headers,
|
|
250
|
+
createdAt: Number.isFinite(timestamp) ? new Date(timestamp) : /* @__PURE__ */ new Date(),
|
|
251
|
+
attempt: retries + 1,
|
|
252
|
+
context: params
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async handleRetries(subscription, params, error) {
|
|
258
256
|
var _a, _b;
|
|
259
257
|
const { message, topic } = params;
|
|
260
258
|
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 (
|
|
259
|
+
if (subscription.attemptsBeforeDeadLetter === void 0 || subscription.attemptsBeforeDeadLetter === null) {
|
|
262
260
|
throw error;
|
|
263
261
|
}
|
|
264
|
-
const maxRetries =
|
|
262
|
+
const maxRetries = subscription.attemptsBeforeDeadLetter || 0;
|
|
265
263
|
const exceededMaxRetries = retries >= maxRetries;
|
|
266
264
|
const nextTopic = exceededMaxRetries ? `DLQ-${topic}` : topic;
|
|
267
265
|
await this.producer.send({
|
|
@@ -270,6 +268,7 @@ var KafkaManager = class {
|
|
|
270
268
|
{
|
|
271
269
|
value: message.value.toString(),
|
|
272
270
|
headers: {
|
|
271
|
+
...message.headers,
|
|
273
272
|
retries: String(retries + 1),
|
|
274
273
|
error: error.message
|
|
275
274
|
}
|
|
@@ -277,59 +276,322 @@ var KafkaManager = class {
|
|
|
277
276
|
]
|
|
278
277
|
});
|
|
279
278
|
if (exceededMaxRetries) {
|
|
280
|
-
|
|
279
|
+
import_logger.logger.error(
|
|
281
280
|
"Echoes: a message has reached the maximum number of retries, sending it to DLQ",
|
|
282
281
|
{ topic: nextTopic }
|
|
283
282
|
);
|
|
284
283
|
} else {
|
|
285
|
-
|
|
284
|
+
import_logger.logger.warn("Echoes: a retryable message failed", { error, topic: nextTopic });
|
|
286
285
|
}
|
|
287
286
|
}
|
|
288
287
|
};
|
|
289
288
|
var KafkaManager_default = KafkaManager;
|
|
290
289
|
|
|
290
|
+
// src/events/EventBus.ts
|
|
291
|
+
var import_logger2 = require("@orion-js/logger");
|
|
292
|
+
var EventBus = class {
|
|
293
|
+
echoes;
|
|
294
|
+
transports;
|
|
295
|
+
consumeFrom;
|
|
296
|
+
publishTo;
|
|
297
|
+
startedTransports = [];
|
|
298
|
+
started = false;
|
|
299
|
+
closed = false;
|
|
300
|
+
constructor(options) {
|
|
301
|
+
this.echoes = options.echoes;
|
|
302
|
+
this.transports = options.transports;
|
|
303
|
+
this.consumeFrom = [...new Set(options.consumeFrom)];
|
|
304
|
+
this.publishTo = options.publishTo;
|
|
305
|
+
}
|
|
306
|
+
async start() {
|
|
307
|
+
if (this.started) return;
|
|
308
|
+
const selected = new Set(this.consumeFrom);
|
|
309
|
+
if (this.publishTo) selected.add(this.publishTo);
|
|
310
|
+
for (const name of selected) {
|
|
311
|
+
if (!this.transports[name]) {
|
|
312
|
+
throw new Error(`Echoes event transport "${name}" is selected but not configured`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const subscriptions = Object.entries(this.echoes).filter(([, echo2]) => echo2.type === "event").map(([topic, echo2]) => ({
|
|
316
|
+
topic,
|
|
317
|
+
attemptsBeforeDeadLetter: echo2.attemptsBeforeDeadLetter
|
|
318
|
+
}));
|
|
319
|
+
try {
|
|
320
|
+
for (const name of selected) {
|
|
321
|
+
const transport = this.transports[name];
|
|
322
|
+
this.startedTransports.push(transport);
|
|
323
|
+
await transport.start({
|
|
324
|
+
consume: this.consumeFrom.includes(name),
|
|
325
|
+
publish: this.publishTo === name,
|
|
326
|
+
subscriptions,
|
|
327
|
+
onEvent: (event) => this.handleEvent(event)
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
this.started = true;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
await this.close().catch(() => void 0);
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async publish(options) {
|
|
337
|
+
if (!this.started) {
|
|
338
|
+
throw new Error("You must initialize echoes configuration to use publish");
|
|
339
|
+
}
|
|
340
|
+
if (!this.publishTo) {
|
|
341
|
+
throw new Error("Echoes does not have a publish transport configured");
|
|
342
|
+
}
|
|
343
|
+
return await this.transports[this.publishTo].publish(options);
|
|
344
|
+
}
|
|
345
|
+
async close() {
|
|
346
|
+
if (this.closed) return;
|
|
347
|
+
this.closed = true;
|
|
348
|
+
await Promise.all(this.startedTransports.map((transport) => transport.close()));
|
|
349
|
+
}
|
|
350
|
+
async handleEvent(event) {
|
|
351
|
+
const echo2 = this.echoes[event.topic];
|
|
352
|
+
if (!echo2 || echo2.type !== "event") {
|
|
353
|
+
import_logger2.logger.warn(`Echoes: Received a message for an unknown topic: ${event.topic}, ignoring it`);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (event.transport === "kafka" && event.context) {
|
|
357
|
+
await echo2.onMessage(event.context);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (echo2.onEvent) {
|
|
361
|
+
await echo2.onEvent(event);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await echo2.resolve(event.data.params, {
|
|
365
|
+
...event.context || {},
|
|
366
|
+
transport: event.transport,
|
|
367
|
+
eventId: event.id,
|
|
368
|
+
topic: event.topic,
|
|
369
|
+
headers: event.headers,
|
|
370
|
+
createdAt: event.createdAt,
|
|
371
|
+
attempt: event.attempt,
|
|
372
|
+
data: event.data
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// src/events/PulseManager.ts
|
|
378
|
+
var PulseManager = class {
|
|
379
|
+
name = "pulse";
|
|
380
|
+
options;
|
|
381
|
+
pulse;
|
|
382
|
+
subscriptions = [];
|
|
383
|
+
constructor(options) {
|
|
384
|
+
this.options = options;
|
|
385
|
+
}
|
|
386
|
+
async start(options) {
|
|
387
|
+
let pulseModule;
|
|
388
|
+
try {
|
|
389
|
+
pulseModule = await import("@orion-js/pulse");
|
|
390
|
+
} catch (error) {
|
|
391
|
+
const wrapped = new Error(
|
|
392
|
+
"Echoes Pulse transport requires @orion-js/pulse to be installed in the application"
|
|
393
|
+
);
|
|
394
|
+
wrapped.cause = error;
|
|
395
|
+
throw wrapped;
|
|
396
|
+
}
|
|
397
|
+
const { subscription, ...connectOptions } = this.options;
|
|
398
|
+
this.pulse = pulseModule.connect(connectOptions);
|
|
399
|
+
await this.pulse.awaitConnection();
|
|
400
|
+
if (!options.consume) return;
|
|
401
|
+
this.subscriptions = await Promise.all(
|
|
402
|
+
options.subscriptions.map(
|
|
403
|
+
(definition) => this.pulse.subscribe(
|
|
404
|
+
definition.topic,
|
|
405
|
+
(event) => options.onEvent(this.createReceivedEvent(event)),
|
|
406
|
+
this.getSubscribeOptions(subscription, definition.attemptsBeforeDeadLetter)
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
async publish(options) {
|
|
412
|
+
if (!this.pulse) {
|
|
413
|
+
throw new Error("Echoes Pulse client is not connected");
|
|
414
|
+
}
|
|
415
|
+
return await this.pulse.publish({
|
|
416
|
+
topic: options.topic,
|
|
417
|
+
data: { params: options.params }
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
async close() {
|
|
421
|
+
var _a;
|
|
422
|
+
await ((_a = this.pulse) == null ? void 0 : _a.close());
|
|
423
|
+
this.subscriptions = [];
|
|
424
|
+
}
|
|
425
|
+
getSubscribeOptions(defaults = {}, attemptsBeforeDeadLetter) {
|
|
426
|
+
return {
|
|
427
|
+
...defaults,
|
|
428
|
+
...attemptsBeforeDeadLetter === void 0 ? {} : { maxRetries: attemptsBeforeDeadLetter }
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
createReceivedEvent(event) {
|
|
432
|
+
return {
|
|
433
|
+
id: event.id,
|
|
434
|
+
topic: event.topic,
|
|
435
|
+
data: event.data,
|
|
436
|
+
transport: "pulse",
|
|
437
|
+
headers: event.headers,
|
|
438
|
+
createdAt: event.createdAt,
|
|
439
|
+
attempt: event.attempt,
|
|
440
|
+
context: event
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/events/createEventBus.ts
|
|
446
|
+
function resolveEventsConfig(options) {
|
|
447
|
+
var _a, _b, _c, _d;
|
|
448
|
+
const legacyKafka = options.client ? {
|
|
449
|
+
client: options.client,
|
|
450
|
+
producer: options.producer,
|
|
451
|
+
consumer: options.consumer,
|
|
452
|
+
readTopicsFromBeginning: options.readTopicsFromBeginning,
|
|
453
|
+
partitionsConsumedConcurrently: options.partitionsConsumedConcurrently,
|
|
454
|
+
membersToPartitionsRatio: options.membersToPartitionsRatio
|
|
455
|
+
} : void 0;
|
|
456
|
+
const kafka = ((_a = options.events) == null ? void 0 : _a.kafka) || legacyKafka;
|
|
457
|
+
const pulse = (_b = options.events) == null ? void 0 : _b.pulse;
|
|
458
|
+
if (!kafka && !pulse && !options.events) return void 0;
|
|
459
|
+
const defaultTransport = kafka ? "kafka" : pulse ? "pulse" : void 0;
|
|
460
|
+
return {
|
|
461
|
+
kafka,
|
|
462
|
+
pulse,
|
|
463
|
+
consumeFrom: ((_c = options.events) == null ? void 0 : _c.consumeFrom) || (defaultTransport ? [defaultTransport] : []),
|
|
464
|
+
publishTo: ((_d = options.events) == null ? void 0 : _d.publishTo) || defaultTransport
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function createEventBus(options) {
|
|
468
|
+
const resolved = resolveEventsConfig(options);
|
|
469
|
+
if (!resolved) return void 0;
|
|
470
|
+
const transports = {};
|
|
471
|
+
if (resolved.kafka) transports.kafka = new KafkaManager_default(resolved.kafka);
|
|
472
|
+
if (resolved.pulse) transports.pulse = new PulseManager(resolved.pulse);
|
|
473
|
+
return new EventBus({
|
|
474
|
+
echoes: options.echoes,
|
|
475
|
+
transports,
|
|
476
|
+
consumeFrom: resolved.consumeFrom,
|
|
477
|
+
publishTo: resolved.publishTo
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/requestsHandler/getEcho.ts
|
|
482
|
+
function getEcho_default(method) {
|
|
483
|
+
const echo2 = config_default.echoes[method];
|
|
484
|
+
if (!echo2) {
|
|
485
|
+
throw new Error(`Echo named ${method} not found in this service`);
|
|
486
|
+
}
|
|
487
|
+
if (echo2.type !== "request") {
|
|
488
|
+
throw new Error(`Echo named ${method} is not of type request`);
|
|
489
|
+
}
|
|
490
|
+
return echo2;
|
|
491
|
+
}
|
|
492
|
+
|
|
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
|
+
// src/requestsHandler/index.ts
|
|
528
|
+
var import_http = require("@orion-js/http");
|
|
529
|
+
var requestsHandler_default = (options) => (0, import_http.route)({
|
|
530
|
+
method: "post",
|
|
531
|
+
path: options.requests.handlerPath || "/echoes-services",
|
|
532
|
+
bodyParser: "json",
|
|
533
|
+
bodyParserOptions: {
|
|
534
|
+
limit: "10mb"
|
|
535
|
+
},
|
|
536
|
+
async resolve(req) {
|
|
537
|
+
try {
|
|
538
|
+
const { body, signature } = req.body;
|
|
539
|
+
checkSignature_default(body, signature);
|
|
540
|
+
const { method, serializedParams } = body;
|
|
541
|
+
const echo2 = getEcho_default(method);
|
|
542
|
+
const result = await echo2.onRequest(serializedParams);
|
|
543
|
+
return {
|
|
544
|
+
body: {
|
|
545
|
+
result: serialize_default(result)
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (!error.getInfo) {
|
|
550
|
+
console.error("Error at echo requests handler:", error);
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
body: {
|
|
554
|
+
error: error.message,
|
|
555
|
+
errorInfo: error.getInfo ? error.getInfo() : null,
|
|
556
|
+
isValidationError: !!error.isValidationError,
|
|
557
|
+
isUserError: !!error.isUserError
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
291
564
|
// src/startService/index.ts
|
|
292
|
-
var
|
|
293
|
-
var kafkaManager = null;
|
|
565
|
+
var eventBus = null;
|
|
294
566
|
async function startService(options) {
|
|
295
567
|
config_default.echoes = options.echoes;
|
|
296
568
|
if (options.requests) {
|
|
297
569
|
config_default.requests = options.requests;
|
|
298
570
|
(0, import_http2.registerRoute)(requestsHandler_default(options));
|
|
299
571
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
await
|
|
303
|
-
|
|
304
|
-
config_default.
|
|
572
|
+
const nextEventBus = createEventBus(options);
|
|
573
|
+
if (nextEventBus) {
|
|
574
|
+
await nextEventBus.start();
|
|
575
|
+
eventBus = nextEventBus;
|
|
576
|
+
config_default.eventBus = eventBus;
|
|
305
577
|
}
|
|
306
578
|
}
|
|
307
579
|
async function stopService() {
|
|
308
|
-
if (
|
|
580
|
+
if (eventBus) {
|
|
309
581
|
console.info("Stoping echoes...");
|
|
310
|
-
await
|
|
582
|
+
await eventBus.close();
|
|
583
|
+
eventBus = null;
|
|
584
|
+
config_default.eventBus = void 0;
|
|
311
585
|
console.info("Echoes stopped");
|
|
312
586
|
}
|
|
313
587
|
}
|
|
314
588
|
|
|
315
589
|
// src/publish/index.ts
|
|
316
590
|
async function publish(options) {
|
|
317
|
-
if (!config_default.
|
|
318
|
-
throw new Error("You must initialize echoes
|
|
591
|
+
if (!config_default.eventBus) {
|
|
592
|
+
throw new Error("You must initialize echoes configuration to use publish");
|
|
319
593
|
}
|
|
320
|
-
|
|
321
|
-
params: options.params
|
|
322
|
-
};
|
|
323
|
-
return await config_default.producer.send({
|
|
324
|
-
acks: options.acks,
|
|
325
|
-
timeout: options.timeout,
|
|
326
|
-
topic: options.topic,
|
|
327
|
-
messages: [
|
|
328
|
-
{
|
|
329
|
-
value: serialize_default(payload)
|
|
330
|
-
}
|
|
331
|
-
]
|
|
332
|
-
});
|
|
594
|
+
return await config_default.eventBus.publish(options);
|
|
333
595
|
}
|
|
334
596
|
|
|
335
597
|
// src/request/getURL.ts
|
|
@@ -343,15 +605,6 @@ function getURL_default(serviceName) {
|
|
|
343
605
|
return url;
|
|
344
606
|
}
|
|
345
607
|
|
|
346
|
-
// src/echo/deserialize.ts
|
|
347
|
-
function deserialize_default(serializedJavascript) {
|
|
348
|
-
try {
|
|
349
|
-
return eval("(" + serializedJavascript + ")");
|
|
350
|
-
} catch (error) {
|
|
351
|
-
throw new Error("Error deserializing echo message");
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
608
|
// src/request/makeRequest.ts
|
|
356
609
|
var import_axios = __toESM(require("axios"), 1);
|
|
357
610
|
var import_helpers2 = require("@orion-js/helpers");
|
|
@@ -424,7 +677,7 @@ async function request(options) {
|
|
|
424
677
|
}
|
|
425
678
|
|
|
426
679
|
// src/service/index.ts
|
|
427
|
-
var
|
|
680
|
+
var import_logger4 = require("@orion-js/logger");
|
|
428
681
|
|
|
429
682
|
// src/echo/index.ts
|
|
430
683
|
var import_schema2 = require("@orion-js/schema");
|
|
@@ -437,20 +690,44 @@ var echo = function createNewEcho(options) {
|
|
|
437
690
|
}
|
|
438
691
|
return result;
|
|
439
692
|
};
|
|
693
|
+
const onEvent = async (event) => {
|
|
694
|
+
const context = {
|
|
695
|
+
...event.context || {},
|
|
696
|
+
transport: event.transport,
|
|
697
|
+
eventId: event.id,
|
|
698
|
+
topic: event.topic,
|
|
699
|
+
headers: event.headers,
|
|
700
|
+
createdAt: event.createdAt,
|
|
701
|
+
attempt: event.attempt,
|
|
702
|
+
data: event.data
|
|
703
|
+
};
|
|
704
|
+
await resolve(event.data.params, context);
|
|
705
|
+
};
|
|
440
706
|
return {
|
|
441
707
|
type: options.type,
|
|
442
708
|
params: options.params,
|
|
443
709
|
returns: options.returns,
|
|
444
710
|
attemptsBeforeDeadLetter: options.type === "event" ? options.attemptsBeforeDeadLetter : void 0,
|
|
445
711
|
resolve,
|
|
712
|
+
onEvent,
|
|
446
713
|
onMessage: async (messageData) => {
|
|
714
|
+
var _a, _b, _c, _d;
|
|
447
715
|
const { message } = messageData;
|
|
448
716
|
const data = deserialize_default(message.value.toString());
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
await
|
|
717
|
+
const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
|
|
718
|
+
const timestamp = Number(message.timestamp);
|
|
719
|
+
const createdAt = Number.isFinite(timestamp) ? new Date(timestamp) : /* @__PURE__ */ new Date();
|
|
720
|
+
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}`;
|
|
721
|
+
await onEvent({
|
|
722
|
+
id: eventId,
|
|
723
|
+
topic: messageData.topic,
|
|
724
|
+
data,
|
|
725
|
+
transport: "kafka",
|
|
726
|
+
headers: message.headers,
|
|
727
|
+
createdAt,
|
|
728
|
+
attempt: retries + 1,
|
|
729
|
+
context: messageData
|
|
730
|
+
});
|
|
454
731
|
},
|
|
455
732
|
onRequest: async (serializedParams) => {
|
|
456
733
|
const context = {};
|
|
@@ -491,7 +768,7 @@ function EchoEvent(options = {}) {
|
|
|
491
768
|
return createEchoEvent({
|
|
492
769
|
...options,
|
|
493
770
|
resolve: async (params, contextData) => {
|
|
494
|
-
return await (0,
|
|
771
|
+
return await (0, import_logger4.runWithOrionAsyncContext)(
|
|
495
772
|
{
|
|
496
773
|
controllerType: "echo",
|
|
497
774
|
echoName: propertyKey,
|
|
@@ -520,7 +797,7 @@ function EchoRequest(options = {}) {
|
|
|
520
797
|
return createEchoRequest({
|
|
521
798
|
...options,
|
|
522
799
|
resolve: async (params, contextData) => {
|
|
523
|
-
return await (0,
|
|
800
|
+
return await (0, import_logger4.runWithOrionAsyncContext)(
|
|
524
801
|
{
|
|
525
802
|
controllerType: "echo",
|
|
526
803
|
echoName: propertyKey,
|