@opra/rabbitmq 1.16.0 → 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,99 +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
- const channel = this._client.createChannel();
126
- for (const topic of args.topics) {
127
- const opts = this._config.queues?.[topic];
128
- if (opts)
129
- await channel.assertQueue(topic, opts);
130
- }
131
- for (const topic of args.topics) {
132
- await channel.assertQueue(topic);
133
- await channel
134
- .consume(topic, async (msg) => {
135
- if (!msg)
136
- return;
137
- await this.emitAsync('message', msg, topic).catch(noOp);
138
- try {
139
- await args.handler(channel, topic, msg);
140
- }
141
- catch (e) {
142
- this._emitError(e);
143
- }
144
- },
145
- /** Consume options */
146
- args.operationConfig.consumer)
147
- .catch(e => {
148
- this._emitError(e);
149
- throw e;
150
- });
151
- this.logger?.info?.(`Subscribed to topic${args.topics.length > 1 ? 's' : ''} "${args.topics}"`);
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) {
104
+ this._emitError(e);
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
+ }));
152
118
  }
153
119
  }
120
+ await Promise.all(promises);
154
121
  this._status = 'started';
155
122
  }
156
- catch (e) {
123
+ catch (err) {
124
+ this._emitError(err);
157
125
  await this.close();
158
- throw e;
159
126
  }
160
127
  }
161
128
  /**
162
129
  * Closes all connections and stops the service
163
130
  */
164
131
  async close() {
132
+ if (this._consumers.length)
133
+ await Promise.allSettled(this._consumers.map(consumer => consumer.close()));
165
134
  await this._client?.close();
166
135
  this._client = undefined;
136
+ this._consumers = [];
167
137
  this._controllerInstances.clear();
168
138
  this._status = 'idle';
169
139
  }
@@ -171,43 +141,6 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
171
141
  const controller = this.api.findController(controllerPath);
172
142
  return controller && this._controllerInstances.get(controller);
173
143
  }
174
- /**
175
- *
176
- * @param controller
177
- * @param instance
178
- * @param operation
179
- * @protected
180
- */
181
- async _getOperationConfig(controller, instance, operation) {
182
- if (typeof instance[operation.name] !== 'function')
183
- return;
184
- const proto = controller.ctor?.prototype || Object.getPrototypeOf(instance);
185
- if (Reflect.hasMetadata(common_1.RPC_CONTROLLER_METADATA, proto, operation.name))
186
- return;
187
- const operationConfig = {
188
- consumer: {
189
- noAck: true,
190
- },
191
- };
192
- if (this._config.defaults) {
193
- if (this._config.defaults.consumer) {
194
- Object.assign(operationConfig.consumer, this._config.defaults.consumer);
195
- }
196
- }
197
- let metadata = Reflect.getMetadata(constants_js_1.RMQ_OPERATION_METADATA, proto, operation.name);
198
- if (!metadata) {
199
- const configResolver = Reflect.getMetadata(constants_js_1.RMQ_OPERATION_METADATA_RESOLVER, proto, operation.name);
200
- if (configResolver) {
201
- metadata = await configResolver();
202
- }
203
- }
204
- if (metadata) {
205
- if (typeof metadata.consumer === 'object') {
206
- Object.assign(operationConfig.consumer, metadata.consumer);
207
- }
208
- }
209
- return operationConfig;
210
- }
211
144
  /**
212
145
  *
213
146
  * @param args
@@ -231,10 +164,17 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
231
164
  this[core_1.kAssetCache].set(header, 'decode', decode);
232
165
  }
233
166
  });
234
- args.handler = async (channel, queue, message) => {
167
+ return async (consumer, queue, message, _reply) => {
235
168
  if (!message)
236
169
  return;
237
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
+ };
238
178
  const headers = {};
239
179
  /** Create context */
240
180
  const context = new rabbitmq_context_js_1.RabbitmqContext({
@@ -245,10 +185,11 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
245
185
  operation,
246
186
  operationHandler,
247
187
  queue,
248
- channel,
188
+ consumer,
249
189
  message,
250
190
  content: undefined,
251
191
  headers,
192
+ reply,
252
193
  });
253
194
  try {
254
195
  /** Parse and decode `payload` */
@@ -260,8 +201,8 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
260
201
  }
261
202
  // message.properties.
262
203
  /** Parse and decode `headers` */
