@opra/rabbitmq 1.16.1 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConfigBuilder = void 0;
4
+ const objects_1 = require("@jsopen/objects");
5
+ const common_1 = require("@opra/common");
6
+ const constants_js_1 = require("./constants.js");
7
+ class ConfigBuilder {
8
+ constructor(document, config) {
9
+ this.document = document;
10
+ this.config = config;
11
+ }
12
+ async build() {
13
+ this.controllerInstances = new Map();
14
+ this.handlerArgs = [];
15
+ this._prepareConnectionOptions();
16
+ /** Initialize consumers */
17
+ for (const controller of this.document.rpcApi.controllers.values()) {
18
+ let instance = controller.instance;
19
+ if (!instance && controller.ctor)
20
+ instance = new controller.ctor();
21
+ if (!instance)
22
+ continue;
23
+ this.controllerInstances.set(controller, instance);
24
+ /** Build HandlerData array */
25
+ for (const operation of controller.operations.values()) {
26
+ const consumerConfig = await this._getConsumerConfig(controller, instance, operation);
27
+ if (!consumerConfig)
28
+ continue;
29
+ const args = {
30
+ // consumer: null as any,
31
+ controller,
32
+ instance,
33
+ operation,
34
+ consumerConfig,
35
+ // handler: null as any,
36
+ topics: (Array.isArray(operation.channel)
37
+ ? operation.channel
38
+ : [operation.channel]).map(String),
39
+ };
40
+ this.handlerArgs.push(args);
41
+ }
42
+ }
43
+ }
44
+ _prepareConnectionOptions() {
45
+ this.connectionOptions = {};
46
+ if (Array.isArray(this.config.connection))
47
+ this.connectionOptions.urls = this.config.connection;
48
+ else if (typeof this.config.connection === 'object') {
49
+ (0, objects_1.merge)(this.connectionOptions, this.config.connection, { deep: true });
50
+ }
51
+ else
52
+ this.connectionOptions.urls = [
53
+ this.config.connection || 'amqp://guest:guest@localhost:5672',
54
+ ];
55
+ }
56
+ /**
57
+ *
58
+ * @param controller
59
+ * @param instance
60
+ * @param operation
61
+ * @protected
62
+ */
63
+ async _getConsumerConfig(controller, instance, operation) {
64
+ if (typeof instance[operation.name] !== 'function')
65
+ return;
66
+ const proto = controller.ctor?.prototype || Object.getPrototypeOf(instance);
67
+ if (Reflect.hasMetadata(common_1.RPC_CONTROLLER_METADATA, proto, operation.name))
68
+ return;
69
+ const operationConfig = {};
70
+ if (this.config.defaults) {
71
+ if (this.config.defaults.consumer) {
72
+ (0, objects_1.merge)(operationConfig, this.config.defaults.consumer, { deep: true });
73
+ }
74
+ }
75
+ let metadata = Reflect.getMetadata(constants_js_1.RMQ_OPERATION_METADATA, proto, operation.name);
76
+ if (!metadata) {
77
+ const configResolver = Reflect.getMetadata(constants_js_1.RMQ_OPERATION_METADATA_RESOLVER, proto, operation.name);
78
+ if (configResolver) {
79
+ metadata = await configResolver();
80
+ }
81
+ }
82
+ if (metadata && typeof metadata === 'object') {
83
+ (0, objects_1.merge)(operationConfig, metadata, { deep: true });
84
+ }
85
+ return operationConfig;
86
+ }
87
+ }
88
+ exports.ConfigBuilder = ConfigBuilder;
@@ -6,12 +6,12 @@ const node_zlib_1 = tslib_1.__importDefault(require("node:zlib"));
6
6
  const type_is_1 = tslib_1.__importDefault(require("@browsery/type-is"));
7
7
  const common_1 = require("@opra/common");
8
8
  const core_1 = require("@opra/core");
9
- const amqp_connection_manager_1 = require("amqp-connection-manager");
10
9
  const content_type_1 = require("content-type");
11
10
  const iconv_lite_1 = tslib_1.__importDefault(require("iconv-lite"));
11
+ const rabbit = tslib_1.__importStar(require("rabbitmq-client"));
12
12
  const util_1 = require("util");
13
13
  const valgen_1 = require("valgen");
14
- const constants_js_1 = require("./constants.js");
14
+ const config_builder_js_1 = require("./config-builder.js");
15
15
  const rabbitmq_context_js_1 = require("./rabbitmq-context.js");
16
16
  const gunzipAsync = (0, util_1.promisify)(node_zlib_1.default.gunzip);
17
17
  const deflateAsync = (0, util_1.promisify)(node_zlib_1.default.deflate);
@@ -34,6 +34,7 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
34
34
  constructor(document, config) {
35
35
  super(config);
36
36
  this._controllerInstances = new Map();
37
+ this._consumers = [];
37
38
  this._status = 'idle';
38
39
  this.protocol = 'rpc';
39
40
  this.platform = RabbitmqAdapter.PlatformName;
@@ -43,7 +44,6 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
43
44
  this.document.api.platform === RabbitmqAdapter.PlatformName)) {
44
45
  throw new TypeError(`The document doesn't expose a RabbitMQ Api`);
45
46
  }
46
- // this._config = config;
47
47
  this.interceptors = [...(config.interceptors || [])];
