elasticio-sailor-nodejs 2.7.7-dev1 → 3.0.0-dev1

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.
@@ -0,0 +1,666 @@
1
+ const log = require('./logging.js');
2
+ const Encryptor = require('./encryptor.js');
3
+ const _ = require('lodash');
4
+ const eventToPromise = require('event-to-promise');
5
+ const uuid = require('uuid');
6
+ const http2 = require('http2');
7
+ const { getJitteredDelay } = require('./utils.js');
8
+ const { Promise } = require('q');
9
+ const pThrottle = require('p-throttle');
10
+
11
+ const {
12
+ HTTP2_HEADER_PATH,
13
+ HTTP2_HEADER_METHOD,
14
+ HTTP2_HEADER_AUTHORIZATION,
15
+ HTTP2_HEADER_STATUS
16
+ } = http2.constants;
17
+
18
+ const HEADER_ROUTING_KEY = 'x-eio-routing-key';
19
+ const AMQP_HEADER_META_PREFIX = 'x-eio-meta-';
20
+ const OBJECT_ID_HEADER = 'x-ipaas-object-storage-id';
21
+ const PROXY_FORWARD_HEADER_PREFIX = 'x-sailor-proxy-forward-';
22
+ const MESSAGE_PROCESSING_STATUS = {
23
+ SUCCESS: 'success',
24
+ ERROR: 'error'
25
+ };
26
+
27
+ class ProxyClient {
28
+ constructor(settings) {
29
+ this.settings = settings;
30
+ this._encryptor = new Encryptor(this.settings.MESSAGE_CRYPTO_PASSWORD, this.settings.MESSAGE_CRYPTO_IV);
31
+ this.closed = true;
32
+ this.clientSession = null;
33
+ this.reconnecting = false;
34
+ this.reconnectAttempts = 0;
35
+ this.reconnectTimer = null;
36
+
37
+ const username = settings.API_USERNAME;
38
+ const password = settings.API_KEY;
39
+ if (!username || !password) {
40
+ throw new Error('API_USERNAME and API_KEY must be set to connect to Sailor Proxy');
41
+ }
42
+ this.authHeader = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
43
+
44
+ this.throttles = {
45
+ // 100 Messages per Second
46
+ data: pThrottle(() => Promise.resolve(true),
47
+ settings.DATA_RATE_LIMIT,
48
+ settings.RATE_INTERVAL),
49
+ error: pThrottle(() => Promise.resolve(true),
50
+ settings.ERROR_RATE_LIMIT,
51
+ settings.RATE_INTERVAL),
52
+ snapshot: pThrottle(() => Promise.resolve(true),
53
+ settings.SNAPSHOT_RATE_LIMIT,
54
+ settings.RATE_INTERVAL)
55
+ };
56
+ }
57
+
58
+ // Health check method for K8s
59
+ // Returns true during reconnection attempts to prevent pod restarts during transient issues
60
+ // Only returns false if:
61
+ // - Connection was intentionally closed (this.closed = true)
62
+ // - Max reconnection attempts exhausted (reconnecting = false, closed = true)
63
+ isConnected() {
64
+ // For K8s health checks: consider the client "connected" if we're actively trying to reconnect
65
+ // This prevents pod restarts during transient network issues
66
+ if (this.reconnecting && !this.closed) {
67
+ return true;
68
+ }
69
+ return !this.closed && this.clientSession && !this.clientSession.destroyed;
70
+ }
71
+
72
+ async connect() {
73
+ try {
74
+ this.clientSession = http2.connect(this.settings.SAILOR_PROXY_URI);
75
+ this.closed = false;
76
+ this.reconnectAttempts = 0;
77
+
78
+ // Set up event listeners for connection management
79
+ this._setupConnectionListeners();
80
+
81
+ await eventToPromise(this.clientSession, 'connect');
82
+ log.info('Successfully connected to Sailor Proxy');
83
+ } catch (err) {
84
+ log.error({ err }, 'Failed to connect to Sailor Proxy');
85
+ throw err;
86
+ }
87
+ }
88
+
89
+ _setupConnectionListeners() {
90
+ if (!this.clientSession) return;
91
+
92
+ // Handle connection errors
93
+ this.clientSession.on('error', (err) => {
94
+ log.error({ err, closed: this.closed }, 'HTTP2 session error');
95
+ if (!this.closed) {
96
+ this._handleDisconnection('error', err);
97
+ }
98
+ });
99
+
100
+ // Handle connection close
101
+ this.clientSession.on('close', () => {
102
+ log.warn({ closed: this.closed, reconnecting: this.reconnecting }, 'HTTP2 session closed');
103
+ if (!this.closed && !this.reconnecting) {
104
+ this._handleDisconnection('close');
105
+ }
106
+ });
107
+
108
+ // Handle GOAWAY frames (server-initiated shutdown)
109
+ this.clientSession.on('goaway', (errorCode, lastStreamID, opaqueData) => {
110
+ log.warn({ errorCode, lastStreamID, closed: this.closed }, 'Received GOAWAY from server');
111
+ if (!this.closed) {
112
+ this._handleDisconnection('goaway', { errorCode, lastStreamID });
113
+ }
114
+ });
115
+
116
+ // Handle timeout
117
+ this.clientSession.on('timeout', () => {
118
+ log.warn({ closed: this.closed }, 'HTTP2 session timeout');
119
+ if (!this.closed) {
120
+ this._handleDisconnection('timeout');
121
+ }
122
+ });
123
+ }
124
+
125
+ _handleDisconnection(reason, details) {
126
+ if (this.reconnecting || this.closed) {
127
+ return;
128
+ }
129
+
130
+ log.warn({ reason, details }, 'Connection lost, initiating reconnection');
131
+ this.reconnecting = true;
132
+
133
+ // Clean up the current session
134
+ if (this.clientSession && !this.clientSession.destroyed) {
135
+ this.clientSession.destroy();
136
+ }
137
+ this.clientSession = null;
138
+
139
+ this._scheduleReconnect();
140
+ }
141
+
142
+ _scheduleReconnect() {
143
+ if (this.closed) {
144
+ log.info('Connection closed intentionally, skipping reconnection');
145
+ this.reconnecting = false;
146
+ return;
147
+ }
148
+
149
+ if (this.reconnectAttempts >= this.settings.PROXY_RECONNECT_MAX_RETRIES) {
150
+ log.error({ attempts: this.reconnectAttempts }, 'Max reconnection attempts reached, giving up');
151
+ this.reconnecting = false;
152
+ this.closed = true; // Mark as closed so K8s health check will fail and restart the pod
153
+ // Optionally emit an event or call a callback here
154
+ return;
155
+ }
156
+
157
+ this.reconnectAttempts++;
158
+ const baseDelay = Math.min(
159
+ this.settings.PROXY_RECONNECT_INITIAL_DELAY *
160
+ Math.pow(this.settings.PROXY_RECONNECT_BACKOFF_MULTIPLIER, this.reconnectAttempts - 1),
161
+ this.settings.PROXY_RECONNECT_MAX_DELAY
162
+ );
163
+ // Apply jitter to avoid thundering herd problem
164
+ const delay = getJitteredDelay(baseDelay, this.settings.PROXY_RECONNECT_JITTER_FACTOR);
165
+
166
+ log.info({ attempt: this.reconnectAttempts, baseDelayMs: baseDelay, jitteredDelayMs: delay }, 'Scheduling reconnection attempt');
167
+
168
+ this.reconnectTimer = setTimeout(async () => {
169
+ try {
170
+ log.info({ attempt: this.reconnectAttempts }, 'Attempting to reconnect');
171
+ await this._reconnect();
172
+ log.info('Successfully reconnected to Sailor Proxy');
173
+ this.reconnecting = false;
174
+ } catch (err) {
175
+ log.error({ attempt: this.reconnectAttempts, error: err }, 'Reconnection attempt failed');
176
+ this._scheduleReconnect();
177
+ }
178
+ }, delay);
179
+ }
180
+
181
+ async _reconnect() {
182
+ this.clientSession = http2.connect(this.settings.SAILOR_PROXY_URI);
183
+ this._setupConnectionListeners();
184
+ await eventToPromise(this.clientSession, 'connect');
185
+ this.reconnectAttempts = 0;
186
+ }
187
+
188
+ async disconnect() {
189
+ this.closed = true;
190
+ this.reconnecting = false;
191
+
192
+ // Clear any pending reconnection timers
193
+ if (this.reconnectTimer) {
194
+ clearTimeout(this.reconnectTimer);
195
+ this.reconnectTimer = null;
196
+ }
197
+
198
+ return new Promise((resolve) => {
199
+ if (!this.clientSession || this.clientSession.destroyed) {
200
+ log.debug('Session already destroyed');
201
+ return resolve();
202
+ }
203
+
204
+ this.clientSession.close(() => {
205
+ log.debug('Successfully closed HTTP2 connection');
206
+ resolve();
207
+ });
208
+ });
209
+ }
210
+
211
+ async _ensureConnection() {
212
+ if (this.isConnected()) {
213
+ return;
214
+ }
215
+
216
+ if (this.reconnecting) {
217
+ // Wait for reconnection to complete
218
+ log.info('Waiting for reconnection to complete');
219
+ const maxWait = 30000; // 30 seconds
220
+ const startTime = Date.now();
221
+ while (this.reconnecting && (Date.now() - startTime) < maxWait) {
222
+ await new Promise(resolve => setTimeout(resolve, 100));
223
+ }
224
+
225
+ if (!this.isConnected()) {
226
+ throw new Error('Failed to reconnect within timeout period');
227
+ }
228
+ return;
229
+ }
230
+
231
+ if (this.closed) {
232
+ throw new Error('Connection is closed. Call connect() first.');
233
+ }
234
+
235
+ // If we get here, connection was lost but reconnection hasn't started
236
+ throw new Error('Connection lost and no reconnection in progress');
237
+ }
238
+
239
+ async fetchMessageBody(message, logger) {
240
+ await this._ensureConnection();
241
+
242
+ const { body, headers } = message;
243
+
244
+ logger.info('Checking if incoming messages is lightweight...');
245
+
246
+ if (!headers) {
247
+ logger.info('Empty headers so not lightweight.');
248
+ return body;
249
+ }
250
+
251
+ const { [OBJECT_ID_HEADER]: objectId } = headers;
252
+
253
+ if (!objectId) {
254
+ logger.trace('No object id header so not lightweight.');
255
+ return body;
256
+ }
257
+
258
+ logger.info('Object id header found, message is lightweight.', { objectId });
259
+
260
+ let object;
261
+
262
+ logger.info('Going to fetch message body.', { objectId });
263
+
264
+ try {
265
+ const getObjectStream = this.clientSession.request({
266
+ [HTTP2_HEADER_PATH]: `/object/${objectId}`,
267
+ [HTTP2_HEADER_METHOD]: 'GET',
268
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
269
+ }).pipe(this._encryptor.createDecipher());
270
+ // TODO: Add retries
271
+ object = await new Promise((resolve, reject) => {
272
+ const chunks = [];
273
+ getObjectStream.on('data', chunk => {
274
+ chunks.push(chunk);
275
+ });
276
+ getObjectStream.on('error', (err) => {
277
+ logger.error(err, 'Error during fetching message body');
278
+ reject(err);
279
+ });
280
+ getObjectStream.on('end', () => {
281
+ logger.info('Message stream ended by server');
282
+ const buffer = Buffer.concat(chunks);
283
+ logger.info({ messageSize: buffer.length }, 'Received complete message from server');
284
+ resolve({ data: JSON.parse(buffer.toString()) });
285
+ });
286
+ });
287
+ } catch (e) {
288
+ log.error(e);
289
+ throw new Error(`Failed to get message body with id=${objectId}`);
290
+ }
291
+
292
+ logger.info('Successfully obtained message body.', { objectId });
293
+ logger.trace('Message body object received');
294
+
295
+ return object.data;
296
+ }
297
+
298
+ async uploadMessageBody(bodyBuf) {
299
+ await this._ensureConnection();
300
+
301
+ return new Promise((resolve, reject) => {
302
+ // TODO: Add retries
303
+ const postMessageStream = this.clientSession.request({
304
+ [HTTP2_HEADER_PATH]: '/object',
305
+ [HTTP2_HEADER_METHOD]: 'POST',
306
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
307
+ });
308
+
309
+ postMessageStream.on('response', (headers, flags) => {
310
+ const status = headers[http2.constants.HTTP2_HEADER_STATUS];
311
+ if (status !== 200) {
312
+ return reject(new Error(`Failed to upload message body, status code: ${status}`));
313
+ }
314
+ });
315
+ let responseData = '';
316
+ postMessageStream.on('data', chunk => {
317
+ responseData += chunk;
318
+ });
319
+ postMessageStream.on('error', (err) => {
320
+ log.error(err, 'Error during upload message body');
321
+ reject(err);
322
+ });
323
+ postMessageStream.on('end', () => {
324
+ try {
325
+ const responseJson = JSON.parse(responseData);
326
+ resolve(responseJson.objectId);
327
+ } catch (e) {
328
+ log.error(e, 'Failed to parse upload message body response');
329
+ reject(e);
330
+ }
331
+ });
332
+
333
+ const cipher = this._encryptor.createCipher();
334
+ cipher.pipe(postMessageStream);
335
+ cipher.write(bodyBuf);
336
+ cipher.end();
337
+ });
338
+ }
339
+
340
+ async listenForMessages(messageHandler) {
341
+ while (!this.closed) {
342
+ try {
343
+ await this._ensureConnection();
344
+
345
+ const stepId = this.settings.STEP_ID;
346
+ const prefetch = this.settings.PROXY_PREFETCH_SAILOR;
347
+ await Promise.all(new Array(prefetch).fill().map(async () => {
348
+ const queryParams = new URLSearchParams({
349
+ stepId,
350
+ prefetch
351
+ }).toString();
352
+ log.info({ stepId, prefetch }, 'Requesting message from proxy');
353
+ const getMessageStream = this.clientSession.request({
354
+ [HTTP2_HEADER_PATH]: `/message?${queryParams}`,
355
+ [HTTP2_HEADER_METHOD]: 'GET',
356
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
357
+ });
358
+
359
+ const { headers, body } = await new Promise((resolve, reject) => {
360
+ getMessageStream.on('response', (headers, flags) => {
361
+ log.info({ headers, flags }, 'Connected to message stream');
362
+ if (headers[HTTP2_HEADER_STATUS] !== 200) {
363
+ return reject(new Error(`Failed to get message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
364
+ }
365
+ const messageId = headers['x-message-id'];
366
+ const chunks = [];
367
+ getMessageStream.on('data', chunk => {
368
+ chunks.push(chunk);
369
+ });
370
+ getMessageStream.on('end', () => {
371
+ log.info('Message stream ended by server');
372
+ const body = Buffer.concat(chunks);
373
+ log.info({
374
+ messageId,
375
+ messageSize: body.length
376
+ }, 'Received complete message from server');
377
+ log.trace({ body: body.toString() }, 'Message body as string');
378
+ resolve({ headers, body });
379
+ });
380
+ });
381
+
382
+ getMessageStream.on('close', () => {
383
+ log.warn('Message stream closed by server');
384
+ reject(new Error('Message stream closed by server'));
385
+ });
386
+ getMessageStream.on('error', (err) => {
387
+ log.error(err, 'Error on message stream');
388
+ reject(err);
389
+ });
390
+ });
391
+
392
+ const proxyHeaders = this._extractProxyHeaders(headers);
393
+ const message = this._decodeMessage(body, headers);
394
+ log.debug({ proxyHeaders, message }, 'Processing received message');
395
+ await messageHandler(proxyHeaders, message);
396
+ }));
397
+ } catch (err) {
398
+ if (this.closed) {
399
+ log.info('Connection closed, stopping message listener');
400
+ break;
401
+ }
402
+
403
+ if (!this.reconnecting) {
404
+ log.error(err, 'Error in listenForMessages, waiting for reconnection');
405
+ } else {
406
+ log.debug('Currently reconnecting, will retry listening for messages after reconnection');
407
+ }
408
+ await new Promise(resolve => setTimeout(resolve, 1000));
409
+ }
410
+ }
411
+ }
412
+
413
+ async sendMessage({
414
+ incomingMessageId,
415
+ type,
416
+ data,
417
+ headers
418
+ }) {
419
+ await this._ensureConnection();
420
+
421
+ const throttledSend = this.throttles[type];
422
+ if (throttledSend) {
423
+ log.debug({ incomingMessageId, type, headers }, 'Applying rate limiting for message send');
424
+ await throttledSend();
425
+ }
426
+
427
+ log.debug({ incomingMessageId, type, headers }, 'Sending message to proxy');
428
+ log.trace({ data }, 'Message data to send to proxy');
429
+ const proxyHeaders = this._createProxyHeaders(headers);
430
+ const encryptedData = this.encryptMessageContent(data, headers.protocolVersion);
431
+ if (encryptedData.length > this.settings.OUTGOING_MESSAGE_SIZE_LIMIT) {
432
+ const error = new Error(`Outgoing message size ${encryptedData.length}`
433
+ + ` exceeds limit of ${this.settings.OUTGOING_MESSAGE_SIZE_LIMIT}.`);
434
+ log.error(error);
435
+ throw error;
436
+ }
437
+
438
+ const messageHeaders = _.mapKeys(data.headers || {}, (value, key) => key.toLowerCase());
439
+ const customRoutingKey = messageHeaders[HEADER_ROUTING_KEY];
440
+ const queryParams = new URLSearchParams({
441
+ incomingMessageId,
442
+ stepId: this.settings.STEP_ID,
443
+ type,
444
+ ...(customRoutingKey ? { customRoutingKey } : {})
445
+ }).toString();
446
+ // TODO: Add retries
447
+ const postMessageStream = this.clientSession.request({
448
+ ...proxyHeaders,
449
+ [HTTP2_HEADER_PATH]: `/message?${queryParams}`,
450
+ [HTTP2_HEADER_METHOD]: 'POST',
451
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
452
+ });
453
+ postMessageStream.write(encryptedData);
454
+ postMessageStream.end();
455
+
456
+ return new Promise((resolve, reject) => {
457
+ postMessageStream.on('response', (headers) => {
458
+ log.debug({ status: headers[HTTP2_HEADER_STATUS] }, 'Send message response');
459
+ if (headers[HTTP2_HEADER_STATUS] !== 200) {
460
+ log.error({ headers }, 'Failed to send message');
461
+ return reject(new Error(`Failed to send message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
462
+ }
463
+ });
464
+ postMessageStream.on('error', (err) => {
465
+ log.error(err, 'Error during sending message');
466
+ reject(err);
467
+ });
468
+ postMessageStream.on('end', () => {
469
+ log.debug('Send message end event');
470
+ resolve();
471
+ });
472
+ });
473
+ }
474
+
475
+ _decodeMessage(originalMessage, headers) {
476
+ log.trace('Message received');
477
+ let message;
478
+ if (this.settings.INPUT_FORMAT === 'error') {
479
+ message = this._decodeErrorMessage(originalMessage, headers);
480
+ } else {
481
+ message = this._decodeDefaultMessage(originalMessage, headers);
482
+ }
483
+ message.headers = message.headers || {};
484
+ if (headers.replyTo) {
485
+ message.headers.reply_to = headers.replyTo;
486
+ }
487
+ return message;
488
+ }
489
+
490
+ _decodeDefaultMessage(originalMessage, headers) {
491
+ const protocolVersion = Number(headers.protocolVersion || 1);
492
+ return this._encryptor.decryptMessageContent(
493
+ originalMessage,
494
+ protocolVersion < 2 ? 'base64' : undefined
495
+ );
496
+ }
497
+
498
+ _decodeErrorMessage(originalMessage, headers) {
499
+ const errorBody = JSON.parse(originalMessage.toString());
500
+ if (errorBody.error) {
501
+ errorBody.error = this._encryptor.decryptMessageContent(Buffer.from(errorBody.error), 'base64');
502
+ }
503
+ if (errorBody.errorInput) {
504
+ errorBody.errorInput = this._encryptor.decryptMessageContent(errorBody.errorInput, 'base64');
505
+ }
506
+ return {
507
+ body: errorBody,
508
+ headers
509
+ };
510
+ }
511
+
512
+ async finishProcessing(incomingHeaders, status) {
513
+ await this._ensureConnection();
514
+
515
+ if (Object.values(MESSAGE_PROCESSING_STATUS).indexOf(status) === -1) {
516
+ throw new Error(`Invalid message processing status: ${status}`);
517
+ }
518
+ const incomingMessageId = incomingHeaders.messageId;
519
+ log.debug({ incomingMessageId, status }, 'Finishing processing of message');
520
+ const queryParams = new URLSearchParams({
521
+ incomingMessageId,
522
+ status
523
+ }).toString();
524
+ // TODO: Add retries
525
+ const postMessageStream = this.clientSession.request({
526
+ [HTTP2_HEADER_PATH]: `/finish-processing?${queryParams}`,
527
+ [HTTP2_HEADER_METHOD]: 'POST',
528
+ [HTTP2_HEADER_AUTHORIZATION]: this.authHeader
529
+ });
530
+ postMessageStream.end();
531
+
532
+ return new Promise((resolve, reject) => {
533
+ postMessageStream.on('response', (headers) => {
534
+ log.debug({ status: headers[HTTP2_HEADER_STATUS] }, 'Finish processing response event');
535
+ if (headers[HTTP2_HEADER_STATUS] !== 200) {
536
+ log.error({ headers }, 'Failed to finish processing message');
537
+ return reject(new Error(`Failed to finish processing message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
538
+ }
539
+ });
540
+
541
+ postMessageStream.on('end', () => {
542
+ log.debug('Finish processing end event');
543
+ resolve();
544
+ });
545
+
546
+ postMessageStream.on('error', reject);
547
+ });
548
+ }
549
+
550
+ encryptMessageContent(body, protocolVersion = 1) {
551
+ return this._encryptor.encryptMessageContent(
552
+ body,
553
+ protocolVersion < 2
554
+ ? 'base64'
555
+ : undefined
556
+ );
557
+ }
558
+
559
+ async sendError(err, headers, originalMessage, incomingHeaders) {
560
+ await this._ensureConnection();
561
+
562
+ const settings = this.settings;
563
+
564
+ const encryptedError = this._encryptor.encryptMessageContent({
565
+ name: err.name,
566
+ message: err.message,
567
+ stack: err.stack
568
+ }, 'base64').toString();
569
+
570
+ const payload = {
571
+ error: encryptedError
572
+ };
573
+ if (originalMessage) {
574
+ const protocolVersion = Number(incomingHeaders.protocolVersion || 1);
575
+ if (protocolVersion >= 2) {
576
+ payload.errorInput = this._encryptor.encryptMessageContent(
577
+ originalMessage,
578
+ 'base64'
579
+ ).toString();
580
+ } else {
581
+ payload.errorInput = originalMessage;
582
+ }
583
+ }
584
+ const errorPayload = JSON.stringify(payload);
585
+
586
+ let result = await this.sendMessage({
587
+ incomingMessageId: incomingHeaders ? incomingHeaders.messageId : undefined,
588
+ type: 'error',
589
+ data: errorPayload,
590
+ headers
591
+ });
592
+
593
+ return result;
594
+ }
595
+
596
+ async sendRebound(reboundError, incomingHeaders, outgoingHeaders) {
597
+ await this._ensureConnection();
598
+
599
+ outgoingHeaders.end = new Date().getTime();
600
+ outgoingHeaders.reboundReason = reboundError.message;
601
+ return this.sendMessage({
602
+ type: 'rebound',
603
+ headers: outgoingHeaders,
604
+ incomingMessageId: incomingHeaders ? incomingHeaders.messageId : undefined,
605
+ data: reboundError
606
+ });
607
+ }
608
+
609
+ async sendSnapshot(data, headers) {
610
+ await this._ensureConnection();
611
+
612
+ const payload = JSON.stringify(data);
613
+ const properties = this._createProxyHeaders(headers);
614
+ return this.sendMessage({
615
+ type: 'snapshot',
616
+ data: payload,
617
+ headers: properties
618
+ });
619
+ }
620
+
621
+ _createProxyHeaders(headers) {
622
+ headers.messageId = headers.messageId || uuid.v4();
623
+ return Object.entries(headers || {}).reduce((acc, [key, value]) => {
624
+ acc[`${PROXY_FORWARD_HEADER_PREFIX}${_.kebabCase(key)}`] = value;
625
+ return acc;
626
+ }, {});
627
+ }
628
+
629
+ _extractProxyHeaders(proxyHeaders) {
630
+ log.trace({ proxyHeaders }, 'Extracting proxy headers');
631
+ const headers = Object.entries(proxyHeaders || {}).reduce((acc, [key, value]) => {
632
+ if (key.startsWith(PROXY_FORWARD_HEADER_PREFIX)) {
633
+ const originalKey = key.substring(PROXY_FORWARD_HEADER_PREFIX.length);
634
+ acc[_.camelCase(originalKey)] = value;
635
+ }
636
+ return acc;
637
+ }, {});
638
+
639
+ const metaHeaderNames = Object.keys(headers)
640
+ .filter(key => key.toLowerCase().startsWith(AMQP_HEADER_META_PREFIX));
641
+
642
+ const metaHeaders = _.pick(headers, metaHeaderNames);
643
+ const metaHeadersLowerCased = _.mapKeys(metaHeaders, (value, key) => key.toLowerCase());
644
+
645
+ const result = {
646
+ stepId: headers.stepId,
647
+ ...metaHeadersLowerCased,
648
+ threadId: headers.threadId || metaHeadersLowerCased['x-eio-meta-trace-id'],
649
+ messageId: headers.messageId,
650
+ parentMessageId: headers.parentMessageId
651
+ };
652
+ if (!result.threadId) {
653
+ const threadId = uuid.v4();
654
+ log.debug({ threadId }, 'Initiate new thread as it is not started ATM');
655
+ result.threadId = threadId;
656
+ }
657
+ if (headers.replyTo) {
658
+ result.replyTo = headers.replyTo;
659
+ }
660
+ log.debug({ result }, 'Extracted proxy headers');
661
+ return result;
662
+ }
663
+ }
664
+
665
+ exports.ProxyClient = ProxyClient;
666
+ exports.MESSAGE_PROCESSING_STATUS = MESSAGE_PROCESSING_STATUS;