@opra/rabbitmq 1.16.1 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 => {
@@ -58,6 +58,9 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
58
58
  get api() {
59
59
  return this.document.rpcApi;
60
60
  }
61
+ get connection() {
62
+ return this._connection;
63
+ }
61
64
  get scope() {
62
65
  return this._config.scope;
63
66
  }
@@ -71,98 +74,69 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
71
74
  if (this.status !== 'idle')
72
75
  return;
73
76
  this._status = 'starting';
74
- const handlerArgs = [];
77
+ // const handlerArgs: HandlerArguments[] = [];
78
+ const configBuilder = new config_builder_js_1.ConfigBuilder(this.document, this._config);
79
+ await configBuilder.build();
80
+ this._connection = new rabbit.Connection(configBuilder.connectionOptions);
75
81
  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;
82
+ /** Establish connection */
83
+ await this._connection.onConnect().catch(e => {
84
+ e.message = `RabbitMQ connection error. ${e.message}`;
120
85
  throw e;
121
86
  });
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 => {
87
+ this.logger?.info?.(`Connected RabbitMQ at ${configBuilder.connectionOptions.urls}`);
88
+ /** Subscribe to queues */
89
+ const promises = [];
90
+ for (const args of configBuilder.handlerArgs) {
91
+ for (const queue of args.topics) {
92
+ const handler = this._createHandler(args);
93
+ promises.push(new Promise((resolve, reject) => {
94
+ const consumer = this._connection.createConsumer({
95
+ ...args.consumerConfig,
96
+ queueOptions: {
97
+ ...args.consumerConfig.queueOptions,
98
+ durable: args.consumerConfig.queueOptions?.durable ?? true,
99
+ },
100
+ queue,
101
+ }, async (msg, reply) => {
102
+ await this.emitAsync('message', msg, queue).catch(noOp);
103
+ try {
104
+ await handler(consumer, queue, msg, reply);
105
+ }
106
+ catch (e) {
145
107
  this._emitError(e);
146
- throw e;
147
- });
148
- this.logger?.info?.(`Subscribed to topic${args.topics.length > 1 ? 's' : ''} "${args.topics}"`);
149
- }
150
- },
151
- });
108
+ }
109
+ });
110
+ this._consumers.push(consumer);
111
+ consumer.on('ready', () => {
112
+ this.logger?.info?.(`Subscribed to topic "${queue}"`);
113
+ resolve();
114
+ });
115
+ consumer.on('error', (err) => {
116
+ err.message = `Consumer error (${queue})". ${err.message}`;
117
+ err.queue = queue;
118
+ reject(err);
119
+ });
120
+ }));
121
+ }
152
122
  }
123
+ await Promise.all(promises);
153
124
  this._status = 'started';
154
125
  }
155
- catch (e) {
126
+ catch (err) {
127
+ this._emitError(err);
156
128
  await this.close();
157
- throw e;
158
129
  }
159
130
  }
160
131
  /**
161
132
  * Closes all connections and stops the service
162
133
  */
163
134
  async close() {
164
- await this._client?.close();
165
- this._client = undefined;
135
+ if (this._consumers.length)
136
+ await Promise.allSettled(this._consumers.map(consumer => consumer.close()));
137
+ await this._connection?.close();
138
+ this._connection = undefined;
139
+ this._consumers = [];
166
140
  this._controllerInstances.clear();
167
141
  this._status = 'idle';
168
142
  }
@@ -170,43 +144,6 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
170
144
  const controller = this.api.findController(controllerPath);
171
145
  return controller && this._controllerInstances.get(controller);
172
146
  }
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
147
  /**
211
148
  *
212
149
  * @param args
@@ -230,10 +167,17 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
230
167
  this[core_1.kAssetCache].set(header, 'decode', decode);
231
168
  }
232
169
  });
233
- args.handler = async (channel, queue, message) => {
170
+ return async (consumer, queue, message, _reply) => {
234
171
  if (!message)
235
172
  return;
236
173
  const operationHandler = instance[operation.name];
174
+ let replyCalled = false;
175
+ const reply = async (body, envelope) => {
176
+ if (replyCalled)
177
+ return;
178
+ replyCalled = true;
179
+ return _reply(body, envelope);
180
+ };
237
181
  const headers = {};
238
182
  /** Create context */
239
183
  const context = new rabbitmq_context_js_1.RabbitmqContext({
@@ -244,10 +188,11 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
244
188
  operation,
245
189
  operationHandler,
246
190
  queue,
247
- channel,
191
+ consumer,
248
192
  message,
249
193
  content: undefined,
250
194
  headers,
195
+ reply,
251
196
  });
252
197
  try {
253
198
  /** Parse and decode `payload` */
@@ -259,8 +204,8 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
259
204
  }
