@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 +21 -0
- package/README.md +3 -0
- package/cjs/augmentation/opra-common.augmentation.js +19 -0
- package/cjs/constants.js +6 -0
- package/cjs/index.js +7 -0
- package/cjs/package.json +3 -0
- package/cjs/rabbitmq-adapter.js +358 -0
- package/cjs/rabbitmq-context.js +38 -0
- package/esm/augmentation/opra-common.augmentation.js +17 -0
- package/esm/constants.js +3 -0
- package/esm/index.js +4 -0
- package/esm/package.json +3 -0
- package/esm/rabbitmq-adapter.js +353 -0
- package/esm/rabbitmq-context.js +34 -0
- package/package.json +63 -0
- package/types/augmentation/opra-common.augmentation.d.ts +7 -0
- package/types/constants.d.ts +3 -0
- package/types/index.d.cts +4 -0
- package/types/index.d.ts +4 -0
- package/types/rabbitmq-adapter.d.ts +102 -0
- package/types/rabbitmq-context.d.ts +40 -0
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,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
|
+
});
|
package/cjs/constants.js
ADDED
|
@@ -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);
|
package/cjs/package.json
ADDED
|
@@ -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
|
+
});
|
package/esm/constants.js
ADDED
package/esm/index.js
ADDED
package/esm/package.json
ADDED
|
@@ -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
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -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
|
+
}
|