@flusys/nestjs-shared 6.4.0 → 7.0.0-beta.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.
- package/README.md +437 -0
- package/cjs/classes/api-service.class.js +71 -8
- package/cjs/classes/api-service.class.spec.js +220 -0
- package/cjs/classes/disabled-event-bus.class.js +15 -0
- package/cjs/classes/disabled-event-bus.class.spec.js +40 -0
- package/cjs/classes/domain-event.publisher.js +76 -0
- package/cjs/classes/domain-event.publisher.spec.js +234 -0
- package/cjs/classes/event-bus.registry.js +69 -0
- package/cjs/classes/event-bus.registry.spec.js +116 -0
- package/cjs/classes/event-transports/event-transport.registry.js +41 -0
- package/cjs/classes/event-transports/event-transport.registry.spec.js +31 -0
- package/cjs/classes/event-transports/index.js +23 -0
- package/cjs/classes/event-transports/kafka-transport.optional.js +151 -0
- package/cjs/classes/event-transports/kafka-transport.optional.spec.js +199 -0
- package/cjs/classes/event-transports/memory-transport.js +54 -0
- package/cjs/classes/event-transports/memory-transport.spec.js +58 -0
- package/cjs/classes/event-transports/rabbitmq-transport.optional.js +184 -0
- package/cjs/classes/event-transports/rabbitmq-transport.optional.spec.js +256 -0
- package/cjs/classes/hybrid-event-bus.class.js +261 -0
- package/cjs/classes/hybrid-event-bus.class.spec.js +189 -0
- package/cjs/classes/index.js +5 -0
- package/cjs/constants/index.js +24 -0
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/on-domain-event.decorator.js +13 -0
- package/cjs/decorators/on-domain-event.decorator.spec.js +50 -0
- package/cjs/entities/user-root.js +2 -2
- package/cjs/enums/event-action.enum.js +18 -0
- package/cjs/enums/index.js +1 -0
- package/cjs/interfaces/event-bus.interface.js +4 -0
- package/cjs/interfaces/index.js +1 -0
- package/cjs/modules/event-bus/event-bus.explorer.js +124 -0
- package/cjs/modules/event-bus/event-bus.explorer.spec.js +189 -0
- package/cjs/modules/event-bus/event-bus.integration.spec.js +254 -0
- package/cjs/modules/event-bus/event-bus.module.js +94 -0
- package/cjs/modules/event-bus/event-bus.module.spec.js +149 -0
- package/cjs/modules/event-bus/module-events.provider.js +39 -0
- package/cjs/modules/event-bus/module-events.provider.spec.js +56 -0
- package/cjs/modules/index.js +3 -0
- package/cjs/utils/event-pattern.util.js +98 -0
- package/cjs/utils/event-pattern.util.spec.js +234 -0
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/optional-module.util.js +35 -0
- package/cjs/utils/optional-module.util.spec.js +14 -0
- package/classes/api-service.class.d.ts +11 -1
- package/classes/disabled-event-bus.class.d.ts +6 -0
- package/classes/domain-event.publisher.d.ts +14 -0
- package/classes/event-bus.registry.d.ts +14 -0
- package/classes/event-transports/event-transport.registry.d.ts +10 -0
- package/classes/event-transports/index.d.ts +2 -0
- package/classes/event-transports/kafka-transport.optional.d.ts +21 -0
- package/classes/event-transports/memory-transport.d.ts +13 -0
- package/classes/event-transports/rabbitmq-transport.optional.d.ts +27 -0
- package/classes/hybrid-event-bus.class.d.ts +32 -0
- package/classes/index.d.ts +5 -0
- package/constants/index.d.ts +6 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/on-domain-event.decorator.d.ts +1 -0
- package/enums/event-action.enum.d.ts +7 -0
- package/enums/index.d.ts +1 -0
- package/fesm/classes/api-service.class.js +71 -8
- package/fesm/classes/api-service.class.spec.js +220 -0
- package/fesm/classes/disabled-event-bus.class.js +11 -0
- package/fesm/classes/disabled-event-bus.class.spec.js +36 -0
- package/fesm/classes/domain-event.publisher.js +63 -0
- package/fesm/classes/domain-event.publisher.spec.js +230 -0
- package/fesm/classes/event-bus.registry.js +65 -0
- package/fesm/classes/event-bus.registry.spec.js +112 -0
- package/fesm/classes/event-transports/event-transport.registry.js +35 -0
- package/fesm/classes/event-transports/event-transport.registry.spec.js +27 -0
- package/fesm/classes/event-transports/index.js +6 -0
- package/fesm/classes/event-transports/kafka-transport.optional.js +144 -0
- package/fesm/classes/event-transports/kafka-transport.optional.spec.js +195 -0
- package/fesm/classes/event-transports/memory-transport.js +46 -0
- package/fesm/classes/event-transports/memory-transport.spec.js +54 -0
- package/fesm/classes/event-transports/rabbitmq-transport.optional.js +174 -0
- package/fesm/classes/event-transports/rabbitmq-transport.optional.spec.js +252 -0
- package/fesm/classes/hybrid-event-bus.class.js +217 -0
- package/fesm/classes/hybrid-event-bus.class.spec.js +185 -0
- package/fesm/classes/index.js +5 -0
- package/fesm/constants/index.js +7 -0
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/on-domain-event.decorator.js +10 -0
- package/fesm/decorators/on-domain-event.decorator.spec.js +46 -0
- package/fesm/entities/user-root.js +1 -1
- package/fesm/enums/event-action.enum.js +8 -0
- package/fesm/enums/index.js +1 -0
- package/fesm/interfaces/event-bus.interface.js +1 -0
- package/fesm/interfaces/index.js +1 -0
- package/fesm/modules/event-bus/event-bus.explorer.js +114 -0
- package/fesm/modules/event-bus/event-bus.explorer.spec.js +185 -0
- package/fesm/modules/event-bus/event-bus.integration.spec.js +250 -0
- package/fesm/modules/event-bus/event-bus.module.js +84 -0
- package/fesm/modules/event-bus/event-bus.module.spec.js +145 -0
- package/fesm/modules/event-bus/module-events.provider.js +26 -0
- package/fesm/modules/event-bus/module-events.provider.spec.js +52 -0
- package/fesm/modules/index.js +3 -0
- package/fesm/utils/event-pattern.util.js +86 -0
- package/fesm/utils/event-pattern.util.spec.js +230 -0
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/optional-module.util.js +25 -0
- package/fesm/utils/optional-module.util.spec.js +10 -0
- package/interfaces/event-bus.interface.d.ts +74 -0
- package/interfaces/index.d.ts +1 -0
- package/modules/event-bus/event-bus.explorer.d.ts +14 -0
- package/modules/event-bus/event-bus.module.d.ts +8 -0
- package/modules/event-bus/module-events.provider.d.ts +3 -0
- package/modules/index.d.ts +3 -0
- package/package.json +2 -2
- package/utils/event-pattern.util.d.ts +3 -0
- package/utils/index.d.ts +2 -0
- package/utils/optional-module.util.d.ts +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional RabbitMQ event transport
|
|
3
|
+
* Requires: npm install amqplib
|
|
4
|
+
*/ "use strict";
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
Object.defineProperty(exports, "RabbitMqTransport", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
get: function() {
|
|
11
|
+
return RabbitMqTransport;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const _common = require("@nestjs/common");
|
|
15
|
+
const _constants = require("../../constants");
|
|
16
|
+
const _eventpatternutil = require("../../utils/event-pattern.util");
|
|
17
|
+
const _optionalmoduleutil = require("../../utils/optional-module.util");
|
|
18
|
+
const _eventtransportregistry = require("./event-transport.registry");
|
|
19
|
+
function _define_property(obj, key, value) {
|
|
20
|
+
if (key in obj) {
|
|
21
|
+
Object.defineProperty(obj, key, {
|
|
22
|
+
value: value,
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
obj[key] = value;
|
|
29
|
+
}
|
|
30
|
+
return obj;
|
|
31
|
+
}
|
|
32
|
+
const DEFAULT_URL = 'amqp://localhost:5672';
|
|
33
|
+
const DEFAULT_RECONNECT_DELAY = 5000;
|
|
34
|
+
let RabbitMqTransport = class RabbitMqTransport {
|
|
35
|
+
async connect() {
|
|
36
|
+
let amqp;
|
|
37
|
+
try {
|
|
38
|
+
amqp = await (0, _optionalmoduleutil.loadOptionalModule)('amqplib');
|
|
39
|
+
} catch {
|
|
40
|
+
throw new _common.InternalServerErrorException({
|
|
41
|
+
message: 'amqplib not found',
|
|
42
|
+
messageKey: _constants.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
|
|
43
|
+
messageVariables: {
|
|
44
|
+
sdk: 'amqplib'
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
this.connection = await (amqp.connect ?? amqp.default.connect)(this.config.url ?? DEFAULT_URL);
|
|
49
|
+
this.channel = await this.connection.createChannel();
|
|
50
|
+
await this.channel.assertExchange(this.exchange, 'topic', {
|
|
51
|
+
durable: this.durable
|
|
52
|
+
});
|
|
53
|
+
await this.channel.assertQueue(this.queue, {
|
|
54
|
+
durable: this.durable
|
|
55
|
+
});
|
|
56
|
+
await this.channel.prefetch(this.config.prefetch ?? 10);
|
|
57
|
+
// A channel dies on its own for any protocol error - a mismatched assert, a
|
|
58
|
+
// deleted exchange, an unknown delivery tag - while the connection stays up.
|
|
59
|
+
// Watching only the connection would leave the transport holding a dead
|
|
60
|
+
// channel: publishing throws into the void and nothing ever reconnects.
|
|
61
|
+
this.connection.on('close', ()=>this.scheduleReconnect());
|
|
62
|
+
this.connection.on('error', (error)=>this.logger.warn(error.message));
|
|
63
|
+
this.channel.on('close', ()=>this.scheduleReconnect());
|
|
64
|
+
this.channel.on('error', (error)=>this.logger.warn(error.message));
|
|
65
|
+
for (const pattern of this.boundPatterns){
|
|
66
|
+
await this.channel.bindQueue(this.queue, this.exchange, this.toRoutingKey(pattern));
|
|
67
|
+
}
|
|
68
|
+
if (this.boundPatterns.size) await this.startConsuming();
|
|
69
|
+
}
|
|
70
|
+
async publish(event) {
|
|
71
|
+
if (!this.channel) return;
|
|
72
|
+
this.channel.publish(this.exchange, event.name, Buffer.from(JSON.stringify(event)), {
|
|
73
|
+
persistent: this.durable,
|
|
74
|
+
messageId: event.id,
|
|
75
|
+
appId: this.config.queue ?? _constants.DEFAULT_EVENT_SERVICE_NAME,
|
|
76
|
+
contentType: 'application/json'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async bind(pattern) {
|
|
80
|
+
if (this.boundPatterns.has(pattern)) return;
|
|
81
|
+
this.boundPatterns.add(pattern);
|
|
82
|
+
if (!this.channel) return;
|
|
83
|
+
await this.channel.bindQueue(this.queue, this.exchange, this.toRoutingKey(pattern));
|
|
84
|
+
await this.startConsuming();
|
|
85
|
+
}
|
|
86
|
+
onMessage(handler) {
|
|
87
|
+
this.handlers.push(handler);
|
|
88
|
+
}
|
|
89
|
+
async close() {
|
|
90
|
+
this.closed = true;
|
|
91
|
+
if (this.reconnectTimer) {
|
|
92
|
+
clearTimeout(this.reconnectTimer);
|
|
93
|
+
this.reconnectTimer = undefined;
|
|
94
|
+
}
|
|
95
|
+
await this.teardown();
|
|
96
|
+
}
|
|
97
|
+
isReady() {
|
|
98
|
+
return Boolean(this.channel);
|
|
99
|
+
}
|
|
100
|
+
get exchange() {
|
|
101
|
+
return this.config.exchange ?? _constants.DEFAULT_EVENT_EXCHANGE;
|
|
102
|
+
}
|
|
103
|
+
get queue() {
|
|
104
|
+
return this.config.queue ?? `${_constants.DEFAULT_EVENT_SERVICE_NAME}.events`;
|
|
105
|
+
}
|
|
106
|
+
get durable() {
|
|
107
|
+
return this.config.durable ?? true;
|
|
108
|
+
}
|
|
109
|
+
/** `**` is the FLUSYS alias for the AMQP multi word wildcard */ toRoutingKey(pattern) {
|
|
110
|
+
return pattern.split('.').map((segment)=>(0, _eventpatternutil.isMultiWildcard)(segment) ? '#' : segment).join('.');
|
|
111
|
+
}
|
|
112
|
+
async startConsuming() {
|
|
113
|
+
// Captured, because a reconnect swaps `this.channel` while deliveries are in
|
|
114
|
+
// flight - a delivery tag means nothing on the new channel, and acking it
|
|
115
|
+
// there is a protocol error that kills that channel too.
|
|
116
|
+
const channel = this.channel;
|
|
117
|
+
if (this.consuming || !channel) return;
|
|
118
|
+
this.consuming = true;
|
|
119
|
+
try {
|
|
120
|
+
await channel.consume(this.queue, async (message)=>{
|
|
121
|
+
if (!message) return;
|
|
122
|
+
try {
|
|
123
|
+
const event = JSON.parse(message.content.toString());
|
|
124
|
+
await Promise.all(this.handlers.map((handler)=>handler(event)));
|
|
125
|
+
channel.ack(message);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.logger.error(error instanceof Error ? error.message : 'Failed to handle event');
|
|
128
|
+
channel.nack(message, false, false);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// A latch left set would keep the queue unconsumed for the process lifetime
|
|
133
|
+
this.consuming = false;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** A dropped connection reports itself twice - once per channel, once per connection */ scheduleReconnect() {
|
|
138
|
+
if (this.closed || this.reconnectTimer) return;
|
|
139
|
+
// Not awaited: the fields are cleared before its first await, so publishing
|
|
140
|
+
// stops using the dead channel now rather than after the sockets close.
|
|
141
|
+
void this.teardown();
|
|
142
|
+
this.reconnectTimer = setTimeout(()=>{
|
|
143
|
+
this.reconnectTimer = undefined;
|
|
144
|
+
this.connect().catch((error)=>{
|
|
145
|
+
this.logger.warn(`RabbitMQ reconnect failed: ${error.message}`);
|
|
146
|
+
this.scheduleReconnect();
|
|
147
|
+
});
|
|
148
|
+
}, this.config.reconnectDelay ?? DEFAULT_RECONNECT_DELAY);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Drops the current channel and connection. The connection is closed even when
|
|
152
|
+
* it was only the channel that died, otherwise every channel level failure
|
|
153
|
+
* would leave one more live connection behind on the broker.
|
|
154
|
+
*/ async teardown() {
|
|
155
|
+
const { channel, connection } = this;
|
|
156
|
+
this.channel = undefined;
|
|
157
|
+
this.connection = undefined;
|
|
158
|
+
this.consuming = false;
|
|
159
|
+
channel?.removeAllListeners?.();
|
|
160
|
+
connection?.removeAllListeners?.();
|
|
161
|
+
await channel?.close().catch(()=>{});
|
|
162
|
+
await connection?.close().catch(()=>{});
|
|
163
|
+
}
|
|
164
|
+
constructor(config = {}){
|
|
165
|
+
_define_property(this, "config", void 0);
|
|
166
|
+
_define_property(this, "name", void 0);
|
|
167
|
+
_define_property(this, "logger", void 0);
|
|
168
|
+
_define_property(this, "handlers", void 0);
|
|
169
|
+
_define_property(this, "boundPatterns", void 0);
|
|
170
|
+
_define_property(this, "connection", void 0);
|
|
171
|
+
_define_property(this, "channel", void 0);
|
|
172
|
+
_define_property(this, "consuming", void 0);
|
|
173
|
+
_define_property(this, "closed", void 0);
|
|
174
|
+
_define_property(this, "reconnectTimer", void 0);
|
|
175
|
+
this.config = config;
|
|
176
|
+
this.name = 'rabbitmq';
|
|
177
|
+
this.logger = new _common.Logger(RabbitMqTransport.name);
|
|
178
|
+
this.handlers = [];
|
|
179
|
+
this.boundPatterns = new Set();
|
|
180
|
+
this.consuming = false;
|
|
181
|
+
this.closed = false;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
_eventtransportregistry.EventTransportRegistry.register('rabbitmq', (config)=>new RabbitMqTransport(config));
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _common = require("@nestjs/common");
|
|
6
|
+
const _eventactionenum = require("../../enums/event-action.enum");
|
|
7
|
+
const _rabbitmqtransportoptional = require("./rabbitmq-transport.optional");
|
|
8
|
+
const mockLoadOptionalModule = jest.fn();
|
|
9
|
+
jest.mock('../../utils/optional-module.util', ()=>({
|
|
10
|
+
loadOptionalModule: (...args)=>mockLoadOptionalModule(...args)
|
|
11
|
+
}));
|
|
12
|
+
function buildEvent() {
|
|
13
|
+
return {
|
|
14
|
+
id: 'event-1',
|
|
15
|
+
name: 'storage.folder.created',
|
|
16
|
+
module: 'storage',
|
|
17
|
+
entity: 'folder',
|
|
18
|
+
action: _eventactionenum.EventAction.CREATED,
|
|
19
|
+
payload: {
|
|
20
|
+
ids: [
|
|
21
|
+
'1'
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
actor: {},
|
|
25
|
+
occurredAt: '2026-01-01T00:00:00.000Z',
|
|
26
|
+
version: 1
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function createChannelMock() {
|
|
30
|
+
return {
|
|
31
|
+
assertExchange: jest.fn().mockResolvedValue(undefined),
|
|
32
|
+
assertQueue: jest.fn().mockResolvedValue(undefined),
|
|
33
|
+
prefetch: jest.fn().mockResolvedValue(undefined),
|
|
34
|
+
bindQueue: jest.fn().mockResolvedValue(undefined),
|
|
35
|
+
consume: jest.fn().mockResolvedValue(undefined),
|
|
36
|
+
publish: jest.fn(),
|
|
37
|
+
ack: jest.fn(),
|
|
38
|
+
nack: jest.fn(),
|
|
39
|
+
on: jest.fn(),
|
|
40
|
+
removeAllListeners: jest.fn(),
|
|
41
|
+
close: jest.fn().mockResolvedValue(undefined)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** A reconnect must land on a fresh channel, so every createChannel yields one */ function createAmqpMock() {
|
|
45
|
+
const channels = [];
|
|
46
|
+
const connection = {
|
|
47
|
+
createChannel: jest.fn(async ()=>{
|
|
48
|
+
const channel = createChannelMock();
|
|
49
|
+
channels.push(channel);
|
|
50
|
+
return channel;
|
|
51
|
+
}),
|
|
52
|
+
on: jest.fn(),
|
|
53
|
+
removeAllListeners: jest.fn(),
|
|
54
|
+
close: jest.fn().mockResolvedValue(undefined)
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
channels,
|
|
58
|
+
connection,
|
|
59
|
+
connect: jest.fn().mockResolvedValue(connection),
|
|
60
|
+
get channel () {
|
|
61
|
+
return channels[channels.length - 1];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function listenerFor(target, event) {
|
|
66
|
+
const registered = target.on.mock.calls.find(([name])=>name === event);
|
|
67
|
+
if (!registered) throw new Error(`No '${event}' listener was registered`);
|
|
68
|
+
return registered[1];
|
|
69
|
+
}
|
|
70
|
+
describe('RabbitMqTransport', ()=>{
|
|
71
|
+
let amqp;
|
|
72
|
+
let warnSpy;
|
|
73
|
+
let errorSpy;
|
|
74
|
+
beforeEach(()=>{
|
|
75
|
+
amqp = createAmqpMock();
|
|
76
|
+
mockLoadOptionalModule.mockResolvedValue({
|
|
77
|
+
connect: amqp.connect
|
|
78
|
+
});
|
|
79
|
+
warnSpy = jest.spyOn(_common.Logger.prototype, 'warn').mockImplementation(()=>undefined);
|
|
80
|
+
errorSpy = jest.spyOn(_common.Logger.prototype, 'error').mockImplementation(()=>undefined);
|
|
81
|
+
});
|
|
82
|
+
afterEach(()=>{
|
|
83
|
+
warnSpy.mockRestore();
|
|
84
|
+
errorSpy.mockRestore();
|
|
85
|
+
jest.clearAllMocks();
|
|
86
|
+
});
|
|
87
|
+
it('should throw a descriptive error when amqplib is not installed', async ()=>{
|
|
88
|
+
mockLoadOptionalModule.mockRejectedValue(new Error('Cannot find module'));
|
|
89
|
+
await expect(new _rabbitmqtransportoptional.RabbitMqTransport().connect()).rejects.toThrow(_common.InternalServerErrorException);
|
|
90
|
+
});
|
|
91
|
+
it('should assert a topic exchange and a durable queue on connect', async ()=>{
|
|
92
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
93
|
+
exchange: 'ex',
|
|
94
|
+
queue: 'svc.events'
|
|
95
|
+
});
|
|
96
|
+
await transport.connect();
|
|
97
|
+
expect(amqp.channel.assertExchange).toHaveBeenCalledWith('ex', 'topic', {
|
|
98
|
+
durable: true
|
|
99
|
+
});
|
|
100
|
+
expect(amqp.channel.assertQueue).toHaveBeenCalledWith('svc.events', {
|
|
101
|
+
durable: true
|
|
102
|
+
});
|
|
103
|
+
expect(transport.isReady()).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
it('should publish with the event name as the routing key', async ()=>{
|
|
106
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
107
|
+
exchange: 'ex'
|
|
108
|
+
});
|
|
109
|
+
await transport.connect();
|
|
110
|
+
const event = buildEvent();
|
|
111
|
+
await transport.publish(event);
|
|
112
|
+
expect(amqp.channel.publish).toHaveBeenCalledWith('ex', 'storage.folder.created', expect.any(Buffer), expect.objectContaining({
|
|
113
|
+
messageId: 'event-1',
|
|
114
|
+
contentType: 'application/json'
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
it('should ignore publish before connect instead of throwing', async ()=>{
|
|
118
|
+
await expect(new _rabbitmqtransportoptional.RabbitMqTransport().publish(buildEvent())).resolves.toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
it('should translate the ** wildcard to the AMQP # wildcard when binding', async ()=>{
|
|
121
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
122
|
+
exchange: 'ex',
|
|
123
|
+
queue: 'q'
|
|
124
|
+
});
|
|
125
|
+
await transport.connect();
|
|
126
|
+
await transport.bind('storage.**');
|
|
127
|
+
expect(amqp.channel.bindQueue).toHaveBeenCalledWith('q', 'ex', 'storage.#');
|
|
128
|
+
});
|
|
129
|
+
it('should start consuming only once across several binds', async ()=>{
|
|
130
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport();
|
|
131
|
+
await transport.connect();
|
|
132
|
+
await transport.bind('storage.#');
|
|
133
|
+
await transport.bind('auth.#');
|
|
134
|
+
expect(amqp.channel.consume).toHaveBeenCalledTimes(1);
|
|
135
|
+
});
|
|
136
|
+
it('should hand a consumed message to its handlers and ack it', async ()=>{
|
|
137
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport();
|
|
138
|
+
const handler = jest.fn();
|
|
139
|
+
transport.onMessage(handler);
|
|
140
|
+
await transport.connect();
|
|
141
|
+
await transport.bind('storage.#');
|
|
142
|
+
const event = buildEvent();
|
|
143
|
+
const message = {
|
|
144
|
+
content: Buffer.from(JSON.stringify(event))
|
|
145
|
+
};
|
|
146
|
+
await amqp.channel.consume.mock.calls[0][1](message);
|
|
147
|
+
expect(handler).toHaveBeenCalledWith(event);
|
|
148
|
+
expect(amqp.channel.ack).toHaveBeenCalledWith(message);
|
|
149
|
+
});
|
|
150
|
+
it('should nack a message it cannot parse', async ()=>{
|
|
151
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport();
|
|
152
|
+
await transport.connect();
|
|
153
|
+
await transport.bind('storage.#');
|
|
154
|
+
const message = {
|
|
155
|
+
content: Buffer.from('not json')
|
|
156
|
+
};
|
|
157
|
+
await amqp.channel.consume.mock.calls[0][1](message);
|
|
158
|
+
expect(amqp.channel.nack).toHaveBeenCalledWith(message, false, false);
|
|
159
|
+
});
|
|
160
|
+
it('should close the channel and the connection', async ()=>{
|
|
161
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport();
|
|
162
|
+
await transport.connect();
|
|
163
|
+
await transport.close();
|
|
164
|
+
expect(amqp.channel.close).toHaveBeenCalled();
|
|
165
|
+
expect(amqp.connection.close).toHaveBeenCalled();
|
|
166
|
+
expect(transport.isReady()).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('should reconnect when the channel dies while the connection is still up', async ()=>{
|
|
169
|
+
jest.useFakeTimers();
|
|
170
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
171
|
+
reconnectDelay: 10
|
|
172
|
+
});
|
|
173
|
+
await transport.connect();
|
|
174
|
+
listenerFor(amqp.channels[0], 'close')();
|
|
175
|
+
expect(transport.isReady()).toBe(false);
|
|
176
|
+
await jest.advanceTimersByTimeAsync(10);
|
|
177
|
+
expect(amqp.connect).toHaveBeenCalledTimes(2);
|
|
178
|
+
expect(transport.isReady()).toBe(true);
|
|
179
|
+
jest.useRealTimers();
|
|
180
|
+
});
|
|
181
|
+
it('should close the surviving connection rather than leak it on a channel failure', async ()=>{
|
|
182
|
+
jest.useFakeTimers();
|
|
183
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
184
|
+
reconnectDelay: 10
|
|
185
|
+
});
|
|
186
|
+
await transport.connect();
|
|
187
|
+
listenerFor(amqp.channels[0], 'close')();
|
|
188
|
+
await jest.advanceTimersByTimeAsync(10);
|
|
189
|
+
expect(amqp.connection.close).toHaveBeenCalledTimes(1);
|
|
190
|
+
jest.useRealTimers();
|
|
191
|
+
});
|
|
192
|
+
it('should schedule a single reconnect when the channel and the connection both close', async ()=>{
|
|
193
|
+
jest.useFakeTimers();
|
|
194
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
195
|
+
reconnectDelay: 10
|
|
196
|
+
});
|
|
197
|
+
await transport.connect();
|
|
198
|
+
listenerFor(amqp.channels[0], 'close')();
|
|
199
|
+
listenerFor(amqp.connection, 'close')();
|
|
200
|
+
await jest.advanceTimersByTimeAsync(10);
|
|
201
|
+
expect(amqp.connect).toHaveBeenCalledTimes(2);
|
|
202
|
+
jest.useRealTimers();
|
|
203
|
+
});
|
|
204
|
+
it('should re-bind every pattern onto the new channel after a reconnect', async ()=>{
|
|
205
|
+
jest.useFakeTimers();
|
|
206
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
207
|
+
exchange: 'ex',
|
|
208
|
+
queue: 'q',
|
|
209
|
+
reconnectDelay: 10
|
|
210
|
+
});
|
|
211
|
+
await transport.connect();
|
|
212
|
+
await transport.bind('storage.**');
|
|
213
|
+
listenerFor(amqp.channels[0], 'close')();
|
|
214
|
+
await jest.advanceTimersByTimeAsync(10);
|
|
215
|
+
expect(amqp.channels[1].bindQueue).toHaveBeenCalledWith('q', 'ex', 'storage.#');
|
|
216
|
+
expect(amqp.channels[1].consume).toHaveBeenCalledTimes(1);
|
|
217
|
+
jest.useRealTimers();
|
|
218
|
+
});
|
|
219
|
+
it('should ack an in-flight delivery on the channel it arrived on', async ()=>{
|
|
220
|
+
jest.useFakeTimers();
|
|
221
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
222
|
+
reconnectDelay: 10
|
|
223
|
+
});
|
|
224
|
+
await transport.connect();
|
|
225
|
+
await transport.bind('storage.#');
|
|
226
|
+
const deliver = amqp.channels[0].consume.mock.calls[0][1];
|
|
227
|
+
listenerFor(amqp.channels[0], 'close')();
|
|
228
|
+
await jest.advanceTimersByTimeAsync(10);
|
|
229
|
+
await deliver({
|
|
230
|
+
content: Buffer.from(JSON.stringify(buildEvent()))
|
|
231
|
+
});
|
|
232
|
+
expect(amqp.channels[0].ack).toHaveBeenCalled();
|
|
233
|
+
expect(amqp.channels[1].ack).not.toHaveBeenCalled();
|
|
234
|
+
jest.useRealTimers();
|
|
235
|
+
});
|
|
236
|
+
it('should let a later bind retry consuming after the broker rejects consume', async ()=>{
|
|
237
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport();
|
|
238
|
+
await transport.connect();
|
|
239
|
+
amqp.channel.consume.mockRejectedValueOnce(new Error('CHANNEL_ERROR'));
|
|
240
|
+
await expect(transport.bind('storage.#')).rejects.toThrow('CHANNEL_ERROR');
|
|
241
|
+
await transport.bind('auth.#');
|
|
242
|
+
expect(amqp.channel.consume).toHaveBeenCalledTimes(2);
|
|
243
|
+
});
|
|
244
|
+
it('should stop reconnecting once it has been closed', async ()=>{
|
|
245
|
+
jest.useFakeTimers();
|
|
246
|
+
const transport = new _rabbitmqtransportoptional.RabbitMqTransport({
|
|
247
|
+
reconnectDelay: 10
|
|
248
|
+
});
|
|
249
|
+
await transport.connect();
|
|
250
|
+
await transport.close();
|
|
251
|
+
listenerFor(amqp.channels[0], 'close')();
|
|
252
|
+
await jest.advanceTimersByTimeAsync(50);
|
|
253
|
+
expect(amqp.connect).toHaveBeenCalledTimes(1);
|
|
254
|
+
jest.useRealTimers();
|
|
255
|
+
});
|
|
256
|
+
});
|