@opra/rabbitmq 1.10.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Panates
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @opra/core
2
+
3
+ OPRA schema package.
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("@opra/core");
4
+ const common_1 = require("@opra/common");
5
+ const constants_js_1 = require("../constants.js");
6
+ /** Implementation **/
7
+ common_1.classes.RpcOperationDecoratorFactory.augment((decorator, decoratorChain) => {
8
+ decorator.RabbitMQ = (config) => {
9
+ decoratorChain.push((_, target, propertyKey) => {
10
+ if (typeof config === 'function') {
11
+ Reflect.defineMetadata(constants_js_1.RMQ_OPERATION_METADATA_RESOLVER, config, target, propertyKey);
12
+ }
13
+ else {
14
+ Reflect.defineMetadata(constants_js_1.RMQ_OPERATION_METADATA, { ...config }, target, propertyKey);
15
+ }
16
+ });
17
+ return decorator;
18
+ };
19
+ });
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RMQ_OPERATION_METADATA_RESOLVER = exports.RMQ_OPERATION_METADATA = exports.RMQ_DEFAULT_GROUP = void 0;
4
+ exports.RMQ_DEFAULT_GROUP = 'default';
5
+ exports.RMQ_OPERATION_METADATA = 'RMQ_OPERATION_METADATA';
6
+ exports.RMQ_OPERATION_METADATA_RESOLVER = 'RMQ_OPERATION_METADATA_RESOLVER';
package/cjs/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ require("./augmentation/opra-common.augmentation.js");
5
+ tslib_1.__exportStar(require("./constants.js"), exports);
6
+ tslib_1.__exportStar(require("./rabbitmq-adapter.js"), exports);
7
+ tslib_1.__exportStar(require("./rabbitmq-context.js"), exports);
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,358 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RabbitmqAdapter = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const node_zlib_1 = tslib_1.__importDefault(require("node:zlib"));
6
+ const type_is_1 = tslib_1.__importDefault(require("@browsery/type-is"));
7
+ const common_1 = require("@opra/common");
8
+ const core_1 = require("@opra/core");
9
+ const amqplib_1 = tslib_1.__importDefault(require("amqplib"));
10
+ const content_type_1 = require("content-type");
11
+ const iconv_lite_1 = tslib_1.__importDefault(require("iconv-lite"));
12
+ const util_1 = require("util");
13
+ const valgen_1 = require("valgen");
14
+ const constants_js_1 = require("./constants.js");
15
+ const rabbitmq_context_js_1 = require("./rabbitmq-context.js");
16
+ const gunzipAsync = (0, util_1.promisify)(node_zlib_1.default.gunzip);
17
+ const deflateAsync = (0, util_1.promisify)(node_zlib_1.default.deflate);
18
+ const inflateAsync = (0, util_1.promisify)(node_zlib_1.default.inflate);
19
+ const brotliAsync = (0, util_1.promisify)(node_zlib_1.default.brotliCompress);
20
+ const globalErrorTypes = ['unhandledRejection', 'uncaughtException'];
21
+ const signalTraps = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
22
+ const noOp = () => undefined;
23
+ /**
24
+ *
25
+ * @class RabbitmqAdapter
26
+ */
27
+ class RabbitmqAdapter extends core_1.PlatformAdapter {
28
+ /**
29
+ *
30
+ * @param document
31
+ * @param config
32
+ * @constructor
33
+ */
34
+ constructor(document, config) {
35
+ super(config);
36
+ this._controllerInstances = new Map();
37
+ this._connections = [];
38
+ this._status = 'idle';
39
+ this.protocol = 'rpc';
40
+ this.platform = RabbitmqAdapter.PlatformName;
41
+ this._document = document;
42
+ this._config = config;
43
+ if (!(this.document.api instanceof common_1.RpcApi &&
44
+ this.document.api.platform === RabbitmqAdapter.PlatformName)) {
45
+ throw new TypeError(`The document doesn't expose a RabbitMQ Api`);
46
+ }
47
+ // this._config = config;
48
+ this.interceptors = [...(config.interceptors || [])];
49
+ globalErrorTypes.forEach(type => {
50
+ process.on(type, e => {
51
+ this._emitError(e);
52
+ return this.close();
53
+ });
54
+ });
55
+ signalTraps.forEach(type => {
56
+ process.once(type, () => this.close());
57
+ });
58
+ }
59
+ get api() {
60
+ return this.document.rpcApi;
61
+ }
62
+ get scope() {
63
+ return this._config.scope;
64
+ }
65
+ get status() {
66
+ return this._status;
67
+ }
68
+ /**
69
+ * Starts the service
70
+ */
71
+ async start() {
72
+ if (this.status !== 'idle')
73
+ return;
74
+ this._status = 'starting';
75
+ const handlerArgs = [];
76
+ try {
77
+ /** Initialize consumers */
78
+ for (const controller of this.document.rpcApi.controllers.values()) {
79
+ let instance = controller.instance;
80
+ if (!instance && controller.ctor)
81
+ instance = new controller.ctor();
82
+ if (!instance)
83
+ continue;
84
+ this._controllerInstances.set(controller, instance);
85
+ /** Build HandlerData array */
86
+ for (const operation of controller.operations.values()) {
87
+ const operationConfig = await this._getOperationConfig(controller, instance, operation);
88
+ if (!operationConfig)
89
+ continue;
90
+ const args = {
91
+ consumer: null,
92
+ controller,
93
+ instance,
94
+ operation,
95
+ operationConfig,
96
+ handler: null,
97
+ topics: (Array.isArray(operation.channel)
98
+ ? operation.channel
99
+ : [operation.channel]).map(String),
100
+ };
101
+ this._createHandler(args);
102
+ handlerArgs.push(args);
103
+ }
104
+ }
105
+ /** Connect to server */
106
+ for (const connectionOptions of this._config.connection) {
107
+ const connection = await amqplib_1.default.connect(connectionOptions);
108
+ this._connections.push(connection);
109
+ const hostname = typeof connectionOptions === 'object'
110
+ ? connectionOptions.hostname
111
+ : connectionOptions;
112
+ this.logger?.info?.(`Connected to ${hostname}`);
113
+ /** Subscribe to channels */
114
+ for (const args of handlerArgs) {
115
+ /** Create channel per operation */
116
+ const channel = await connection.createChannel();
117
+ for (const topic of args.topics) {
118
+ const opts = this._config.queues?.[topic];
119
+ if (opts)
120
+ await channel.assertQueue(topic, opts);
121
+ }
122
+ for (const topic of args.topics) {
123
+ await channel.assertQueue(topic);
124
+ await channel
125
+ .consume(topic, async (msg) => {
126
+ if (!msg)
127
+ return;
128
+ await this.emitAsync('message', msg).catch(() => undefined);
129
+ try {
130
+ await args.handler(msg);
131
+ // channel.ack(msg);
132
+ }
133
+ catch (e) {
134
+ this._emitError(e);
135
+ }
136
+ await this.emitAsync('message-finish', msg);
137
+ },
138
+ /** Consume options */
139
+ args.operationConfig.consumer)
140
+ .catch(e => {
141
+ this._emitError(e);
142
+ throw e;
143
+ });
144
+ this.logger?.info?.(`Subscribed to topic${args.topics.length > 1 ? 's' : ''} "${args.topics}"`);
145
+ }
146
+ }
147
+ }
148
+ this._status = 'started';
149
+ }
150
+ catch (e) {
151
+ await this.close();
152
+ throw e;
153
+ }
154
+ }
155
+ // protected async _connect() {
156
+ // if (!this._connection)
157
+ // this._connection = await amqplib.connect(this._config.connection);
158
+ // return this._connection;
159
+ // }
160
+ /**
161
+ * Closes all connections and stops the service
162
+ */
163
+ async close() {
164
+ await Promise.all(this._connections.map(c => c.close()));
165
+ this._connections = [];
166
+ this._controllerInstances.clear();
167
+ this._status = 'idle';
168
+ }
169
+ getControllerInstance(controllerPath) {
170
+ const controller = this.api.findController(controllerPath);
171
+ return controller && this._controllerInstances.get(controller);
172
+ }
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
+ /**
211
+ *
212
+ * @param args
213
+ * @protected
214
+ */
215
+ _createHandler(args) {
216
+ const { controller, instance, operation } = args;
217
+ /** Prepare parsers */
218
+ const decodePayload = operation.payloadType?.generateCodec('decode', {
219
+ scope: this.scope,
220
+ ignoreWriteonlyFields: true,
221
+ });
222
+ operation.headers.forEach(header => {
223
+ let decode = this[core_1.kAssetCache].get(header, 'decode');
224
+ if (!decode) {
225
+ decode =
226
+ header.type?.generateCodec('decode', {
227
+ scope: this.scope,
228
+ ignoreReadonlyFields: true,
229
+ }) || valgen_1.vg.isAny();
230
+ this[core_1.kAssetCache].set(header, 'decode', decode);
231
+ }
232
+ });
233
+ args.handler = async (message) => {
234
+ if (!message)
235
+ return;
236
+ const operationHandler = instance[operation.name];
237
+ let content;
238
+ const headers = {};
239
+ try {
240
+ /** Parse and decode `payload` */
241
+ content = await this._parseContent(message);
242
+ if (content && decodePayload) {
243
+ if (Buffer.isBuffer(content))
244
+ content = content.toString('utf-8');
245
+ content = decodePayload(content);
246
+ }
247
+ // message.properties.
248
+ /** Parse and decode `headers` */
249
+ if (message.properties.headers) {
250
+ for (const [k, v] of Object.entries(message.properties.headers)) {
251
+ const header = operation.findHeader(k);
252
+ const decode = this[core_1.kAssetCache].get(header, 'decode') || valgen_1.vg.isAny();
253
+ headers[k] = decode(Buffer.isBuffer(v) ? v.toString() : v);
254
+ }
255
+ }
256
+ }
257
+ catch (e) {
258
+ this._emitError(e);
259
+ return;
260
+ }
261
+ /** Create context */
262
+ const context = new rabbitmq_context_js_1.RabbitmqContext({
263
+ adapter: this,
264
+ platform: this.platform,
265
+ controller,
266
+ controllerInstance: instance,
267
+ operation,
268
+ operationHandler,
269
+ fields: message.fields,
270
+ properties: message.properties,
271
+ content,
272
+ headers,
273
+ });
274
+ await this.emitAsync('before-execute', context);
275
+ try {
276
+ /** Call operation handler */
277
+ const result = await operationHandler.call(instance, context);
278
+ await this.emitAsync('after-execute', context, result);
279
+ }
280
+ catch (e) {
281
+ this._emitError(e, context);
282
+ }
283
+ };
284
+ }
285
+ async _parseContent(msg) {
286
+ if (!msg.content?.length)
287
+ return;
288
+ let content = msg.content;
289
+ if (msg.properties.contentEncoding) {
290
+ switch (msg.properties.contentEncoding) {
291
+ case 'gzip':
292
+ case 'x-gzip': {
293
+ content = await gunzipAsync(content);
294
+ break;
295
+ }
296
+ case 'deflate':
297
+ case 'x-deflate': {
298
+ content = await deflateAsync(content);
299
+ break;
300
+ }
301
+ case 'inflate':
302
+ case 'x-inflate': {
303
+ content = await inflateAsync(content);
304
+ break;
305
+ }
306
+ case 'br': {
307
+ content = await brotliAsync(content);
308
+ break;
309
+ }
310
+ case 'base64': {
311
+ content = content.toString('base64');
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ const mediaType = msg.properties.contentType &&
317
+ (0, content_type_1.parse)(msg.properties.contentType || '');
318
+ let charset = (mediaType?.parameters.charset || '').toLowerCase();
319
+ if (!charset && type_is_1.default.is(mediaType?.type, ['json', 'xml', 'txt']))
320
+ charset = 'utf-8';
321
+ if (charset) {
322
+ content = iconv_lite_1.default.decode(content, charset);
323
+ if (type_is_1.default.is(mediaType.type, ['json']))
324
+ return JSON.parse(content);
325
+ }
326
+ return content;
327
+ }
328
+ _emitError(error, context) {
329
+ Promise.resolve()
330
+ .then(async () => {
331
+ const logger = this.logger;
332
+ if (context) {
333
+ if (!context.errors.length)
334
+ context.errors.push(error);
335
+ context.errors = this._wrapExceptions(context.errors);
336
+ if (context.listenerCount('error')) {
337
+ await this.emitAsync('error', context.errors[0], context);
338
+ }
339
+ if (logger?.error) {
340
+ context.errors.forEach(err => logger.error(err));
341
+ }
342
+ return;
343
+ }
344
+ this.logger?.error(error);
345
+ if (this.listenerCount('error'))
346
+ this.emit('error', error);
347
+ })
348
+ .catch(noOp);
349
+ }
350
+ _wrapExceptions(exceptions) {
351
+ const wrappedErrors = exceptions.map(e => e instanceof common_1.OpraException ? e : new common_1.OpraException(e));
352
+ if (!wrappedErrors.length)
353
+ wrappedErrors.push(new common_1.OpraException('Internal Server Error'));
354
+ return wrappedErrors;
355
+ }
356
+ }
357
+ exports.RabbitmqAdapter = RabbitmqAdapter;
358
+ RabbitmqAdapter.PlatformName = 'rabbitmq';
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RabbitmqContext = void 0;
4
+ const core_1 = require("@opra/core");
5
+ /**
6
+ * RabbitmqContext class provides the context for handling RabbitMQ messages.
7
+ * It extends the ExecutionContext and implements the AsyncEventEmitter.
8
+ */
9
+ class RabbitmqContext extends core_1.ExecutionContext {
10
+ /**
11
+ * Constructor
12
+ * @param init the context options
13
+ */
14
+ constructor(init) {
15
+ super({
16
+ ...init,
17
+ document: init.adapter.document,
18
+ documentNode: init.controller?.node,
19
+ protocol: 'rpc',
20
+ });
21
+ this.adapter = init.adapter;
22
+ this.platform = init.adapter.platform;
23
+ this.protocol = 'rpc';
24
+ if (init.controller)
25
+ this.controller = init.controller;
26
+ if (init.controllerInstance)
27
+ this.controllerInstance = init.controllerInstance;
28
+ if (init.operation)
29
+ this.operation = init.operation;
30
+ if (init.operationHandler)
31
+ this.operationHandler = init.operationHandler;
32
+ this.fields = init.fields;
33
+ this.properties = init.properties;
34
+ this.headers = init.headers || {};
35
+ this.content = init.content;
36
+ }
37
+ }
38
+ exports.RabbitmqContext = RabbitmqContext;
@@ -0,0 +1,17 @@
1
+ import '@opra/core';
2
+ import { classes } from '@opra/common';
3
+ import { RMQ_OPERATION_METADATA, RMQ_OPERATION_METADATA_RESOLVER, } from '../constants.js';
4
+ /** Implementation **/
5
+ classes.RpcOperationDecoratorFactory.augment((decorator, decoratorChain) => {
6
+ decorator.RabbitMQ = (config) => {
7
+ decoratorChain.push((_, target, propertyKey) => {
8
+ if (typeof config === 'function') {
9
+ Reflect.defineMetadata(RMQ_OPERATION_METADATA_RESOLVER, config, target, propertyKey);
10
+ }
11
+ else {
12
+ Reflect.defineMetadata(RMQ_OPERATION_METADATA, { ...config }, target, propertyKey);
13
+ }
14
+ });
15
+ return decorator;
16
+ };
17
+ });
@@ -0,0 +1,3 @@
1
+ export const RMQ_DEFAULT_GROUP = 'default';
2
+ export const RMQ_OPERATION_METADATA = 'RMQ_OPERATION_METADATA';
3
+ export const RMQ_OPERATION_METADATA_RESOLVER = 'RMQ_OPERATION_METADATA_RESOLVER';
package/esm/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import './augmentation/opra-common.augmentation.js';
2
+ export * from './constants.js';
3
+ export * from './rabbitmq-adapter.js';
4
+ export * from './rabbitmq-context.js';
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,353 @@
1
+ import zlib from 'node:zlib';
2
+ import typeIs from '@browsery/type-is';
3
+ import { OpraException, RPC_CONTROLLER_METADATA, RpcApi, } from '@opra/common';
4
+ import { kAssetCache, PlatformAdapter } from '@opra/core';
5
+ import amqplib from 'amqplib';
6
+ import { parse as parseContentType } from 'content-type';
7
+ import iconv from 'iconv-lite';
8
+ import { promisify } from 'util';
9
+ import { vg } from 'valgen';
10
+ import { RMQ_OPERATION_METADATA, RMQ_OPERATION_METADATA_RESOLVER, } from './constants.js';
11
+ import { RabbitmqContext } from './rabbitmq-context.js';
12
+ const gunzipAsync = promisify(zlib.gunzip);
13
+ const deflateAsync = promisify(zlib.deflate);
14
+ const inflateAsync = promisify(zlib.inflate);
15
+ const brotliAsync = promisify(zlib.brotliCompress);
16
+ const globalErrorTypes = ['unhandledRejection', 'uncaughtException'];
17
+ const signalTraps = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
18
+ const noOp = () => undefined;
19
+ /**
20
+ *
21
+ * @class RabbitmqAdapter
22
+ */
23
+ export class RabbitmqAdapter extends PlatformAdapter {
24
+ /**
25
+ *
26
+ * @param document
27
+ * @param config
28
+ * @constructor
29
+ */
30
+ constructor(document, config) {
31
+ super(config);
32
+ this._controllerInstances = new Map();
33
+ this._connections = [];
34
+ this._status = 'idle';
35
+ this.protocol = 'rpc';
36
+ this.platform = RabbitmqAdapter.PlatformName;
37
+ this._document = document;
38
+ this._config = config;
39
+ if (!(this.document.api instanceof RpcApi &&
40
+ this.document.api.platform === RabbitmqAdapter.PlatformName)) {
41
+ throw new TypeError(`The document doesn't expose a RabbitMQ Api`);
42
+ }
43
+ // this._config = config;
44
+ this.interceptors = [...(config.interceptors || [])];
45
+ globalErrorTypes.forEach(type => {
46
+ process.on(type, e => {
47
+ this._emitError(e);
48
+ return this.close();
49
+ });
50
+ });
51
+ signalTraps.forEach(type => {
52
+ process.once(type, () => this.close());
53
+ });
54
+ }
55
+ get api() {
56
+ return this.document.rpcApi;
57
+ }
58
+ get scope() {
59
+ return this._config.scope;
60
+ }
61
+ get status() {
62
+ return this._status;
63
+ }
64
+ /**
65
+ * Starts the service
66
+ */
67
+ async start() {
68
+ if (this.status !== 'idle')
69
+ return;
70
+ this._status = 'starting';
71
+ const handlerArgs = [];
72
+ try {
73
+ /** Initialize consumers */
74
+ for (const controller of this.document.rpcApi.controllers.values()) {
75
+ let instance = controller.instance;
76
+ if (!instance && controller.ctor)
77
+ instance = new controller.ctor();
78
+ if (!instance)
79
+ continue;
80
+ this._controllerInstances.set(controller, instance);
81
+ /** Build HandlerData array */
82
+ for (const operation of controller.operations.values()) {
83
+ const operationConfig = await this._getOperationConfig(controller, instance, operation);
84
+ if (!operationConfig)
85
+ continue;
86
+ const args = {
87
+ consumer: null,
88
+ controller,
89
+ instance,
90
+ operation,
91
+ operationConfig,
92
+ handler: null,
93
+ topics: (Array.isArray(operation.channel)
94
+ ? operation.channel
95
+ : [operation.channel]).map(String),
96
+ };
97
+ this._createHandler(args);
98
+ handlerArgs.push(args);
99
+ }
100
+ }
101
+ /** Connect to server */
102
+ for (const connectionOptions of this._config.connection) {
103
+ const connection = await amqplib.connect(connectionOptions);
104
+ this._connections.push(connection);
105
+ const hostname = typeof connectionOptions === 'object'
106
+ ? connectionOptions.hostname
107
+ : connectionOptions;
108
+ this.logger?.info?.(`Connected to ${hostname}`);
109
+ /** Subscribe to channels */
110
+ for (const args of handlerArgs) {
111
+ /** Create channel per operation */
112
+ const channel = await connection.createChannel();
113
+ for (const topic of args.topics) {
114
+ const opts = this._config.queues?.[topic];
115
+ if (opts)
116
+ await channel.assertQueue(topic, opts);
117
+ }
118
+ for (const topic of args.topics) {
119
+ await channel.assertQueue(topic);
120
+ await channel
121
+ .consume(topic, async (msg) => {
122
+ if (!msg)
123
+ return;
124
+ await this.emitAsync('message', msg).catch(() => undefined);
125
+ try {
126
+ await args.handler(msg);
127
+ // channel.ack(msg);
128
+ }
129
+ catch (e) {
130
+ this._emitError(e);
131
+ }
132
+ await this.emitAsync('message-finish', msg);
133
+ },
134
+ /** Consume options */
135
+ args.operationConfig.consumer)
136
+ .catch(e => {
137
+ this._emitError(e);
138
+ throw e;
139
+ });
140
+ this.logger?.info?.(`Subscribed to topic${args.topics.length > 1 ? 's' : ''} "${args.topics}"`);
141
+ }
142
+ }
143
+ }
144
+ this._status = 'started';
145
+ }
146
+ catch (e) {
147
+ await this.close();
148
+ throw e;
149
+ }
150
+ }
151
+ // protected async _connect() {
152
+ // if (!this._connection)
153
+ // this._connection = await amqplib.connect(this._config.connection);
154
+ // return this._connection;
155
+ // }
156
+ /**
157
+ * Closes all connections and stops the service
158
+ */
159
+ async close() {
160
+ await Promise.all(this._connections.map(c => c.close()));
161
+ this._connections = [];
162
+ this._controllerInstances.clear();
163
+ this._status = 'idle';
164
+ }
165
+ getControllerInstance(controllerPath) {
166
+ const controller = this.api.findController(controllerPath);
167
+ return controller && this._controllerInstances.get(controller);
168
+ }
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
+ /**
207
+ *
208
+ * @param args
209
+ * @protected
210
+ */
211
+ _createHandler(args) {
212
+ const { controller, instance, operation } = args;
213
+ /** Prepare parsers */
214
+ const decodePayload = operation.payloadType?.generateCodec('decode', {
215
+ scope: this.scope,
216
+ ignoreWriteonlyFields: true,
217
+ });
218
+ operation.headers.forEach(header => {
219
+ let decode = this[kAssetCache].get(header, 'decode');
220
+ if (!decode) {
221
+ decode =
222
+ header.type?.generateCodec('decode', {
223
+ scope: this.scope,
224
+ ignoreReadonlyFields: true,
225
+ }) || vg.isAny();
226
+ this[kAssetCache].set(header, 'decode', decode);
227
+ }
228
+ });
229
+ args.handler = async (message) => {
230
+ if (!message)
231
+ return;
232
+ const operationHandler = instance[operation.name];
233
+ let content;
234
+ const headers = {};
235
+ try {
236
+ /** Parse and decode `payload` */
237
+ content = await this._parseContent(message);
238
+ if (content && decodePayload) {
239
+ if (Buffer.isBuffer(content))
240
+ content = content.toString('utf-8');
241
+ content = decodePayload(content);
242
+ }
243
+ // message.properties.
244
+ /** Parse and decode `headers` */
245
+ if (message.properties.headers) {
246
+ for (const [k, v] of Object.entries(message.properties.headers)) {
247
+ const header = operation.findHeader(k);
248
+ const decode = this[kAssetCache].get(header, 'decode') || vg.isAny();
249
+ headers[k] = decode(Buffer.isBuffer(v) ? v.toString() : v);
250
+ }
251
+ }
252
+ }
253
+ catch (e) {
254
+ this._emitError(e);
255
+ return;
256
+ }
257
+ /** Create context */
258
+ const context = new RabbitmqContext({
259
+ adapter: this,
260
+ platform: this.platform,
261
+ controller,
262
+ controllerInstance: instance,
263
+ operation,
264
+ operationHandler,
265
+ fields: message.fields,
266
+ properties: message.properties,
267
+ content,
268
+ headers,
269
+ });
270
+ await this.emitAsync('before-execute', context);
271
+ try {
272
+ /** Call operation handler */
273
+ const result = await operationHandler.call(instance, context);
274
+ await this.emitAsync('after-execute', context, result);
275
+ }
276
+ catch (e) {
277
+ this._emitError(e, context);
278
+ }
279
+ };
280
+ }
281
+ async _parseContent(msg) {
282
+ if (!msg.content?.length)
283
+ return;
284
+ let content = msg.content;
285
+ if (msg.properties.contentEncoding) {
286
+ switch (msg.properties.contentEncoding) {
287
+ case 'gzip':
288
+ case 'x-gzip': {
289
+ content = await gunzipAsync(content);
290
+ break;
291
+ }
292
+ case 'deflate':
293
+ case 'x-deflate': {
294
+ content = await deflateAsync(content);
295
+ break;
296
+ }
297
+ case 'inflate':
298
+ case 'x-inflate': {
299
+ content = await inflateAsync(content);
300
+ break;
301
+ }
302
+ case 'br': {
303
+ content = await brotliAsync(content);
304
+ break;
305
+ }
306
+ case 'base64': {
307
+ content = content.toString('base64');
308
+ break;
309
+ }
310
+ }
311
+ }
312
+ const mediaType = msg.properties.contentType &&
313
+ parseContentType(msg.properties.contentType || '');
314
+ let charset = (mediaType?.parameters.charset || '').toLowerCase();
315
+ if (!charset && typeIs.is(mediaType?.type, ['json', 'xml', 'txt']))
316
+ charset = 'utf-8';
317
+ if (charset) {
318
+ content = iconv.decode(content, charset);
319
+ if (typeIs.is(mediaType.type, ['json']))
320
+ return JSON.parse(content);
321
+ }
322
+ return content;
323
+ }
324
+ _emitError(error, context) {
325
+ Promise.resolve()
326
+ .then(async () => {
327
+ const logger = this.logger;
328
+ if (context) {
329
+ if (!context.errors.length)
330
+ context.errors.push(error);
331
+ context.errors = this._wrapExceptions(context.errors);
332
+ if (context.listenerCount('error')) {
333
+ await this.emitAsync('error', context.errors[0], context);
334
+ }
335
+ if (logger?.error) {
336
+ context.errors.forEach(err => logger.error(err));
337
+ }
338
+ return;
339
+ }
340
+ this.logger?.error(error);
341
+ if (this.listenerCount('error'))
342
+ this.emit('error', error);
343
+ })
344
+ .catch(noOp);
345
+ }
346
+ _wrapExceptions(exceptions) {
347
+ const wrappedErrors = exceptions.map(e => e instanceof OpraException ? e : new OpraException(e));
348
+ if (!wrappedErrors.length)
349
+ wrappedErrors.push(new OpraException('Internal Server Error'));
350
+ return wrappedErrors;
351
+ }
352
+ }
353
+ RabbitmqAdapter.PlatformName = 'rabbitmq';
@@ -0,0 +1,34 @@
1
+ import { ExecutionContext } from '@opra/core';
2
+ /**
3
+ * RabbitmqContext class provides the context for handling RabbitMQ messages.
4
+ * It extends the ExecutionContext and implements the AsyncEventEmitter.
5
+ */
6
+ export class RabbitmqContext extends ExecutionContext {
7
+ /**
8
+ * Constructor
9
+ * @param init the context options
10
+ */
11
+ constructor(init) {
12
+ super({
13
+ ...init,
14
+ document: init.adapter.document,
15
+ documentNode: init.controller?.node,
16
+ protocol: 'rpc',
17
+ });
18
+ this.adapter = init.adapter;
19
+ this.platform = init.adapter.platform;
20
+ this.protocol = 'rpc';
21
+ if (init.controller)
22
+ this.controller = init.controller;
23
+ if (init.controllerInstance)
24
+ this.controllerInstance = init.controllerInstance;
25
+ if (init.operation)
26
+ this.operation = init.operation;
27
+ if (init.operationHandler)
28
+ this.operationHandler = init.operationHandler;
29
+ this.fields = init.fields;
30
+ this.properties = init.properties;
31
+ this.headers = init.headers || {};
32
+ this.content = init.content;
33
+ }
34
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@opra/rabbitmq",
3
+ "version": "1.10.0",
4
+ "description": "Opra RabbitMQ package",
5
+ "author": "Panates",
6
+ "license": "MIT",
7
+ "dependencies": {
8
+ "@browsery/type-is": "^1.6.18-r8",
9
+ "@opra/common": "^1.10.0",
10
+ "@opra/core": "^1.10.0",
11
+ "content-type": "^1.0.5",
12
+ "iconv-lite": "^0.6.3",
13
+ "node-events-async": "^1.0.0",
14
+ "tslib": "^2.8.1",
15
+ "valgen": "^5.13.0"
16
+ },
17
+ "peerDependencies": {
18
+ "amqplib": "^0.10.5"
19
+ },
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "import": {
24
+ "types": "./types/index.d.ts",
25
+ "default": "./esm/index.js"
26
+ },
27
+ "require": {
28
+ "types": "./types/index.d.cts",
29
+ "default": "./cjs/index.js"
30
+ },
31
+ "default": "./esm/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "main": "./cjs/index.js",
36
+ "module": "./esm/index.js",
37
+ "types": "./types/index.d.ts",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/panates/opra.git",
41
+ "directory": "packages/rabbitmq"
42
+ },
43
+ "engines": {
44
+ "node": ">=16.0",
45
+ "npm": ">=7.0.0"
46
+ },
47
+ "files": [
48
+ "cjs/",
49
+ "esm/",
50
+ "types/",
51
+ "LICENSE",
52
+ "README.md"
53
+ ],
54
+ "keywords": [
55
+ "opra",
56
+ "rabbitmq",
57
+ "amqp",
58
+ "amqplib",
59
+ "message",
60
+ "queue",
61
+ "consumer"
62
+ ]
63
+ }
@@ -0,0 +1,7 @@
1
+ import '@opra/core';
2
+ import { RabbitmqAdapter } from '../rabbitmq-adapter.js';
3
+ declare module '@opra/common' {
4
+ interface RpcOperationDecorator {
5
+ RabbitMQ(config: RabbitmqAdapter.OperationOptions | (() => RabbitmqAdapter.OperationOptions | Promise<RabbitmqAdapter.OperationOptions>)): this;
6
+ }
7
+ }
@@ -0,0 +1,3 @@
1
+ export declare const RMQ_DEFAULT_GROUP = "default";
2
+ export declare const RMQ_OPERATION_METADATA = "RMQ_OPERATION_METADATA";
3
+ export declare const RMQ_OPERATION_METADATA_RESOLVER = "RMQ_OPERATION_METADATA_RESOLVER";
@@ -0,0 +1,4 @@
1
+ import './augmentation/opra-common.augmentation.js';
2
+ export * from './constants.js';
3
+ export * from './rabbitmq-adapter.js';
4
+ export * from './rabbitmq-context.js';
@@ -0,0 +1,4 @@
1
+ import './augmentation/opra-common.augmentation.js';
2
+ export * from './constants.js';
3
+ export * from './rabbitmq-adapter.js';
4
+ export * from './rabbitmq-context.js';
@@ -0,0 +1,102 @@
1
+ import { ApiDocument, OpraException, OpraSchema, RpcApi, RpcController, RpcOperation } from '@opra/common';
2
+ import { PlatformAdapter } from '@opra/core';
3
+ import amqplib from 'amqplib';
4
+ import { ConsumeMessage } from 'amqplib/properties';
5
+ import { RabbitmqContext } from './rabbitmq-context.js';
6
+ export interface OperationConfig {
7
+ consumer: amqplib.Options.Consume & {};
8
+ }
9
+ interface HandlerArguments {
10
+ consumer: amqplib.Options.Consume & {};
11
+ controller: RpcController;
12
+ instance: any;
13
+ operation: RpcOperation;
14
+ operationConfig: OperationConfig;
15
+ handler: (msg: ConsumeMessage | null) => void | Promise<void>;
16
+ topics: string[];
17
+ }
18
+ /**
19
+ *
20
+ * @class RabbitmqAdapter
21
+ */
22
+ export declare class RabbitmqAdapter extends PlatformAdapter {
23
+ static readonly PlatformName = "rabbitmq";
24
+ protected _config: RabbitmqAdapter.Config;
25
+ protected _controllerInstances: Map<RpcController, any>;
26
+ protected _connections: amqplib.Connection[];
27
+ protected _status: RabbitmqAdapter.Status;
28
+ readonly protocol: OpraSchema.Transport;
29
+ readonly platform = "rabbitmq";
30
+ readonly interceptors: (RabbitmqAdapter.InterceptorFunction | RabbitmqAdapter.IRabbitmqInterceptor)[];
31
+ /**
32
+ *
33
+ * @param document
34
+ * @param config
35
+ * @constructor
36
+ */
37
+ constructor(document: ApiDocument, config: RabbitmqAdapter.Config);
38
+ get api(): RpcApi;
39
+ get scope(): string | undefined;
40
+ get status(): RabbitmqAdapter.Status;
41
+ /**
42
+ * Starts the service
43
+ */
44
+ start(): Promise<void>;
45
+ /**
46
+ * Closes all connections and stops the service
47
+ */
48
+ close(): Promise<void>;
49
+ getControllerInstance<T>(controllerPath: string): T | undefined;
50
+ /**
51
+ *
52
+ * @param controller
53
+ * @param instance
54
+ * @param operation
55
+ * @protected
56
+ */
57
+ protected _getOperationConfig(controller: RpcController, instance: any, operation: RpcOperation): Promise<OperationConfig | undefined>;
58
+ /**
59
+ *
60
+ * @param args
61
+ * @protected
62
+ */
63
+ protected _createHandler(args: HandlerArguments): void;
64
+ protected _parseContent(msg: ConsumeMessage): Promise<any>;
65
+ protected _emitError(error: any, context?: RabbitmqContext): void;
66
+ protected _wrapExceptions(exceptions: any[]): OpraException[];
67
+ }
68
+ /**
69
+ * @namespace RabbitmqAdapter
70
+ */
71
+ export declare namespace RabbitmqAdapter {
72
+ type NextCallback = () => Promise<any>;
73
+ type Status = 'idle' | 'starting' | 'started';
74
+ type ConnectionOptions = amqplib.Options.Connect;
75
+ interface Config extends PlatformAdapter.Options {
76
+ connection: (string | ConnectionOptions)[];
77
+ queues?: Record<string, amqplib.Options.AssertQueue>;
78
+ defaults?: {
79
+ consumer?: amqplib.Options.Consume;
80
+ };
81
+ scope?: string;
82
+ interceptors?: (InterceptorFunction | IRabbitmqInterceptor)[];
83
+ logExtra?: boolean;
84
+ }
85
+ interface OperationOptions {
86
+ /**
87
+ * ConsumerConfig
88
+ */
89
+ consumer?: amqplib.Options.Consume;
90
+ }
91
+ /**
92
+ * @type InterceptorFunction
93
+ */
94
+ type InterceptorFunction = IRabbitmqInterceptor['intercept'];
95
+ /**
96
+ * @interface IRabbitmqInterceptor
97
+ */
98
+ type IRabbitmqInterceptor = {
99
+ intercept(context: RabbitmqContext, next: NextCallback): Promise<any>;
100
+ };
101
+ }
102
+ export {};
@@ -0,0 +1,40 @@
1
+ import { OpraSchema, RpcController, RpcOperation } from '@opra/common';
2
+ import { ExecutionContext } from '@opra/core';
3
+ import amqplib from 'amqplib';
4
+ import type { AsyncEventEmitter } from 'node-events-async';
5
+ import type { RabbitmqAdapter } from './rabbitmq-adapter';
6
+ /**
7
+ * RabbitmqContext class provides the context for handling RabbitMQ messages.
8
+ * It extends the ExecutionContext and implements the AsyncEventEmitter.
9
+ */
10
+ export declare class RabbitmqContext extends ExecutionContext implements AsyncEventEmitter {
11
+ readonly protocol: OpraSchema.Transport;
12
+ readonly platform: string;
13
+ readonly adapter: RabbitmqAdapter;
14
+ readonly controller?: RpcController;
15
+ readonly controllerInstance?: any;
16
+ readonly operation?: RpcOperation;
17
+ readonly operationHandler?: Function;
18
+ readonly fields: amqplib.MessageFields;
19
+ readonly properties: amqplib.MessageProperties;
20
+ readonly content: any;
21
+ readonly headers: Record<string, any>;
22
+ /**
23
+ * Constructor
24
+ * @param init the context options
25
+ */
26
+ constructor(init: RabbitmqContext.Initiator);
27
+ }
28
+ export declare namespace RabbitmqContext {
29
+ interface Initiator extends Omit<ExecutionContext.Initiator, 'document' | 'protocol' | 'documentNode'> {
30
+ adapter: RabbitmqAdapter;
31
+ controller?: RpcController;
32
+ controllerInstance?: any;
33
+ operation?: RpcOperation;
34
+ operationHandler?: Function;
35
+ content: any;
36
+ headers: Record<string, any>;
37
+ fields: amqplib.MessageFields;
38
+ properties: amqplib.MessageProperties;
39
+ }
40
+ }