elasticio-sailor-nodejs 3.0.0-dev6 → 3.0.0-dev7

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