@quatrain/queue-amqp 1.2.12 → 1.2.13

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.md ADDED
@@ -0,0 +1,15 @@
1
+ # LICENSE UPDATE NOTICE
2
+
3
+ As of 01/01/2026, Quatrain Core is licensed under the **GNU Affero General Public License v3.0 (AGPL v3)**.
4
+ Previous versions remain under the MIT License.
5
+
6
+ ## Why AGPL?
7
+
8
+ We believe in open collaboration for the development ecosystem. The AGPL ensures that any modification or deployment of this BaaS stack, including over a network, benefits the entire community.
9
+
10
+ ## Commercial Services & Enterprise Usage
11
+
12
+ We provide official deployment services, technical training, and certification for Quatrain Core.
13
+ For organizations requiring a non-copyleft license (commercial license) or custom proprietary integrations, please contact the copyright holder: **Quatrain Technologies**.
14
+
15
+ Copyright © 2024-2026 Quatrain Technologies. All Rights Reserved.
@@ -5,5 +5,12 @@ export declare class AmqpQueueAdapter extends AbstractQueueAdapter {
5
5
  protected _connect(): Promise<ChannelModel>;
6
6
  protected _disconnect(): Promise<void>;
7
7
  send(data: any, topic: string): Promise<string>;
8
+ /**
9
+ * Listen to given queue and process messages with messageHandler function
10
+ * @param topic
11
+ * @param messageHandler
12
+ * @param params
13
+ * @returns
14
+ */
8
15
  listen(topic: string | undefined, messageHandler: Function, params?: any): Promise<amqplib.Replies.Consume>;
9
16
  }
@@ -45,6 +45,13 @@ class AmqpQueueAdapter extends queue_1.AbstractQueueAdapter {
45
45
  return String(id);
46
46
  });
47
47
  }