263
- if (message.properties.headers) {
264
- for (const [k, v] of Object.entries(message.properties.headers)) {
204
+ if (message.headers) {
205
+ for (const [k, v] of Object.entries(message.headers)) {
265
206
  const header = operation.findHeader(k);
266
207
  const decode = this[core_1.kAssetCache].get(header, 'decode') || valgen_1.vg.isAny();
267
208
  headers[k] = decode(Buffer.isBuffer(v) ? v.toString() : v);
@@ -270,7 +211,6 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
270
211
  context.content = content;
271
212
  }
272
213
  catch (e) {
273
- context.ack();
274
214
  this._emitError(e, context);
275
215
  return;
276
216
  }
@@ -278,6 +218,8 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
278
218
  try {
279
219
  /** Call operation handler */
280
220
  const result = await operationHandler.call(instance, context);
221
+ if (result !== undefined)
222
+ await reply(result);
281
223
  await this.emitAsync('finish', context, result).catch(noOp);
282
224
  }
283
225
  catch (e) {
@@ -286,11 +228,13 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
286
228
  };
287
229
  }
288
230
  async _parseContent(msg) {
289
- if (!msg.content?.length)
231
+ if (!Buffer.isBuffer(msg.body))
232
+ return msg.body;
233
+ if (!msg.body?.length)
290
234
  return;
291
- let content = msg.content;
292
- if (msg.properties.contentEncoding) {
293
- switch (msg.properties.contentEncoding) {
235
+ let content = msg.body;
236
+ if (msg.contentEncoding) {
237
+ switch (msg.contentEncoding) {
294
238
  case 'gzip':
295
239
  case 'x-gzip': {
296
240
  content = await gunzipAsync(content);
@@ -316,12 +260,11 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
316
260
  }
317
261
  }
318
262
  }
319
- const mediaType = msg.properties.contentType &&
320
- (0, content_type_1.parse)(msg.properties.contentType || '');
321
- let charset = (mediaType?.parameters.charset || '').toLowerCase();
322
- if (!charset && type_is_1.default.is(mediaType?.type, ['json', 'xml', 'txt']))
323
- charset = 'utf-8';
324
- 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';
325
268
  content = iconv_lite_1.default.decode(content, charset);
326
269
  if (type_is_1.default.is(mediaType.type, ['json']))
327
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,99 +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
- const channel = this._client.createChannel();
122
- for (const topic of args.topics) {
123
- const opts = this._config.queues?.[topic];
124
- if (opts)
125
- await channel.assertQueue(topic, opts);
126
- }
127
- for (const topic of args.topics) {
128
- await channel.assertQueue(topic);
129
- await channel
130
- .consume(topic, async (msg) => {
131
- if (!msg)
132
- return;
133
- await this.emitAsync('message', msg, topic).catch(noOp);
134
- try {
135
- await args.handler(channel, topic, msg);
136
- }
137
- catch (e) {
138
- this._emitError(e);
139
- }
140
- },
141
- /** Consume options */
142
- args.operationConfig.consumer)
143
- .catch(e => {
144
- this._emitError(e);
145
- throw e;
146
- });
147
- this.logger?.info?.(`Subscribed to topic${args.topics.length > 1 ? 's' : ''} "${args.topics}"`);
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) {
100
+ this._emitError(e);
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
+ }));
148
114
  }
149
115
  }
116
+ await Promise.all(promises);
150
117
  this._status = 'started';
151
118
  }
152
- catch (e) {
119
+ catch (err) {
120
+ this._emitError(err);
153
121
  await this.close();
154
- throw e;
155
122
  }
156
123
  }
157
124
  /**
158
125
  * Closes all connections and stops the service
159
126
  */
160
127
  async close() {
128
+ if (this._consumers.length)
129
+ await Promise.allSettled(this._consumers.map(consumer => consumer.close()));
161
130
  await this._client?.close();
162
131
  this._client = undefined;
132
+ this._consumers = [];
163
133
  this._controllerInstances.clear();
164
134
  this._status = 'idle';
165
135
  }
@@ -167,43 +137,6 @@ export class RabbitmqAdapter extends PlatformAdapter {
167
137
  const controller = this.api.findController(controllerPath);
168
138
  return controller && this._controllerInstances.get(controller);
169
139
  }