48
48
  globalErrorTypes.forEach(type => {
49
49
  process.on(type, e => {
@@ -71,98 +71,69 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
71
71
  if (this.status !== 'idle')
72
72
  return;
73
73
  this._status = 'starting';
74
- const handlerArgs = [];
74
+ // const handlerArgs: HandlerArguments[] = [];
75
+ const configBuilder = new config_builder_js_1.ConfigBuilder(this.document, this._config);
76
+ await configBuilder.build();
77
+ this._client = new rabbit.Connection(configBuilder.connectionOptions);
75
78
  try {
76
- /** Initialize consumers */
77
- for (const controller of this.document.rpcApi.controllers.values()) {
78
- let instance = controller.instance;
79
- if (!instance && controller.ctor)
80
- instance = new controller.ctor();
81
- if (!instance)
82
- continue;
83
- this._controllerInstances.set(controller, instance);
84
- /** Build HandlerData array */
85
- for (const operation of controller.operations.values()) {
86
- const operationConfig = await this._getOperationConfig(controller, instance, operation);
87
- if (!operationConfig)
88
- continue;
89
- const args = {
90
- consumer: null,
91
- controller,
92
- instance,
93
- operation,
94
- operationConfig,
95
- handler: null,
96
- topics: (Array.isArray(operation.channel)
97
- ? operation.channel
98
- : [operation.channel]).map(String),
99
- };
100
- this._createHandler(args);
101
- handlerArgs.push(args);
102
- }
103
- }
104
- const connectionOptions = typeof this._config.connection === 'string'
105
- ? {
106
- urls: [this._config.connection],
107
- }
108
- : Array.isArray(this._config.connection)
109
- ? {
110
- urls: this._config.connection,
111
- }
112
- : this._config.connection;
113
- this._client = new amqp_connection_manager_1.AmqpConnectionManagerClass(connectionOptions.urls, connectionOptions);
114
- this._client.connect().catch(e => {
115
- e.message =
116
- 'Unable to connect to RabbitMQ server at ' +
117
- connectionOptions.urls +
118
- '. ' +
119
- e.message;
79
+ /** Establish connection */
80
+ await this._client.onConnect().catch(e => {
81
+ e.message = `RabbitMQ connection error. ${e.message}`;
120
82
  throw e;
121
83
  });
122
- this.logger?.info?.(`Connected RabbitMQ at ${connectionOptions.urls}`);
123
- for (const args of handlerArgs) {
124
- /** Create channel per operation */
125
- this._client.createChannel({
126
- setup: async (channel) => {
127
- for (const topic of args.topics) {
128
- const opts = this._config.queues?.[topic];
129
- await channel.assertQueue(topic, opts);
130
- await channel
131
- .consume(topic, async (msg) => {
132
- if (!msg)
133
- return;
134
- await this.emitAsync('message', msg, topic).catch(noOp);
135
- try {
136
- await args.handler(channel, topic, msg);
137
- }
138
- catch (e) {
139
- this._emitError(e);
140
- }
141
- },
142
- /** Consume options */
143
- args.operationConfig.consumer)
144
- .catch(e => {
84
+ this.logger?.info?.(`Connected RabbitMQ at ${configBuilder.connectionOptions.urls}`);
85
+ /** Subscribe to queues */
86
+ const promises = [];
87
+ for (const args of configBuilder.handlerArgs) {
88
+ for (const queue of args.topics) {
89
+ const handler = this._createHandler(args);
90
+ promises.push(new Promise((resolve, reject) => {
91
+ const consumer = this._client.createConsumer({
92
+ ...args.consumerConfig,
93
+ queueOptions: {
94
+ ...args.consumerConfig.queueOptions,
95
+ durable: args.consumerConfig.queueOptions?.durable ?? true,
96
+ },
97
+ queue,
98
+ }, async (msg, reply) => {
99
+ await this.emitAsync('message', msg, queue).catch(noOp);
100
+ try {
101
+ await handler(consumer, queue, msg, reply);
102
+ }
103
+ catch (e) {
145
104
  this._emitError(e);
146
- throw e;
147
- });
148
- this.logger?.info?.(`Subscribed to topic${args.topics.length > 1 ? 's' : ''} "${args.topics}"`);
149
- }
150
- },
151
- });
105
+ }
106
+ });
107
+ this._consumers.push(consumer);
108
+ consumer.on('ready', () => {
109
+ this.logger?.info?.(`Subscribed to topic "${queue}"`);
110
+ resolve();
111
+ });
112
+ consumer.on('error', (err) => {
113
+ err.message = `Consumer error (${queue})". ${err.message}`;
114
+ err.queue = queue;
115
+ reject(err);
116
+ });
117
+ }));
118
+ }
152
119
  }
120
+ await Promise.all(promises);
153
121
  this._status = 'started';
154
122
  }
155
- catch (e) {
123
+ catch (err) {
124
+ this._emitError(err);
156
125
  await this.close();
157
- throw e;
158
126
  }
159
127
  }
160
128
  /**
161
129
  * Closes all connections and stops the service
162
130
  */
163
131
  async close() {
132
+ if (this._consumers.length)
133
+ await Promise.allSettled(this._consumers.map(consumer => consumer.close()));
164
134
  await this._client?.close();
165
135
  this._client = undefined;
136
+ this._consumers = [];
166
137
  this._controllerInstances.clear();
167
138
  this._status = 'idle';
168
139
  }
@@ -170,43 +141,6 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
170
141
  const controller = this.api.findController(controllerPath);
171
142
  return controller && this._controllerInstances.get(controller);
172
143
  }
173
- /**
174
- *
175
- * @param controller
176
- * @param instance
177
- * @param operation
178
- * @protected
179
- */
180
- async _getOperationConfig(controller, instance, operation) {
181
- if (typeof instance[operation.name] !== 'function')
182
- return;
183
- const proto = controller.ctor?.prototype || Object.getPrototypeOf(instance);
184
- if (Reflect.hasMetadata(common_1.RPC_CONTROLLER_METADATA, proto, operation.name))
185
- return;
186
- const operationConfig = {
187
- consumer: {
188
- noAck: true,
189
- },
190
- };
191
- if (this._config.defaults) {
192
- if (this._config.defaults.consumer) {
193
- Object.assign(operationConfig.consumer, this._config.defaults.consumer);
194
- }
195
- }
196
- let metadata = Reflect.getMetadata(constants_js_1.RMQ_OPERATION_METADATA, proto, operation.name);
197
- if (!metadata) {
198
- const configResolver = Reflect.getMetadata(constants_js_1.RMQ_OPERATION_METADATA_RESOLVER, proto, operation.name);
199
- if (configResolver) {
200
- metadata = await configResolver();
201
- }
202
- }
203
- if (metadata) {
204
- if (typeof metadata.consumer === 'object') {
205
- Object.assign(operationConfig.consumer, metadata.consumer);
206
- }
207
- }
208
- return operationConfig;
209
- }
210
144
  /**
211
145
  *
212
146
  * @param args
@@ -230,10 +164,17 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
230
164
  this[core_1.kAssetCache].set(header, 'decode', decode);
231
165
  }
232
166
  });
233
- args.handler = async (channel, queue, message) => {
167
+ return async (consumer, queue, message, _reply) => {
234
168
  if (!message)
235
169
  return;
236
170
  const operationHandler = instance[operation.name];
171
+ let replyCalled = false;
172
+ const reply = async (body, envelope) => {
173
+ if (replyCalled)
174
+ return;
175
+ replyCalled = true;
176
+ return _reply(body, envelope);
177
+ };
237
178
  const headers = {};
238
179
  /** Create context */
239
180
  const context = new rabbitmq_context_js_1.RabbitmqContext({
@@ -244,10 +185,11 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
244
185
  operation,
245
186
  operationHandler,
246
187
  queue,
247
- channel,
188
+ consumer,
248
189
  message,
249
190
  content: undefined,
250
191
  headers,
192
+ reply,
251
193
  });
252
194
  try {
253
195
  /** Parse and decode `payload` */
@@ -259,8 +201,8 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
259
201
  }
260
202
  // message.properties.
261
203
  /** Parse and decode `headers` */
262
- if (message.properties.headers) {
263
- for (const [k, v] of Object.entries(message.properties.headers)) {
204
+ if (message.headers) {
205
+ for (const [k, v] of Object.entries(message.headers)) {
264
206
  const header = operation.findHeader(k);
265
207
  const decode = this[core_1.kAssetCache].get(header, 'decode') || valgen_1.vg.isAny();
266
208
  headers[k] = decode(Buffer.isBuffer(v) ? v.toString() : v);
@@ -269,7 +211,6 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
269
211
  context.content = content;
270
212
  }
271
213
  catch (e) {
272
- context.ack();
273
214
  this._emitError(e, context);
274
215
  return;
275
216
  }
@@ -277,6 +218,8 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
277
218
  try {
278
219
  /** Call operation handler */
279
220
  const result = await operationHandler.call(instance, context);
221
+ if (result !== undefined)
222
+ await reply(result);
280
223
  await this.emitAsync('finish', context, result).catch(noOp);
281
224
  }
282
225
  catch (e) {
@@ -285,11 +228,13 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
285
228
  };
286
229
  }
287
230
  async _parseContent(msg) {
288
- if (!msg.content?.length)
231
+ if (!Buffer.isBuffer(msg.body))
232
+ return msg.body;
233
+ if (!msg.body?.length)
289
234
  return;
290
- let content = msg.content;
291
- if (msg.properties.contentEncoding) {
292
- switch (msg.properties.contentEncoding) {
235
+ let content = msg.body;
236
+ if (msg.contentEncoding) {
237
+ switch (msg.contentEncoding) {
293
238
  case 'gzip':
294
239
  case 'x-gzip': {
295
240
  content = await gunzipAsync(content);
@@ -315,12 +260,11 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
315
260
  }
316
261
  }
317
262
  }
318
- const mediaType = msg.properties.contentType &&
319
- (0, content_type_1.parse)(msg.properties.contentType || '');
320
- let charset = (mediaType?.parameters.charset || '').toLowerCase();
321
- if (!charset && type_is_1.default.is(mediaType?.type, ['json', 'xml', 'txt']))
322
- charset = 'utf-8';
323
- if (charset) {
263
+ const mediaType = msg.contentType
264
+ ? (0, content_type_1.parse)(msg.contentType || '')
265
+ : undefined;
266
+ if (mediaType && type_is_1.default.is(mediaType.type, ['json', 'xml', 'txt'])) {
267
+ const charset = (mediaType.parameters.charset || '').toLowerCase() || 'utf-8';
324
268
  content = iconv_lite_1.default.decode(content, charset);
325
269
  if (type_is_1.default.is(mediaType.type, ['json']))
326
270
  return JSON.parse(content);
@@ -18,7 +18,6 @@ class RabbitmqContext extends core_1.ExecutionContext {
18
18
  documentNode: init.controller?.node,
19
19
  protocol: 'rpc',
20
20
  });
21
- this._ackSent = false;
22
21
  this.adapter = init.adapter;
23
22
  this.platform = init.adapter.platform;
24
23
  this.protocol = 'rpc';
@@ -30,29 +29,12 @@ class RabbitmqContext extends core_1.ExecutionContext {
30
29
  this.operation = init.operation;
31
30
  if (init.operationHandler)
32
31
  this.operationHandler = init.operationHandler;
33
- this.channel = init.channel;
32
+ this.consumer = init.consumer;
34
33
  this.queue = init.queue;
35
34
  this.message = init.message;
36
35
  this.headers = init.headers || {};
37
36
  this.content = init.content;
38
- }
39
- get properties() {
40
- return this.message.properties;
41
- }
42
- get fields() {
43
- return this.message.fields;
44
- }
45
- ack() {
46
- if (this._ackSent)
47
- return;
48
- this._ackSent = true;
49
- this.channel.ack(this.message);
50
- }
51
- nack() {
52
- if (this._ackSent)
53
- return;
54
- this._ackSent = true;
55
- this.channel.nack(this.message);
37
+ this.reply = init.reply;
56
38
  }
57
39
  }
58
40
  exports.RabbitmqContext = RabbitmqContext;
@@ -0,0 +1,84 @@
1
+ import { merge } from '@jsopen/objects';
2
+ import { RPC_CONTROLLER_METADATA, } from '@opra/common';
3
+ import { RMQ_OPERATION_METADATA, RMQ_OPERATION_METADATA_RESOLVER, } from './constants.js';
4
+ export class ConfigBuilder {
5
+ constructor(document, config) {
6
+ this.document = document;
7
+ this.config = config;
8
+ }
9
+ async build() {
10
+ this.controllerInstances = new Map();
11
+ this.handlerArgs = [];
12
+ this._prepareConnectionOptions();
13
+ /** Initialize consumers */
14
+ for (const controller of this.document.rpcApi.controllers.values()) {
15
+ let instance = controller.instance;
16
+ if (!instance && controller.ctor)
17
+ instance = new controller.ctor();
18
+ if (!instance)
19
+ continue;
20
+ this.controllerInstances.set(controller, instance);
21
+ /** Build HandlerData array */
22
+ for (const operation of controller.operations.values()) {
23
+ const consumerConfig = await this._getConsumerConfig(controller, instance, operation);
24
+ if (!consumerConfig)
25
+ continue;
26
+ const args = {
27
+ // consumer: null as any,
28
+ controller,
29
+ instance,
30
+ operation,
31
+ consumerConfig,
32
+ // handler: null as any,
33
+ topics: (Array.isArray(operation.channel)
34
+ ? operation.channel
35
+ : [operation.channel]).map(String),
36
+ };
37
+ this.handlerArgs.push(args);
38
+ }
39
+ }
40
+ }
41
+ _prepareConnectionOptions() {
42
+ this.connectionOptions = {};
43
+ if (Array.isArray(this.config.connection))
44
+ this.connectionOptions.urls = this.config.connection;
45
+ else if (typeof this.config.connection === 'object') {
46
+ merge(this.connectionOptions, this.config.connection, { deep: true });
47
+ }
48
+ else
49
+ this.connectionOptions.urls = [
50
+ this.config.connection || 'amqp://guest:guest@localhost:5672',
51
+ ];
52
+ }
53
+ /**
54
+ *
55
+ * @param controller
56
+ * @param instance
57
+ * @param operation
58
+ * @protected
59
+ */
60
+ async _getConsumerConfig(controller, instance, operation) {
61
+ if (typeof instance[operation.name] !== 'function')
62
+ return;
63
+ const proto = controller.ctor?.prototype || Object.getPrototypeOf(instance);
64
+ if (Reflect.hasMetadata(RPC_CONTROLLER_METADATA, proto, operation.name))
65
+ return;
66
+ const operationConfig = {};
67
+ if (this.config.defaults) {
68
+ if (this.config.defaults.consumer) {
69
+ merge(operationConfig, this.config.defaults.consumer, { deep: true });
70
+ }
71
+ }
72
+ let metadata = Reflect.getMetadata(RMQ_OPERATION_METADATA, proto, operation.name);
73
+ if (!metadata) {
74
+ const configResolver = Reflect.getMetadata(RMQ_OPERATION_METADATA_RESOLVER, proto, operation.name);
75
+ if (configResolver) {
76
+ metadata = await configResolver();
77
+ }
78
+ }
79
+ if (metadata && typeof metadata === 'object') {
80
+ merge(operationConfig, metadata, { deep: true });
81
+ }
82
+ return operationConfig;
83
+ }
84
+ }
@@ -1,13 +1,13 @@
1
1
  import zlib from 'node:zlib';
2
2
  import typeIs from '@browsery/type-is';
3
- import { OpraException, RPC_CONTROLLER_METADATA, RpcApi, } from '@opra/common';
3
+ import { OpraException, RpcApi, } from '@opra/common';
4
4
  import { kAssetCache, PlatformAdapter } from '@opra/core';
5
- import { AmqpConnectionManagerClass, } from 'amqp-connection-manager';
6
5
  import { parse as parseContentType } from 'content-type';
7
6
  import iconv from 'iconv-lite';
7
+ import * as rabbit from 'rabbitmq-client';
8
8
  import { promisify } from 'util';
9
9
  import { vg } from 'valgen';
10
- import { RMQ_OPERATION_METADATA, RMQ_OPERATION_METADATA_RESOLVER, } from './constants.js';
10
+ import { ConfigBuilder } from './config-builder.js';
11
11
  import { RabbitmqContext } from './rabbitmq-context.js';
12
12
  const gunzipAsync = promisify(zlib.gunzip);
13
13
  const deflateAsync = promisify(zlib.deflate);
@@ -30,6 +30,7 @@ export class RabbitmqAdapter extends PlatformAdapter {
30
30
  constructor(document, config) {
31
31
  super(config);
32
32
  this._controllerInstances = new Map();
33
+ this._consumers = [];
33
34
  this._status = 'idle';
34
35
  this.protocol = 'rpc';
35
36
  this.platform = RabbitmqAdapter.PlatformName;
@@ -39,7 +40,6 @@ export class RabbitmqAdapter extends PlatformAdapter {
39
40
  this.document.api.platform === RabbitmqAdapter.PlatformName)) {
40
41
  throw new TypeError(`The document doesn't expose a RabbitMQ Api`);
41
42
  }
42
- // this._config = config;
43
43
  this.interceptors = [...(config.interceptors || [])];
44
44
  globalErrorTypes.forEach(type => {
45
45
  process.on(type, e => {
@@ -67,98 +67,69 @@ export class RabbitmqAdapter extends PlatformAdapter {
67
67
  if (this.status !== 'idle')
68
68
  return;
69
69
  this._status = 'starting';
70
- const handlerArgs = [];
70
+ // const handlerArgs: HandlerArguments[] = [];
71
+ const configBuilder = new ConfigBuilder(this.document, this._config);
72
+ await configBuilder.build();
73
+ this._client = new rabbit.Connection(configBuilder.connectionOptions);
71
74
  try {
72
- /** Initialize consumers */
73
- for (const controller of this.document.rpcApi.controllers.values()) {
74
- let instance = controller.instance;
75
- if (!instance && controller.ctor)
76
- instance = new controller.ctor();
77
- if (!instance)
78
- continue;
79
- this._controllerInstances.set(controller, instance);
80
- /** Build HandlerData array */
81
- for (const operation of controller.operations.values()) {
82
- const operationConfig = await this._getOperationConfig(controller, instance, operation);
83
- if (!operationConfig)
84
- continue;
85
- const args = {
86
- consumer: null,
87
- controller,
88
- instance,
89
- operation,
90
- operationConfig,
91
- handler: null,
92
- topics: (Array.isArray(operation.channel)
93
- ? operation.channel
94
- : [operation.channel]).map(String),
95
- };
96
- this._createHandler(args);
97
- handlerArgs.push(args);
98
- }
99
- }
100
- const connectionOptions = typeof this._config.connection === 'string'
101
- ? {
102
- urls: [this._config.connection],
103
- }
104
- : Array.isArray(this._config.connection)
105
- ? {
106
- urls: this._config.connection,
107
- }
108
- : this._config.connection;
109
- this._client = new AmqpConnectionManagerClass(connectionOptions.urls, connectionOptions);
110
- this._client.connect().catch(e => {
111
- e.message =
112
- 'Unable to connect to RabbitMQ server at ' +
113
- connectionOptions.urls +
114
- '. ' +
115
- e.message;
75
+ /** Establish connection */
76
+ await this._client.onConnect().catch(e => {
77
+ e.message = `RabbitMQ connection error. ${e.message}`;
116
78
  throw e;
117
79
  });
118
- this.logger?.info?.(`Connected RabbitMQ at ${connectionOptions.urls}`);
119
- for (const args of handlerArgs) {
120
- /** Create channel per operation */
121
- this._client.createChannel({
122
- setup: async (channel) => {
123
- for (const topic of args.topics) {
124
- const opts = this._config.queues?.[topic];
125
- await channel.assertQueue(topic, opts);
126
- await channel
127
- .consume(topic, async (msg) => {
128
- if (!msg)
129
- return;
130
- await this.emitAsync('message', msg, topic).catch(noOp);
131
- try {
132
- await args.handler(channel, topic, msg);
133
- }
134
- catch (e) {
135
- this._emitError(e);
136
- }
137
- },
138
- /** Consume options */
139
- args.operationConfig.consumer)
140
- .catch(e => {
80
+ this.logger?.info?.(`Connected RabbitMQ at ${configBuilder.connectionOptions.urls}`);
81
+ /** Subscribe to queues */
82
+ const promises = [];
83
+ for (const args of configBuilder.handlerArgs) {
84
+ for (const queue of args.topics) {
85
+ const handler = this._createHandler(args);
86
+ promises.push(new Promise((resolve, reject) => {
87
+ const consumer = this._client.createConsumer({
88
+ ...args.consumerConfig,
89
+ queueOptions: {
90
+ ...args.consumerConfig.queueOptions,
91
+ durable: args.consumerConfig.queueOptions?.durable ?? true,
92
+ },
93
+ queue,
94
+ }, async (msg, reply) => {
95
+ await this.emitAsync('message', msg, queue).catch(noOp);
96
+ try {
97
+ await handler(consumer, queue, msg, reply);
98
+ }
99
+ catch (e) {
141
100
  this._emitError(e);
142
- throw e;
143
- });
144
- this.logger?.info?.(`Subscribed to topic${args.topics.length > 1 ? 's' : ''} "${args.topics}"`);
145
- }
146
- },
147
- });
101
+ }
102
+ });
103
+ this._consumers.push(consumer);
104
+ consumer.on('ready', () => {
105
+ this.logger?.info?.(`Subscribed to topic "${queue}"`);
106
+ resolve();
107
+ });
108
+ consumer.on('error', (err) => {
109
+ err.message = `Consumer error (${queue})". ${err.message}`;
110
+ err.queue = queue;
111
+ reject(err);
112
+ });
113
+ }));
114
+ }
148
115
  }
116
+ await Promise.all(promises);
149
117
  this._status = 'started';
150
118
  }
151
- catch (e) {
119
+ catch (err) {
120
+ this._emitError(err);
152
121
  await this.close();
153
- throw e;
154
122
  }
155
123
  }
156
124
  /**
157
125
  * Closes all connections and stops the service
158
126
  */
159
127
  async close() {
128
+ if (this._consumers.length)
129
+ await Promise.allSettled(this._consumers.map(consumer => consumer.close()));
160
130
  await this._client?.close();
161
131
  this._client = undefined;
132
+ this._consumers = [];
162
133
  this._controllerInstances.clear();
163
134
  this._status = 'idle';
164
135
  }
@@ -166,43 +137,6 @@ export class RabbitmqAdapter extends PlatformAdapter {
166
137
  const controller = this.api.findController(controllerPath);
167
138
  return controller && this._controllerInstances.get(controller);
168
139
  }
169
- /**
170
- *
171
- * @param controller
172
- * @param instance
173
- * @param operation
174
- * @protected
175
- */
176
- async _getOperationConfig(controller, instance, operation) {
177
- if (typeof instance[operation.name] !== 'function')
178
- return;
179
- const proto = controller.ctor?.prototype || Object.getPrototypeOf(instance);
180
- if (Reflect.hasMetadata(RPC_CONTROLLER_METADATA, proto, operation.name))
181
- return;
182
- const operationConfig = {
183
- consumer: {
184
- noAck: true,
185
- },
186
- };
187
- if (this._config.defaults) {
188
- if (this._config.defaults.consumer) {
189
- Object.assign(operationConfig.consumer, this._config.defaults.consumer);
190
- }
191
- }
192
- let metadata = Reflect.getMetadata(RMQ_OPERATION_METADATA, proto, operation.name);
193
- if (!metadata) {
194
- const configResolver = Reflect.getMetadata(RMQ_OPERATION_METADATA_RESOLVER, proto, operation.name);
195
- if (configResolver) {
196
- metadata = await configResolver();
197
- }
198
- }
199
- if (metadata) {
200
- if (typeof metadata.consumer === 'object') {
201
- Object.assign(operationConfig.consumer, metadata.consumer);
202
- }
203
- }
204
- return operationConfig;
205
- }
206
140
  /**
207
141
  *
208
142
  * @param args
@@ -226,10 +160,17 @@ export class RabbitmqAdapter extends PlatformAdapter {
226
160
  this[kAssetCache].set(header, 'decode', decode);
227
161
  }
228
162
  });
229
- args.handler = async (channel, queue, message) => {
163
+ return async (consumer, queue, message, _reply) => {
230
164
  if (!message)
231
165
  return;
232
166
  const operationHandler = instance[operation.name];
167
+ let replyCalled = false;
168
+ const reply = async (body, envelope) => {
169
+ if (replyCalled)
170
+ return;
171
+ replyCalled = true;
172
+ return _reply(body, envelope);
173
+ };
233
174
  const headers = {};
234
175
  /** Create context */
235
176
  const context = new RabbitmqContext({
@@ -240,10 +181,11 @@ export class RabbitmqAdapter extends PlatformAdapter {
240
181
  operation,
241
182
  operationHandler,
242
183
  queue,
243
- channel,
184
+ consumer,
244
185
  message,
245
186
  content: undefined,
246
187
  headers,
188
+ reply,
247
189
  });
248
190
  try {
249
191
  /** Parse and decode `payload` */
@@ -255,8 +197,8 @@ export class RabbitmqAdapter extends PlatformAdapter {
255
197
  }
256
198
  // message.properties.
257
199
  /** Parse and decode `headers` */
258
- if (message.properties.headers) {
259
- for (const [k, v] of Object.entries(message.properties.headers)) {
200
+ if (message.headers) {
201
+ for (const [k, v] of Object.entries(message.headers)) {
260
202
  const header = operation.findHeader(k);
261
203
  const decode = this[kAssetCache].get(header, 'decode') || vg.isAny();
262
204
  headers[k] = decode(Buffer.isBuffer(v) ? v.toString() : v);
@@ -265,7 +207,6 @@ export class RabbitmqAdapter extends PlatformAdapter {
265
207
  context.content = content;
266
208
  }
267
209
  catch (e) {
268
- context.ack();
269
210
  this._emitError(e, context);
270
211
  return;
271
212
  }
@@ -273,6 +214,8 @@ export class RabbitmqAdapter extends PlatformAdapter {
273
214
  try {
274
215
  /** Call operation handler */
275
216
  const result = await operationHandler.call(instance, context);
217
+ if (result !== undefined)
218
+ await reply(result);
276
219
  await this.emitAsync('finish', context, result).catch(noOp);
277
220
  }
278
221
  catch (e) {
@@ -281,11 +224,13 @@ export class RabbitmqAdapter extends PlatformAdapter {
281
224
  };
282
225
  }
283
226
  async _parseContent(msg) {
284
- if (!msg.content?.length)
227
+ if (!Buffer.isBuffer(msg.body))
228
+ return msg.body;
229
+ if (!msg.body?.length)
285
230
  return;
286
- let content = msg.content;
287
- if (msg.properties.contentEncoding) {
288
- switch (msg.properties.contentEncoding) {
231
+ let content = msg.body;
232
+ if (msg.contentEncoding) {
233
+ switch (msg.contentEncoding) {
289
234
  case 'gzip':
290
235
  case 'x-gzip': {
291
236
  content = await gunzipAsync(content);
@@ -311,12 +256,11 @@ export class RabbitmqAdapter extends PlatformAdapter {
311
256
  }
312
257
  }
313
258
  }
314
- const mediaType = msg.properties.contentType &&
315
- parseContentType(msg.properties.contentType || '');
316
- let charset = (mediaType?.parameters.charset || '').toLowerCase();
317
- if (!charset && typeIs.is(mediaType?.type, ['json', 'xml', 'txt']))
318
- charset = 'utf-8';
319
- if (charset) {
259
+ const mediaType = msg.contentType
260
+ ? parseContentType(msg.contentType || '')
261
+ : undefined;
262
+ if (mediaType && typeIs.is(mediaType.type, ['json', 'xml', 'txt'])) {
263
+ const charset = (mediaType.parameters.charset || '').toLowerCase() || 'utf-8';
320
264
  content = iconv.decode(content, charset);
321
265
  if (typeIs.is(mediaType.type, ['json']))
322
266
  return JSON.parse(content);
@@ -15,7 +15,6 @@ export class RabbitmqContext extends ExecutionContext {
15
15
  documentNode: init.controller?.node,
16
16
  protocol: 'rpc',
17
17
  });
18
- this._ackSent = false;
19
18
  this.adapter = init.adapter;
20
19
  this.platform = init.adapter.platform;
21
20
  this.protocol = 'rpc';
@@ -27,28 +26,11 @@ export class RabbitmqContext extends ExecutionContext {
27
26
  this.operation = init.operation;
28
27
  if (init.operationHandler)
29
28
  this.operationHandler = init.operationHandler;
30
- this.channel = init.channel;
29
+ this.consumer = init.consumer;
31
30
  this.queue = init.queue;
32
31
  this.message = init.message;
33
32
  this.headers = init.headers || {};
34
33
  this.content = init.content;
35
- }
36
- get properties() {
37
- return this.message.properties;
38
- }
39
- get fields() {
40
- return this.message.fields;
41
- }
42
- ack() {
43
- if (this._ackSent)
44
- return;
45
- this._ackSent = true;
46
- this.channel.ack(this.message);
47
- }
48
- nack() {
49
- if (this._ackSent)
50
- return;
51
- this._ackSent = true;
52
- this.channel.nack(this.message);
34
+ this.reply = init.reply;
53
35
  }
54
36
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@opra/rabbitmq",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "description": "Opra RabbitMQ package",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
8
+ "@jsopen/objects": "^1.5.2",
8
9
  "@browsery/type-is": "^1.6.18-r8",
9
10
  "content-type": "^1.0.5",
10
11
  "iconv-lite": "^0.6.3",
@@ -13,10 +14,9 @@
13
14
  "valgen": "^5.15.0"
14
15
  },
15
16
  "peerDependencies": {
16
- "@opra/common": "^1.16.1",
17
- "@opra/core": "^1.16.1",
18
- "amqp-connection-manager": "^4.1.14",
19
- "amqplib": "^0.10.7"
17
+ "@opra/common": "^1.17.0",
18
+ "@opra/core": "^1.17.0",
19
+ "rabbitmq-client": ">=5.0.0 <6"
20
20
  },
21
21
  "type": "module",
22
22
  "exports": {
@@ -55,8 +55,6 @@
55
55
  "keywords": [
56
56
  "opra",
57
57
  "rabbitmq",
58
- "amqp",
59
- "amqplib",
60
58
  "message",
61
59
  "queue",
62
60
  "consumer"
@@ -2,6 +2,6 @@ import '@opra/core';
2
2
  import { RabbitmqAdapter } from '../rabbitmq-adapter.js';
3
3
  declare module '@opra/common' {
4
4
  interface RpcOperationDecorator {
5
- RabbitMQ(config: RabbitmqAdapter.OperationOptions | (() => RabbitmqAdapter.OperationOptions | Promise<RabbitmqAdapter.OperationOptions>)): this;
5
+ RabbitMQ(config: RabbitmqAdapter.ConsumerConfig | (() => RabbitmqAdapter.ConsumerConfig | Promise<RabbitmqAdapter.ConsumerConfig>)): this;
6
6
  }
7
7
  }
@@ -0,0 +1,29 @@
1
+ import { ApiDocument, RpcController, RpcOperation } from '@opra/common';
2
+ import type { RabbitmqAdapter } from './rabbitmq-adapter.js';
3
+ export declare class ConfigBuilder {
4
+ readonly document: ApiDocument;
5
+ readonly config: RabbitmqAdapter.Config;
6
+ connectionOptions: RabbitmqAdapter.ConnectionOptions;
7
+ controllerInstances: Map<RpcController, any>;
8
+ handlerArgs: ConfigBuilder.OperationArguments[];
9
+ constructor(document: ApiDocument, config: RabbitmqAdapter.Config);
10
+ build(): Promise<void>;
11
+ protected _prepareConnectionOptions(): void;
12
+ /**
13
+ *
14
+ * @param controller
15
+ * @param instance
16
+ * @param operation
17
+ * @protected
18
+ */
19
+ protected _getConsumerConfig(controller: RpcController, instance: any, operation: RpcOperation): Promise<RabbitmqAdapter.ConsumerConfig | undefined>;
20
+ }
21
+ export declare namespace ConfigBuilder {
22
+ interface OperationArguments {
23
+ controller: RpcController;
24
+ instance: any;
25
+ operation: RpcOperation;
26
+ consumerConfig: RabbitmqAdapter.ConsumerConfig;
27
+ topics: string[];
28
+ }
29
+ }
@@ -1,21 +1,9 @@
1
- import { ApiDocument, OpraException, OpraSchema, RpcApi, RpcController, RpcOperation } from '@opra/common';
1
+ import { ApiDocument, OpraException, OpraSchema, RpcApi, RpcController } from '@opra/common';
2
2
  import { PlatformAdapter } from '@opra/core';
3
- import { type AmqpConnectionManager, type AmqpConnectionManagerOptions, type ChannelWrapper, type ConnectionUrl } from 'amqp-connection-manager';
4
- import amqplib from 'amqplib';
5
- import { ConsumeMessage } from 'amqplib/properties';
3
+ import * as rabbit from 'rabbitmq-client';
4
+ import type { Envelope, MessageBody } from 'rabbitmq-client/lib/codec';
5
+ import { ConfigBuilder } from './config-builder.js';
6
6
  import { RabbitmqContext } from './rabbitmq-context.js';
7
- export interface OperationConfig {
8
- consumer: amqplib.Options.Consume & {};
9
- }
10
- interface HandlerArguments {
11
- consumer: amqplib.Options.Consume & {};
12
- controller: RpcController;
13
- instance: any;
14
- operation: RpcOperation;
15
- operationConfig: OperationConfig;
16
- handler: (channel: ChannelWrapper, queue: string, msg: ConsumeMessage | null) => void | Promise<void>;
17
- topics: string[];
18
- }
19
7
  /**
20
8
  *
21
9
  * @class RabbitmqAdapter
@@ -24,7 +12,8 @@ export declare class RabbitmqAdapter extends PlatformAdapter<RabbitmqAdapter.Eve
24
12
  static readonly PlatformName = "rabbitmq";
25
13
  protected _config: RabbitmqAdapter.Config;
26
14
  protected _controllerInstances: Map<RpcController, any>;
27
- protected _client?: AmqpConnectionManager;
15
+ protected _client?: rabbit.Connection;
16
+ protected _consumers: rabbit.Consumer[];
28
17
  protected _status: RabbitmqAdapter.Status;
29
18
  readonly protocol: OpraSchema.Transport;
30
19
  readonly platform = "rabbitmq";
@@ -48,21 +37,13 @@ export declare class RabbitmqAdapter extends PlatformAdapter<RabbitmqAdapter.Eve
48
37
  */
49
38
  close(): Promise<void>;
50
39
  getControllerInstance<T>(controllerPath: string): T | undefined;
51
- /**
52
- *
53
- * @param controller
54
- * @param instance
55
- * @param operation
56
- * @protected
57
- */
58
- protected _getOperationConfig(controller: RpcController, instance: any, operation: RpcOperation): Promise<OperationConfig | undefined>;
59
40
  /**
60
41
  *
61
42
  * @param args
62
43
  * @protected
63
44
  */
64
- protected _createHandler(args: HandlerArguments): void;
65
- protected _parseContent(msg: ConsumeMessage): Promise<any>;
45
+ protected _createHandler(args: ConfigBuilder.OperationArguments): (consumer: rabbit.Consumer, queue: string, message: rabbit.AsyncMessage, _reply: RabbitmqAdapter.ReplyFunction) => Promise<void>;
46
+ protected _parseContent(msg: rabbit.AsyncMessage): Promise<any>;
66
47
  protected _emitError(error: any, context?: RabbitmqContext): void;
67
48
  protected _wrapExceptions(exceptions: any[]): OpraException[];
68
49
  }
@@ -71,26 +52,22 @@ export declare class RabbitmqAdapter extends PlatformAdapter<RabbitmqAdapter.Eve
71
52
  */
72
53
  export declare namespace RabbitmqAdapter {
73
54
  type NextCallback = () => Promise<any>;
55
+ type ReplyFunction = (body: MessageBody, envelope?: Envelope) => Promise<void>;
74
56
  type Status = 'idle' | 'starting' | 'started';
75
- interface ConnectionOptions extends AmqpConnectionManagerOptions {
76
- urls?: ConnectionUrl[];
57
+ interface ConnectionOptions extends Pick<rabbit.ConnectionOptions, 'acquireTimeout' | 'connectionName' | 'frameMax' | 'heartbeat' | 'maxChannels' | 'retryHigh' | 'retryLow' | 'tls' | 'socket'> {
58
+ urls?: string[];
59
+ }
60
+ interface ConsumerConfig extends Pick<rabbit.ConsumerProps, 'concurrency' | 'requeue' | 'qos' | 'queueOptions' | 'exchanges' | 'exchangeBindings' | 'exclusive'> {
77
61
  }
78
62
  interface Config extends PlatformAdapter.Options {
79
- connection: string | string[] | ConnectionOptions;
80
- queues?: Record<string, amqplib.Options.AssertQueue>;
63
+ connection?: string | string[] | ConnectionOptions;
81
64
  defaults?: {
82
- consumer?: amqplib.Options.Consume;
65
+ consumer?: ConsumerConfig;
83
66
  };
84
67
  scope?: string;
85
68
  interceptors?: (InterceptorFunction | IRabbitmqInterceptor)[];
86
69
  logExtra?: boolean;
87
70
  }
88
- interface OperationOptions {
89
- /**
90
- * ConsumerConfig
91
- */
92
- consumer?: amqplib.Options.Consume;
93
- }
94
71
  /**
95
72
  * @type InterceptorFunction
96
73
  */
@@ -105,7 +82,6 @@ export declare namespace RabbitmqAdapter {
105
82
  error: [Error, RabbitmqContext | undefined];
106
83
  execute: [RabbitmqContext];
107
84
  finish: [RabbitmqContext, any];
108
- message: [ConsumeMessage, string];
85
+ message: [rabbit.AsyncMessage, string];
109
86
  }
110
87
  }
111
- export {};
@@ -1,15 +1,13 @@
1
1
  import { OpraSchema, RpcController, RpcOperation } from '@opra/common';
2
2
  import { ExecutionContext } from '@opra/core';
3
- import type { ChannelWrapper } from 'amqp-connection-manager';
4
- import type { ConsumeMessage } from 'amqplib/properties';
5
3
  import type { AsyncEventEmitter } from 'node-events-async';
4
+ import * as rabbit from 'rabbitmq-client';
6
5
  import type { RabbitmqAdapter } from './rabbitmq-adapter';
7
6
  /**
8
7
  * RabbitmqContext class provides the context for handling RabbitMQ messages.
9
8
  * It extends the ExecutionContext and implements the AsyncEventEmitter.
10
9
  */
11
10
  export declare class RabbitmqContext extends ExecutionContext implements AsyncEventEmitter {
12
- private _ackSent;
13
11
  readonly protocol: OpraSchema.Transport;
14
12
  readonly platform: string;
15
13
  readonly adapter: RabbitmqAdapter;
@@ -18,24 +16,21 @@ export declare class RabbitmqContext extends ExecutionContext implements AsyncEv
18
16
  readonly operation?: RpcOperation;
19
17
  readonly operationHandler?: Function;
20
18
  readonly queue: string;
21
- readonly channel: ChannelWrapper;
22
- readonly message: ConsumeMessage;
19
+ readonly consumer: rabbit.Consumer;
20
+ readonly message: rabbit.AsyncMessage;
23
21
  readonly content: any;
24
22
  readonly headers: Record<string, any>;
23
+ readonly reply: RabbitmqAdapter.ReplyFunction;
25
24
  /**
26
25
  * Constructor
27
26
  * @param init the context options
28
27
  */
29
28
  constructor(init: RabbitmqContext.Initiator);
30
- get properties(): import("amqplib/properties").MessageProperties;
31
- get fields(): import("amqplib/properties").ConsumeMessageFields;
32
- ack(): void;
33
- nack(): void;
34
29
  }
35
30
  export declare namespace RabbitmqContext {
36
31
  interface Initiator extends Omit<ExecutionContext.Initiator, 'document' | 'protocol' | 'documentNode'> {
37
32
  adapter: RabbitmqAdapter;
38
- channel: ChannelWrapper;
33
+ consumer: rabbit.Consumer;
39
34
  controller?: RpcController;
40
35
  controllerInstance?: any;
41
36
  operation?: RpcOperation;
@@ -43,6 +38,7 @@ export declare namespace RabbitmqContext {
43
38
  content: any;
44
39
  headers: Record<string, any>;
45
40
  queue: string;
46
- message: ConsumeMessage;
41
+ message: rabbit.AsyncMessage;
42
+ reply: RabbitmqAdapter.ReplyFunction;
47
43
  }
48
44
  }