48
+ /**
49
+ * Listen to given queue and process messages with messageHandler function
50
+ * @param topic
51
+ * @param messageHandler
52
+ * @param params
53
+ * @returns
54
+ */
48
55
  listen(topic = this._params.topic, messageHandler, params) {
49
56
  return __awaiter(this, void 0, void 0, function* () {
50
57
  if (!topic) {
@@ -65,7 +72,10 @@ class AmqpQueueAdapter extends queue_1.AbstractQueueAdapter {
65
72
  queue_1.Queue.info(`Job #${concurrents} of ${concurrency > 0 ? concurrency : 'unlimited'} started`);
66
73
  try {
67
74
  // 1. Process the message synchronously (wait for it to finish)
68
- yield messageHandler(msg.content.toString(), params);
75
+ const result = yield messageHandler(msg.content.toString(), params);
76
+ if (result === false) {
77
+ throw new Error('messageHandler function failed, see status log in jobExecution for more information');
78
+ }
69
79
  // 2. Acknowledge the message only after successful processing
70
80
  channel.ack(msg);
71
81
  // 3. Check if we reached the limit of messages to process
package/package.json CHANGED
@@ -1,44 +1,42 @@
1
1
  {
2
- "name": "@quatrain/queue-amqp",
3
- "version": "1.2.12",
4
- "description": "Queue adapter for AMQP compatible queue",
5
- "main": "lib/index.js",
6
- "types": "lib/index.d.ts",
7
- "files": [
8
- "lib/",
9
- "README.md"
10
- ],
11
- "author": "Quatrain Développement SAS <developers@quatrain.com>",
12
- "license": "MIT",
13
- "devDependencies": {
14
- "@tsconfig/recommended": "^1.0.1",
15
- "@types/amqplib": "^0.10.8",
16
- "@types/fs-extra": "^11.0.4",
17
- "@types/jest": "^27.0.3",
18
- "@types/node": "^22.10.1",
19
- "@types/object-hash": "^3.0.6",
20
- "jest": "^27.4.7",
21
- "jest-node-exports-resolver": "^1.1.6",
22
- "trace-unhandled": "^2.0.1",
23
- "ts-jest": "^27.1.2",
24
- "ts-node": "^10.4.0",
25
- "typescript": "^5.1.5"
26
- },
27
- "dependencies": {
28
- "@cloudamqp/amqp-client": "^3.4.1",
29
- "@quatrain/core": "^1.1.23",
30
- "@quatrain/queue": "^1.1.19",
31
- "amqplib": "^0.10.9"
32
- },
33
- "scripts": {
34
- "test-ci": "jest --runInBand",
35
- "build": "tsc",
36
- "wbuild": "tsc --watch",
37
- "bump-to": "yarn version",
38
- "hash": "node ../../bin/hashFolder.js",
39
- "hash:persist": "yarn hash > .hash_latest.txt",
40
- "hash:compare": "yarn hash > .hash_newest.txt && cmp -s .hash_latest.txt .hash_newest.txt",
41
- "publish": "yarn hash:compare || yarn publish:process",
42
- "publish:process": "yarn version patch && yarn build && yarn npm publish --access public && yarn hash:persist"
43
- }
44
- }
2
+ "name": "@quatrain/queue-amqp",
3
+ "version": "1.2.13",
4
+ "description": "Queue adapter for AMQP compatible queue",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "bun": "src/index.ts",
8
+ "files": [
9
+ "LICENSE.md",
10
+ "src/",
11
+ "lib/",
12
+ "README.md"
13
+ ],
14
+ "author": "Quatrain Développement SAS <developers@quatrain.com>",
15
+ "license": "AGPL-3.0-only",
16
+ "devDependencies": {
17
+ "@tsconfig/recommended": "^1.0.1",
18
+ "@types/amqplib": "^0.10.8",
19
+ "@types/fs-extra": "^11.0.4",
20
+ "@types/jest": "^27.0.3",
21
+ "@types/node": "^22.10.1",
22
+ "@types/object-hash": "^3.0.6",
23
+ "jest": "^30.2.0",
24
+ "jest-node-exports-resolver": "^1.1.6",
25
+ "trace-unhandled": "^2.0.1",
26
+ "ts-jest": "^27.1.2",
27
+ "ts-node": "^10.4.0",
28
+ "typescript": "^5.1.5"
29
+ },
30
+ "dependencies": {
31
+ "@cloudamqp/amqp-client": "^3.4.1",
32
+ "@quatrain/core": "^1.1.43",
33
+ "@quatrain/queue": "^1.1.20",
34
+ "amqplib": "^0.10.9"
35
+ },
36
+ "scripts": {
37
+ "test-ci": "jest --runInBand",
38
+ "build": "tsc",
39
+ "wbuild": "tsc --watch",
40
+ "bump-to": "yarn version"
41
+ }
42
+ }
@@ -0,0 +1,150 @@
1
+ import { AmqpQueueAdapter } from './AmqpQueueAdapter'
2
+ import { AMQPClient, AMQPMessage } from '@cloudamqp/amqp-client'
3
+ import { Queue } from '@quatrain/queue'
4
+
5
+ // Mock the @cloudamqp/amqp-client library
6
+ jest.mock('@cloudamqp/amqp-client')
7
+
8
+ // Mock @quatrain/queue to spy on Queue.info
9
+ jest.mock('@quatrain/queue', () => ({
10
+ ...jest.requireActual('@quatrain/queue'),
11
+ Queue: {
12
+ info: jest.fn(),
13
+ },
14
+ }))
15
+
16
+ const mockPublish = jest
17
+ .fn()
18
+ .mockResolvedValue({ confirmId: 'mock-confirm-id' })
19
+ const mockSubscribe = jest.fn()
20
+ const mockQueue = jest.fn().mockResolvedValue({
21
+ publish: mockPublish,
22
+ subscribe: mockSubscribe,
23
+ })
24
+ const mockChannel = jest.fn().mockResolvedValue({
25
+ queue: mockQueue,
26
+ })
27
+ const mockConnect = jest.fn().mockResolvedValue({
28
+ channel: mockChannel,
29
+ })
30
+
31
+ const AMQPClientMock = AMQPClient as jest.Mock
32
+
33
+ AMQPClientMock.mockImplementation(() => {
34
+ return {
35
+ connect: mockConnect,
36
+ }
37
+ })
38
+
39
+ describe('AmqpQueueAdapter', () => {
40
+ const params = {
41
+ config: {
42
+ host: 'test-host',
43
+ user: 'test-user',
44
+ password: 'test-password',
45
+ port: 1234,
46
+ },
47
+ topic: 'default-topic',
48
+ }
49
+
50
+ beforeEach(() => {
51
+ // Clear all instances and calls to constructor and all methods:
52
+ AMQPClientMock.mockClear()
53
+ mockConnect.mockClear()
54
+ mockChannel.mockClear()
55
+ mockQueue.mockClear()
56
+ mockPublish.mockClear()
57
+ mockSubscribe.mockClear()
58
+ ;(Queue.info as jest.Mock).mockClear()
59
+ })
60
+
61
+ it('should construct with default and custom parameters', () => {
62
+ new AmqpQueueAdapter({ config: {} })
63
+ expect(AMQPClientMock).toHaveBeenCalledWith(
64
+ 'amqp://guest:guest@localhost:5672?frameMax=0'
65
+ )
66
+
67
+ new AmqpQueueAdapter(params)
68
+ expect(AMQPClientMock).toHaveBeenCalledWith(
69
+ 'amqp://test-user:test-password@test-host:1234?frameMax=0'
70
+ )
71
+ })
72
+
73
+ describe('send', () => {
74
+ it('should send a message to the specified topic', async () => {
75
+ const adapter = new AmqpQueueAdapter(params)
76
+ const data = { key: 'value' }
77
+ const topic = 'test-topic'
78
+
79
+ const confirmId = await adapter.send(data, topic)
80
+
81
+ expect(mockConnect).toHaveBeenCalledTimes(1)
82
+ expect(mockChannel).toHaveBeenCalledTimes(1)
83
+ expect(mockQueue).toHaveBeenCalledWith(topic)
84
+ expect(mockPublish).toHaveBeenCalledWith(
85
+ Buffer.from(JSON.stringify(data)),
86
+ { deliveryMode: 2 }
87
+ )
88
+ expect(Queue.info).toHaveBeenCalledWith(
89
+ `[AMQP] Sending message to ${topic}`
90
+ )
91
+ expect(Queue.info).toHaveBeenCalledWith(
92
+ `[AMQP] Message send with id mock-confirm-id`
93
+ )
94
+ expect(confirmId).toBe('mock-confirm-id')
95
+ })
96
+ })
97
+
98
+ describe('listen', () => {
99
+ it('should listen to the default topic if none is provided', async () => {
100
+ const adapter = new AmqpQueueAdapter(params)
101
+ const handler = jest.fn()
102
+
103
+ await adapter.listen(undefined, handler)
104
+
105
+ expect(mockConnect).toHaveBeenCalledTimes(1)
106
+ expect(mockChannel).toHaveBeenCalledTimes(1)
107
+ expect(mockQueue).toHaveBeenCalledWith(params.topic)
108
+ expect(mockSubscribe).toHaveBeenCalledWith(
109
+ { noAck: true },
110
+ expect.any(Function)
111
+ )
112
+ })
113
+
114
+ it('should listen to a specific topic', async () => {
115
+ const adapter = new AmqpQueueAdapter(params)
116
+ const handler = jest.fn()
117
+ const topic = 'specific-topic'
118
+
119
+ await adapter.listen(topic, handler)
120
+
121
+ expect(mockQueue).toHaveBeenCalledWith(topic)
122
+ })
123
+
124
+ it('should throw an error if no topic is available', async () => {
125
+ const adapter = new AmqpQueueAdapter({ config: {} })
126
+ const handler = jest.fn()
127
+
128
+ await expect(adapter.listen(undefined, handler)).rejects.toThrow(
129
+ 'No topic provided for listening.'
130
+ )
131
+ })
132
+
133
+ it('should call the message handler with the message body', async () => {
134
+ const adapter = new AmqpQueueAdapter(params)
135
+ const handler = jest.fn()
136
+ const messageContent = { data: 'test message' }
137
+ const message = {
138
+ bodyToString: () => JSON.stringify(messageContent),
139
+ } as AMQPMessage
140
+
141
+ mockSubscribe.mockImplementation(async (options, callback) => {
142
+ await callback(message)
143
+ })
144
+
145
+ await adapter.listen('any-topic', handler)
146
+
147
+ expect(handler).toHaveBeenCalledWith(JSON.stringify(messageContent))
148
+ })
149
+ })
150
+ })
@@ -0,0 +1,127 @@
1
+ import { AbstractQueueAdapter, Queue } from '@quatrain/queue'
2
+ import amqplib, { ChannelModel, ConsumeMessage, Message } from 'amqplib'
3
+
4
+ export class AmqpQueueAdapter extends AbstractQueueAdapter {
5
+ protected _client: ChannelModel | undefined
6
+
7
+ protected async _connect(): Promise<ChannelModel> {
8
+ if (!this._client) {
9
+ const {
10
+ host = 'localhost',
11
+ user = 'guest',
12
+ password = 'guest',
13
+ port = 5672,
14
+ } = this._params.config || {}
15
+
16
+ this._client = await amqplib.connect(
17
+ `amqp://${user}:${password}@${host}:${port}`
18
+ )
19
+ }
20
+
21
+ return this._client
22
+ }
23
+
24
+ protected async _disconnect() {
25
+ await this._client?.close()
26
+ }
27
+
28
+ async send(data: any, topic: string): Promise<string> {
29
+ await this._connect()
30
+
31
+ const channel = await this._client?.createChannel()
32
+
33
+ const dataBuffer = Buffer.from(JSON.stringify(data))
34
+ Queue.info(`[AMQP] Sending message to ${topic}`)
35
+
36
+ channel?.sendToQueue(topic, dataBuffer)
37
+ channel?.close()
38
+
39
+ const id = Date.now() // fake
40
+ Queue.info(`[AMQP] Message send with id ${id}`)
41
+
42
+ return String(id)
43
+ }
44
+
45
+ /**
46
+ * Listen to given queue and process messages with messageHandler function
47
+ * @param topic
48
+ * @param messageHandler
49
+ * @param params
50
+ * @returns
51
+ */
52
+ async listen(
53
+ topic: string | undefined = this._params.topic,
54
+ messageHandler: Function,
55
+ params?: any
56
+ ) {
57
+ if (!topic) {
58
+ throw new Error(`No topic provided for listening.`)
59
+ }
60
+
61
+ const concurrency = params?.concurrency || 0
62
+
63
+ Queue.info(
64
+ `Starting to listen to topic ${topic} with max concurrency set to ${concurrency}`
65
+ )
66
+
67
+ let concurrents = 0
68
+ const client = await this._connect()
69
+ const channel = await client.createChannel()
70
+
71
+ await channel.assertQueue(topic, { durable: true, autoDelete: false })
72
+ await channel.prefetch(concurrency) // only get one message at a time
73
+
74
+ return channel.consume(
75
+ topic,
76
+ async (msg: ConsumeMessage | null) => {
77
+ if (msg === null || !channel) {
78
+ return
79
+ }
80
+
81
+ concurrents++
82
+ Queue.info(
83
+ `Job #${concurrents} of ${
84
+ concurrency > 0 ? concurrency : 'unlimited'
85
+ } started`
86
+ )
87
+
88
+ try {
89
+ // 1. Process the message synchronously (wait for it to finish)
90
+ const result = await messageHandler(
91
+ msg.content.toString(),
92
+ params
93
+ )
94
+
95
+ if (result === false) {
96
+ throw new Error(
97
+ 'messageHandler function failed, see status log in jobExecution for more information'
98
+ )
99
+ }
100
+
101
+ // 2. Acknowledge the message only after successful processing
102
+ channel.ack(msg)
103
+
104
+ // 3. Check if we reached the limit of messages to process
105
+ if (concurrency > 0 && concurrents >= concurrency) {
106
+ Queue.info(
107
+ `Reached max concurrency of ${concurrency}, disconnecting from queue`
108
+ )
109
+ await channel.close()
110
+ await client.close()
111
+ process.exit(0)
112
+ }
113
+ } catch (err) {
114
+ Queue.error(
115
+ `messageHandler function failed with message: ${
116
+ (err as Error).message
117
+ }`
118
+ )
119
+ // Negative acknowledge the message so it can be requeued or handled by DLQ
120
+ channel.nack(msg)
121
+ process.exit(1)
122
+ }
123
+ },
124
+ { noAck: false } // disable auto-hack
125
+ )
126
+ }
127
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { AmqpQueueAdapter } from './AmqpQueueAdapter'
2
+
3
+ export { AmqpQueueAdapter }