170
- /**
171
- *
172
- * @param controller
173
- * @param instance
174
- * @param operation
175
- * @protected
176
- */
177
- async _getOperationConfig(controller, instance, operation) {
178
- if (typeof instance[operation.name] !== 'function')
179
- return;
180
- const proto = controller.ctor?.prototype || Object.getPrototypeOf(instance);
181
- if (Reflect.hasMetadata(RPC_CONTROLLER_METADATA, proto, operation.name))
182
- return;
183
- const operationConfig = {
184
- consumer: {
185
- noAck: true,
186
- },
187
- };
188
- if (this._config.defaults) {
189
- if (this._config.defaults.consumer) {
190
- Object.assign(operationConfig.consumer, this._config.defaults.consumer);
191
- }
192
- }
193
- let metadata = Reflect.getMetadata(RMQ_OPERATION_METADATA, proto, operation.name);
194
- if (!metadata) {
195
- const configResolver = Reflect.getMetadata(RMQ_OPERATION_METADATA_RESOLVER, proto, operation.name);
196
- if (configResolver) {
197
- metadata = await configResolver();
198
- }
199
- }
200
- if (metadata) {
201
- if (typeof metadata.consumer === 'object') {
202
- Object.assign(operationConfig.consumer, metadata.consumer);
203
- }
204
- }
205
- return operationConfig;
206
- }
207
140
  /**
208
141
  *
209
142
  * @param args
@@ -227,10 +160,17 @@ export class RabbitmqAdapter extends PlatformAdapter {
227
160
  this[kAssetCache].set(header, 'decode', decode);
228
161
  }
229
162
  });
230
- args.handler = async (channel, queue, message) => {
163
+ return async (consumer, queue, message, _reply) => {
231
164
  if (!message)
232
165
  return;
233
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
+ };
234
174
  const headers = {};
235
175
  /** Create context */
236
176
  const context = new RabbitmqContext({
@@ -241,10 +181,11 @@ export class RabbitmqAdapter extends PlatformAdapter {
241
181
  operation,
242
182
  operationHandler,
243
183
  queue,
244
- channel,
184
+ consumer,
245
185
  message,
246
186
  content: undefined,
247
187
  headers,
188
+ reply,
248
189
  });
249
190
  try {
250
191
  /** Parse and decode `payload` */
@@ -256,8 +197,8 @@ export class RabbitmqAdapter extends PlatformAdapter {
256
197
  }
257
198
  // message.properties.
258
199
  /** Parse and decode `headers` */
259
- if (message.properties.headers) {
260
- for (const [k, v] of Object.entries(message.properties.headers)) {
200
+ if (message.headers) {
201
+ for (const [k, v] of Object.entries(message.headers)) {
261
202
  const header = operation.findHeader(k);
262
203
  const decode = this[kAssetCache].get(header, 'decode') || vg.isAny();
263
204
  headers[k] = decode(Buffer.isBuffer(v) ? v.toString() : v);
@@ -266,7 +207,6 @@ export class RabbitmqAdapter extends PlatformAdapter {
266
207
  context.content = content;
267
208
  }
268
209
  catch (e) {
269
- context.ack();
270
210
  this._emitError(e, context);
271
211
  return;
272
212
  }
@@ -274,6 +214,8 @@ export class RabbitmqAdapter extends PlatformAdapter {
274
214
  try {
275
215
  /** Call operation handler */
276
216
  const result = await operationHandler.call(instance, context);
217
+ if (result !== undefined)
218
+ await reply(result);
277
219
  await this.emitAsync('finish', context, result).catch(noOp);
278
220
  }
279
221
  catch (e) {
@@ -282,11 +224,13 @@ export class RabbitmqAdapter extends PlatformAdapter {
282
224
  };
283
225
  }
284
226
  async _parseContent(msg) {
285
- if (!msg.content?.length)
227
+ if (!Buffer.isBuffer(msg.body))
228
+ return msg.body;
229
+ if (!msg.body?.length)
286
230
  return;
287
- let content = msg.content;
288
- if (msg.properties.contentEncoding) {
289
- switch (msg.properties.contentEncoding) {
231
+ let content = msg.body;
232
+ if (msg.contentEncoding) {
233
+ switch (msg.contentEncoding) {
290
234
  case 'gzip':
291
235
  case 'x-gzip': {
292
236
  content = await gunzipAsync(content);
@@ -312,12 +256,11 @@ export class RabbitmqAdapter extends PlatformAdapter {
312
256
  }
313
257
  }
314
258
  }
315
- const mediaType = msg.properties.contentType &&
316
- parseContentType(msg.properties.contentType || '');
317
- let charset = (mediaType?.parameters.charset || '').toLowerCase();
318
- if (!charset && typeIs.is(mediaType?.type, ['json', 'xml', 'txt']))
319
- charset = 'utf-8';
320
- 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';
321
264
  content = iconv.decode(content, charset);
322
265
  if (typeIs.is(mediaType.type, ['json']))
323
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.0",
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.0",
17
- "@opra/core": "^1.16.0",
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
  }