260
205
  // message.properties.
261
206
  /** Parse and decode `headers` */
262
- if (message.properties.headers) {
263
- for (const [k, v] of Object.entries(message.properties.headers)) {
207
+ if (message.headers) {
208
+ for (const [k, v] of Object.entries(message.headers)) {
264
209
  const header = operation.findHeader(k);
265
210
  const decode = this[core_1.kAssetCache].get(header, 'decode') || valgen_1.vg.isAny();
266
211
  headers[k] = decode(Buffer.isBuffer(v) ? v.toString() : v);
@@ -269,7 +214,6 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
269
214
  context.content = content;
270
215
  }
271
216
  catch (e) {
272
- context.ack();
273
217
  this._emitError(e, context);
274
218
  return;
275
219
  }
@@ -277,6 +221,8 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
277
221
  try {
278
222
  /** Call operation handler */
279
223
  const result = await operationHandler.call(instance, context);
224
+ if (result !== undefined)
225
+ await reply(result);
280
226
  await this.emitAsync('finish', context, result).catch(noOp);
281
227
  }
282
228
  catch (e) {
@@ -285,11 +231,13 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
285
231
  };
286
232
  }
287
233
  async _parseContent(msg) {
288
- if (!msg.content?.length)
234
+ if (!Buffer.isBuffer(msg.body))
235
+ return msg.body;
236
+ if (!msg.body?.length)
289
237
  return;
290
- let content = msg.content;
291
- if (msg.properties.contentEncoding) {
292
- switch (msg.properties.contentEncoding) {
238
+ let content = msg.body;
239
+ if (msg.contentEncoding) {
240
+ switch (msg.contentEncoding) {
293
241
  case 'gzip':
294
242
  case 'x-gzip': {
295
243
  content = await gunzipAsync(content);
@@ -315,12 +263,11 @@ class RabbitmqAdapter extends core_1.PlatformAdapter {
315
263
  }
316
264
  }
317
265
  }
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) {
266
+ const mediaType = msg.contentType
267
+ ? (0, content_type_1.parse)(msg.contentType || '')
268
+ : undefined;
269
+ if (mediaType && type_is_1.default.is(mediaType.type, ['json', 'xml', 'txt'])) {
270
+ const charset = (mediaType.parameters.charset || '').toLowerCase() || 'utf-8';
324
271
  content = iconv_lite_1.default.decode(content, charset);
325
272
  if (type_is_1.default.is(mediaType.type, ['json']))
326
273
  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 => {
@@ -54,6 +54,9 @@ export class RabbitmqAdapter extends PlatformAdapter {
54
54
  get api() {
55
55
  return this.document.rpcApi;
56
56
  }
57
+ get connection() {
58
+ return this._connection;
59
+ }
57
60
  get scope() {
58
61
  return this._config.scope;
59
62
  }
@@ -67,98 +70,69 @@ export class RabbitmqAdapter extends PlatformAdapter {
67
70
  if (this.status !== 'idle')
68
71
  return;
69
72
  this._status = 'starting';
70
- const handlerArgs = [];
73
+ // const handlerArgs: HandlerArguments[] = [];
74
+ const configBuilder = new ConfigBuilder(this.document, this._config);
75
+ await configBuilder.build();
76
+ this._connection = new rabbit.Connection(configBuilder.connectionOptions);
71
77
  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;
78
+ /** Establish connection */
79
+ await this._connection.onConnect().catch(e => {
80
+ e.message = `RabbitMQ connection error. ${e.message}`;
116
81
  throw e;
117
82
  });
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 => {
83
+ this.logger?.info?.(`Connected RabbitMQ at ${configBuilder.connectionOptions.urls}`);
84
+ /** Subscribe to queues */
85
+ const promises = [];
86
+ for (const args of configBuilder.handlerArgs) {
87
+ for (const queue of args.topics) {
88
+ const handler = this._createHandler(args);
89
+ promises.push(new Promise((resolve, reject) => {
90
+ const consumer = this._connection.createConsumer({
91
+ ...args.consumerConfig,
92
+ queueOptions: {
93
+ ...args.consumerConfig.queueOptions,
94
+ durable: args.consumerConfig.queueOptions?.durable ?? true,
95
+ },
96
+ queue,
97
+ }, async (msg, reply) => {
98
+ await this.emitAsync('message', msg, queue).catch(noOp);
99
+ try {
100
+ await handler(consumer, queue, msg, reply);
101
+ }
102
+ catch (e) {
141
103
  this._emitError(e);
142
- throw e;
143
- });
144
- this.logger?.info?.(`Subscribed to topic${args.topics.length > 1 ? 's' : ''} "${args.topics}"`);
145
- }
146
- },
147
- });
104
+ }
105
+ });
106
+ this._consumers.push(consumer);
107
+ consumer.on('ready', () => {
108
+ this.logger?.info?.(`Subscribed to topic "${queue}"`);
109
+ resolve();
110
+ });
111
+ consumer.on('error', (err) => {
112
+ err.message = `Consumer error (${queue})". ${err.message}`;
113
+ err.queue = queue;
114
+ reject(err);
115
+ });
116
+ }));
117
+ }
148
118
  }
119
+ await Promise.all(promises);
149
120
  this._status = 'started';
150
121
  }
151
- catch (e) {
122
+ catch (err) {
123
+ this._emitError(err);
152
124
  await this.close();
153
- throw e;
154
125
  }
155
126
  }
156
127
  /**
157
128
  * Closes all connections and stops the service
158
129
  */
159
130
  async close() {
160
- await this._client?.close();
161
- this._client = undefined;
131
+ if (this._consumers.length)
132
+ await Promise.allSettled(this._consumers.map(consumer => consumer.close()));
133
+ await this._connection?.close();
134
+ this._connection = undefined;
135
+ this._consumers = [];
162
136
  this._controllerInstances.clear();
163
137
  this._status = 'idle';
164
138
  }
@@ -166,43 +140,6 @@ export class RabbitmqAdapter extends PlatformAdapter {
166
140
  const controller = this.api.findController(controllerPath);
167
141
  return controller && this._controllerInstances.get(controller);
168
142
  }
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
143
  /**
207
144
  *
208
145
  * @param args
@@ -226,10 +163,17 @@ export class RabbitmqAdapter extends PlatformAdapter {
226
163
  this[kAssetCache].set(header, 'decode', decode);
227
164
  }
228
165
  });
229
- args.handler = async (channel, queue, message) => {
166
+ return async (consumer, queue, message, _reply) => {
230
167
  if (!message)
231
168
  return;
232
169
  const operationHandler = instance[operation.name];
170
+ let replyCalled = false;
171
+ const reply = async (body, envelope) => {
172
+ if (replyCalled)
173
+ return;
174
+ replyCalled = true;
175
+ return _reply(body, envelope);
176
+ };
233
177
  const headers = {};
234
178
  /** Create context */
235
179
  const context = new RabbitmqContext({
@@ -240,10 +184,11 @@ export class RabbitmqAdapter extends PlatformAdapter {
240
184
  operation,
241
185
  operationHandler,
242
186
  queue,
243
- channel,
187
+ consumer,
244
188
  message,
245
189
  content: undefined,
246
190
  headers,
191
+ reply,
247
192
  });
248
193
  try {
249
194
  /** Parse and decode `payload` */
@@ -255,8 +200,8 @@ export class RabbitmqAdapter extends PlatformAdapter {
255
200
  }
256
201
  // message.properties.
257
202
  /** Parse and decode `headers` */
258
- if (message.properties.headers) {
259
- for (const [k, v] of Object.entries(message.properties.headers)) {
203
+ if (message.headers) {
204
+ for (const [k, v] of Object.entries(message.headers)) {
260
205
  const header = operation.findHeader(k);
261
206
  const decode = this[kAssetCache].get(header, 'decode') || vg.isAny();
262
207
  headers[k] = decode(Buffer.isBuffer(v) ? v.toString() : v);
@@ -265,7 +210,6 @@ export class RabbitmqAdapter extends PlatformAdapter {
265
210
  context.content = content;
266
211
  }
267
212
  catch (e) {
268
- context.ack();
269
213
  this._emitError(e, context);
270
214
  return;
271
215
  }
@@ -273,6 +217,8 @@ export class RabbitmqAdapter extends PlatformAdapter {
273
217
  try {
274
218
  /** Call operation handler */
275
219
  const result = await operationHandler.call(instance, context);
220
+ if (result !== undefined)
221
+ await reply(result);
276
222
  await this.emitAsync('finish', context, result).catch(noOp);
277
223
  }
278
224
  catch (e) {
@@ -281,11 +227,13 @@ export class RabbitmqAdapter extends PlatformAdapter {
281
227
  };
282
228
  }
283
229
  async _parseContent(msg) {
284
- if (!msg.content?.length)
230
+ if (!Buffer.isBuffer(msg.body))
231
+ return msg.body;
232
+ if (!msg.body?.length)
285
233
  return;
286
- let content = msg.content;
287
- if (msg.properties.contentEncoding) {
288
- switch (msg.properties.contentEncoding) {
234
+ let content = msg.body;
235
+ if (msg.contentEncoding) {
236
+ switch (msg.contentEncoding) {
289
237
  case 'gzip':
290
238
  case 'x-gzip': {
291
239
  content = await gunzipAsync(content);
@@ -311,12 +259,11 @@ export class RabbitmqAdapter extends PlatformAdapter {
311
259
  }
312
260
  }
313
261
  }
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) {
262
+ const mediaType = msg.contentType
263
+ ? parseContentType(msg.contentType || '')
264
+ : undefined;
265
+ if (mediaType && typeIs.is(mediaType.type, ['json', 'xml', 'txt'])) {
266
+ const charset = (mediaType.parameters.charset || '').toLowerCase() || 'utf-8';
320
267
  content = iconv.decode(content, charset);
321
268
  if (typeIs.is(mediaType.type, ['json']))
322
269
  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.1",
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.1",
18
+ "@opra/core": "^1.17.1",
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 _connection?: rabbit.Connection;
16
+ protected _consumers: rabbit.Consumer[];
28
17
  protected _status: RabbitmqAdapter.Status;
29
18
  readonly protocol: OpraSchema.Transport;
30
19
  readonly platform = "rabbitmq";
@@ -37,6 +26,7 @@ export declare class RabbitmqAdapter extends PlatformAdapter<RabbitmqAdapter.Eve
37
26
  */
38
27
  constructor(document: ApiDocument, config: RabbitmqAdapter.Config);
39
28
  get api(): RpcApi;
29
+ get connection(): rabbit.Connection | undefined;
40
30
  get scope(): string | undefined;
41
31
  get status(): RabbitmqAdapter.Status;
42
32
  /**
@@ -48,21 +38,13 @@ export declare class RabbitmqAdapter extends PlatformAdapter<RabbitmqAdapter.Eve
48
38
  */
49
39
  close(): Promise<void>;
50
40
  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
41
  /**
60
42
  *
61
43
  * @param args
62
44
  * @protected
63
45
  */
64
- protected _createHandler(args: HandlerArguments): void;
65
- protected _parseContent(msg: ConsumeMessage): Promise<any>;
46
+ protected _createHandler(args: ConfigBuilder.OperationArguments): (consumer: rabbit.Consumer, queue: string, message: rabbit.AsyncMessage, _reply: RabbitmqAdapter.ReplyFunction) => Promise<void>;
47
+ protected _parseContent(msg: rabbit.AsyncMessage): Promise<any>;
66
48
  protected _emitError(error: any, context?: RabbitmqContext): void;
67
49
  protected _wrapExceptions(exceptions: any[]): OpraException[];
68
50
  }
@@ -71,26 +53,22 @@ export declare class RabbitmqAdapter extends PlatformAdapter<RabbitmqAdapter.Eve
71
53
  */
72
54
  export declare namespace RabbitmqAdapter {
73
55
  type NextCallback = () => Promise<any>;
56
+ type ReplyFunction = (body: MessageBody, envelope?: Envelope) => Promise<void>;
74
57
  type Status = 'idle' | 'starting' | 'started';
75
- interface ConnectionOptions extends AmqpConnectionManagerOptions {
76
- urls?: ConnectionUrl[];
58
+ interface ConnectionOptions extends Pick<rabbit.ConnectionOptions, 'username' | 'password' | 'acquireTimeout' | 'connectionName' | 'connectionTimeout' | 'frameMax' | 'heartbeat' | 'maxChannels' | 'retryHigh' | 'retryLow' | 'noDelay' | 'tls' | 'socket'> {
59
+ urls?: string[];
60
+ }
61
+ interface ConsumerConfig extends Pick<rabbit.ConsumerProps, 'concurrency' | 'requeue' | 'qos' | 'queueOptions' | 'exchanges' | 'exchangeBindings' | 'exclusive'> {
77
62
  }
78
63
  interface Config extends PlatformAdapter.Options {
79
- connection: string | string[] | ConnectionOptions;
80
- queues?: Record<string, amqplib.Options.AssertQueue>;
64
+ connection?: string | string[] | ConnectionOptions;
81
65
  defaults?: {
82
- consumer?: amqplib.Options.Consume;
66
+ consumer?: ConsumerConfig;
83
67
  };
84
68
  scope?: string;
85
69
  interceptors?: (InterceptorFunction | IRabbitmqInterceptor)[];
86
70
  logExtra?: boolean;
87
71
  }
88
- interface OperationOptions {
89
- /**
90
- * ConsumerConfig
91
- */
92
- consumer?: amqplib.Options.Consume;
93
- }
94
72
  /**
95
73
  * @type InterceptorFunction
96
74
  */
@@ -105,7 +83,6 @@ export declare namespace RabbitmqAdapter {
105
83
  error: [Error, RabbitmqContext | undefined];
106
84
  execute: [RabbitmqContext];
107
85
  finish: [RabbitmqContext, any];
108
- message: [ConsumeMessage, string];
86
+ message: [rabbit.AsyncMessage, string];
109
87
  }
110
88
  }
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
  }