consumer-pgmq 2.0.1 → 3.0.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/README.md CHANGED
@@ -40,7 +40,13 @@ yarn add consumer-pgmq
40
40
  - poolSize: The number of consumers. PS: this is the number of consumers that will be created to consume the messages and
41
41
  if you use read consume type, the pool size is the number of messages will get at the same time.
42
42
  - timeMsWaitBeforeNextPolling: The time in milliseconds to wait before the next polling
43
- - enabledPolling: The enabled polling. PS: if true, the consumer will poll the message, if false, the consumer will consume the message one time and stop. PS: is required to the versions more than 1.0.5
43
+ - enabledPolling: The enabled polling. PS: if true, the consumer will poll the message, if false, the consumer will consume the message one time and stop. PS: is required to the versions more than 1.0.5.
44
+ - queueNameDlq: The name of the dead letter queue. PS: recommended to set the same name of the queue, but suffix with '_dlq'. For example: **messages_dlq**
45
+ - totalRetriesBeforeSendToDlq: The total retries before send to dlq. For example: if you set totalRetriesBeforeSendToDlq equal 2, the message will be sent to dlq if the handler fails 2 times, so the third time the message will be sent to dlq and remove the main queue to avoid infinite retries.
46
+
47
+ ## Extra points to know when use the dlq feature
48
+ - The dead letter queue no work If you setted the consumerType option with value 'pop', because the pop get the message and remove from queue at same time, so if failed when you are processing you lose the message.
49
+ - Recommendation no set lower value to the option 'visibilityTime' if you are using the dead letter queue feature. For example: set visibilityTime value lower than 30 seconds, because if the message wasn't delete and the message be available again the consumer application can consume the message again.
44
50
 
45
51
  ## Events
46
52
 
@@ -89,11 +95,13 @@ async function start() {
89
95
  const consumer = new Consumer(
90
96
  {
91
97
  queueName: 'subscriptions',
92
- visibilityTime: 15,
98
+ visibilityTime: 30,
93
99
  consumeType: "read",
94
- poolSize: 4,
100
+ poolSize: 8,
95
101
  timeMsWaitBeforeNextPolling: 1000,
96
- enabledPolling: false
102
+ enabledPolling: true,
103
+ queueNameDlq: "subscriptions_dlq",
104
+ totalRetriesBeforeSendToDlq: 2
97
105
  },
98
106
  async function (message: { [key: string]: any }, signal): Promise<void> {
99
107
  try {
@@ -143,33 +151,39 @@ start()
143
151
  import { config } from "dotenv"
144
152
  config()
145
153
 
146
- import { Consumer, PostgresQueueDriver } from "consumer-pgmq"
147
- import timersPromises from "node:timers/promises";
148
- import knex from 'knex'
154
+ import Consumer from '../src/consumer';
155
+ import PostgresQueueDriver from '../src/queueDriver/PostgresQueueDriver';
156
+
157
+ import { Client } from 'pg'
149
158
 
150
159
  async function start() {
151
- const connection = knex({
152
- client: 'pg',
153
- connection: {
154
- host: process.env.POSTGRES_HOST,
155
- database: process.env.POSTGRES_DATABASE,
156
- password: process.env.POSTGRES_PASSWORD,
157
- port: Number(process.env.POSTGRES_PORT),
158
- user: process.env.POSTGRES_USER,
159
- ssl: false
160
- }
161
- });
162
160
 
163
- const postgresQueueDriver = new PostgresQueueDriver(connection, "schema_name_here")
161
+ const pgClient = new Client({
162
+ host: process.env.POSTGRES_HOST,
163
+ database: process.env.POSTGRES_DATABASE,
164
+ password: process.env.POSTGRES_PASSWORD,
165
+ port: Number(process.env.POSTGRES_PORT),
166
+ user: process.env.POSTGRES_USER,
167
+ ssl: false,
168
+ })
169
+
170
+ await pgClient.connect()
171
+
172
+
173
+ const postgresQueueDriver = new PostgresQueueDriver(
174
+ pgClient, "pgmq"
175
+ )
164
176
 
165
177
  const consumer = new Consumer(
166
178
  {
167
179
  queueName: 'subscriptions',
168
- visibilityTime: 15,
180
+ visibilityTime: 30,
169
181
  consumeType: "read",
170
- poolSize: 4,
182
+ poolSize: 8,
171
183
  timeMsWaitBeforeNextPolling: 1000,
172
- enabledPolling: false
184
+ enabledPolling: true,
185
+ queueNameDlq: "subscriptions_dlq",
186
+ totalRetriesBeforeSendToDlq: 2
173
187
  },
174
188
  async function (message: { [key: string]: any }, signal): Promise<void> {
175
189
  try {
package/dist/consumer.js CHANGED
@@ -8,9 +8,17 @@ const READ = "read";
8
8
  class Consumer extends events_1.EventEmitter {
9
9
  constructor(options, callback, client) {
10
10
  super();
11
+ this.setTimeoutId = null;
11
12
  this.options = options;
12
13
  this.callback = callback;
13
14
  this.client = client;
15
+ this.valideOptions();
16
+ this.setTimeoutId = null;
17
+ }
18
+ valideOptions() {
19
+ if (this.options.queueNameDlq && !this.options.totalRetriesBeforeSendToDlq) {
20
+ throw new Error("The option totalRetriesBeforeSendToDlq is required when queueNameDlq is set");
21
+ }
14
22
  }
15
23
  /**
16
24
  * Get the message
@@ -51,6 +59,9 @@ class Consumer extends events_1.EventEmitter {
51
59
  * @private
52
60
  */
53
61
  async pollMessage() {
62
+ if (this.setTimeoutId) {
63
+ clearTimeout(this.setTimeoutId);
64
+ }
54
65
  let promises = [];
55
66
  try {
56
67
  const { data, error } = await this.getMessage();
@@ -58,19 +69,35 @@ class Consumer extends events_1.EventEmitter {
58
69
  throw error;
59
70
  }
60
71
  if (data.length === 0 && this.options.enabledPolling) {
61
- setTimeout(() => this.pollMessage(), (this.options.timeMsWaitBeforeNextPolling || 1000) * 10);
72
+ this.setTimeoutId = setTimeout(() => this.pollMessage(), (this.options.timeMsWaitBeforeNextPolling || 1000) * 10);
62
73
  return;
63
74
  }
64
75
  const controller = new AbortController();
65
76
  const signal = controller.signal;
66
- for (let i = 0; i < (data.length || 1); i++) {
77
+ for (let i = 0; i < data.length; i++) {
78
+ const hasSendToDlq = data[i] &&
79
+ this.options.queueNameDlq &&
80
+ this.options.totalRetriesBeforeSendToDlq &&
81
+ data[i].read_ct > this.options.totalRetriesBeforeSendToDlq;
82
+ if (hasSendToDlq) {
83
+ promises.push(this.client.send(
84
+ // @ts-ignore
85
+ this.options.queueNameDlq, data[i].message, signal).then(async () => {
86
+ await this.deleteMessage(data[i], signal);
87
+ this.emit('send-to-dlq', data[i]);
88
+ }));
89
+ continue;
90
+ }
67
91
  promises.push(this.callback(data[i].message, signal).then(async () => {
68
92
  await this.deleteMessage(data[i], signal);
69
93
  this.emit('finish', data[i]);
70
94
  }));
71
95
  }
72
- setTimeout(() => controller.abort(), (this.options.visibilityTime || 1) * 1000);
73
- await Promise.allSettled(promises);
96
+ const timeoutId = setTimeout(() => controller.abort(), (this.options.visibilityTime || 1) * 1000);
97
+ if (promises.length > 0) {
98
+ await Promise.allSettled(promises);
99
+ }
100
+ clearTimeout(timeoutId);
74
101
  promises = [];
75
102
  }
76
103
  catch (err) {
@@ -85,7 +112,7 @@ class Consumer extends events_1.EventEmitter {
85
112
  if (!this.options.enabledPolling) {
86
113
  return;
87
114
  }
88
- setTimeout(() => this.pollMessage(), this.options.timeMsWaitBeforeNextPolling || 1000);
115
+ this.setTimeoutId = setTimeout(() => this.pollMessage(), this.options.timeMsWaitBeforeNextPolling || 1000);
89
116
  }
90
117
  }
91
118
  /**
@@ -5,6 +5,27 @@ class PostgresQueueDriver {
5
5
  this.connection = connection;
6
6
  this.schema = schema;
7
7
  }
8
+ /**
9
+ * Send the message
10
+ * @param queueName The name of the queue
11
+ * @param message The message
12
+ * @returns Promise<{ error: any }>
13
+ */
14
+ async send(queueName, message) {
15
+ try {
16
+ await this.connection.query(`
17
+ SELECT * FROM ${this.schema}.send(
18
+ queue_name => $1,
19
+ msg => $2,
20
+ delay => $3
21
+ );
22
+ `, [queueName, message, 1]);
23
+ return { error: null };
24
+ }
25
+ catch (error) {
26
+ return { error };
27
+ }
28
+ }
8
29
  /**
9
30
  * Get the message
10
31
  * @param queueName The name of the queue
@@ -14,11 +35,11 @@ class PostgresQueueDriver {
14
35
  */
15
36
  async get(queueName, visibilityTime, totalMessages) {
16
37
  try {
17
- const register = await this.connection.raw(`
38
+ const register = await this.connection.query(`
18
39
  SELECT * FROM ${this.schema}.read(
19
- queue_name => ?,
20
- vt => ?,
21
- qty => ?
40
+ queue_name => $1,
41
+ vt => $2,
42
+ qty => $3
22
43
  );
23
44
  `, [queueName, visibilityTime, totalMessages]);
24
45
  if (!register.rows) {
@@ -37,10 +58,10 @@ class PostgresQueueDriver {
37
58
  */
38
59
  async pop(queueName) {
39
60
  try {
40
- const register = await this.connection.raw(`
61
+ const register = await this.connection.query(`
41
62
  SELECT * FROM ${this.schema}.pop(
42
- queue_name => ?
43
- );
63
+ queue_name => $1
64
+ )
44
65
  `, [queueName]);
45
66
  if (!register.rows) {
46
67
  return { data: [], error: null };
@@ -59,10 +80,10 @@ class PostgresQueueDriver {
59
80
  */
60
81
  async delete(queueName, messageID) {
61
82
  try {
62
- await this.connection.raw(`
83
+ await this.connection.query(`
63
84
  SELECT * FROM ${this.schema}.delete(
64
- queue_name => ?,
65
- msg_id => ?
85
+ queue_name => $1,
86
+ msg_id => $2
66
87
  );
67
88
  `, [queueName, messageID]);
68
89
  return { error: null };
@@ -4,6 +4,19 @@ class SupabaseQueueDriver {
4
4
  constructor(supabase) {
5
5
  this.supabase = supabase;
6
6
  }
7
+ /**
8
+ * Send the message
9
+ * @param queueName The name of the queue
10
+ * @param message The message
11
+ * @returns Promise<{ error: any }>
12
+ */
13
+ async send(queueName, message) {
14
+ const { error } = await this.supabase.rpc("send", {
15
+ queue_name: queueName,
16
+ message: message
17
+ });
18
+ return { error };
19
+ }
7
20
  /**
8
21
  * Get the message
9
22
  * @param queueName The name of the queue
@@ -3,70 +3,66 @@ config()
3
3
 
4
4
  import Consumer from '../src/consumer';
5
5
  import PostgresQueueDriver from '../src/queueDriver/PostgresQueueDriver';
6
- import timersPromises from "node:timers/promises";
7
- import knex from 'knex'
6
+
7
+ import { Client } from 'pg'
8
8
 
9
9
  async function start() {
10
- const connection = knex({
11
- client: 'pg',
12
- connection: {
13
- host: process.env.POSTGRES_HOST,
14
- database: process.env.POSTGRES_DATABASE,
15
- password: process.env.POSTGRES_PASSWORD,
16
- port: Number(process.env.POSTGRES_PORT),
17
- user: process.env.POSTGRES_USER,
18
- ssl: false,
19
- }
20
- });
21
10
 
22
- const postgresQueueDriver = new PostgresQueueDriver(connection, "pgmq")
11
+ const pgClient = new Client({
12
+ host: process.env.POSTGRES_HOST,
13
+ database: process.env.POSTGRES_DATABASE,
14
+ password: process.env.POSTGRES_PASSWORD,
15
+ port: Number(process.env.POSTGRES_PORT),
16
+ user: process.env.POSTGRES_USER,
17
+ ssl: false,
18
+ })
19
+
20
+ await pgClient.connect()
21
+
22
+
23
+ const postgresQueueDriver = new PostgresQueueDriver(
24
+ pgClient, "pgmq"
25
+ )
26
+
23
27
 
24
28
  const consumer = new Consumer(
25
29
  {
26
30
  queueName: 'subscriptions',
27
- visibilityTime: 15,
31
+ visibilityTime: 30,
28
32
  consumeType: "read",
29
- poolSize: 4,
33
+ poolSize: 8,
30
34
  timeMsWaitBeforeNextPolling: 1000,
31
- enabledPolling: true
35
+ enabledPolling: true,
36
+ queueNameDlq: "subscriptions_dlq",
37
+ totalRetriesBeforeSendToDlq: 2
32
38
  },
33
39
  async function (message: { [key: string]: any }, signal): Promise<void> {
34
- try {
35
- console.log(message)
36
- // const url = "https://jsonplaceholder.typicode.com/todos/1";
37
- // await timersPromises.setTimeout(100, null, { signal });
38
- // console.log("Fetching data...");
39
- // const response = await fetch(url, { signal });
40
- // const todo = await response.json();
41
- // console.log("Todo:", todo);
42
- } catch (error: any) {
43
- if (error.name === "AbortError") {
44
- console.log("Operation aborted");
45
- } else {
46
- console.error("Error:", error);
47
- }
48
- }
40
+ console.log(message)
41
+ throw new Error("Error in message")
42
+ // const url = "https://jsonplaceholder.typicode.com/todos/1";
43
+ // await timersPromises.setTimeout(100, null, { signal });
44
+ // console.log("Fetching data...");
45
+ // const response = await fetch(url, { signal });
46
+ // const todo = await response.json();
47
+ // console.log("Todo:", todo);
49
48
  },
50
49
  postgresQueueDriver
51
50
  );
52
51
 
53
- // consumer.on('finish', (message: { [key: string]: any }) => {
54
- // console.log('Consumed message =>', message);
55
- // });
56
-
57
- consumer.on("abort-error", (err) => {
58
- console.log("Abort error =>", err)
52
+ consumer.on("send-to-dlq", (message: { [key: string]: any }) => {
53
+ console.log("Send to DLQ =>", message)
59
54
  })
60
55
 
61
56
  consumer.on('error', (err: Error) => {
62
- if (err.message.includes("TypeError: fetch failed")) {
63
- console.log(err)
64
- process.exit(1);
65
- }
66
57
  console.error('Error consuming message:', err.message);
67
58
  });
68
59
 
69
- consumer.start();
60
+ await consumer.start();
61
+
62
+ process.on("SIGINT", async () => {
63
+ await pgClient.end()
64
+ process.exit(0)
65
+ })
70
66
 
71
67
  }
72
68
 
@@ -21,33 +21,30 @@ const supabaseQueueDriver = new SupabaseQueueDriver(
21
21
  supabase as unknown as SupabaseClient
22
22
  )
23
23
 
24
-
25
- import timersPromises from "node:timers/promises";
26
-
27
24
  async function start() {
28
- // for (let i = 0; i < 200; i++) {
29
- // await supabase.rpc("send", {
30
- // queue_name: "subscriptions",
31
- // message: { "message": `Message triggered at ${Date.now()}` }
32
- // });
33
- // }
34
- // console.log("Total messages sent: ", 200)
25
+ for (let i = 0; i < 50; i++) {
26
+ await supabase.rpc("send", {
27
+ queue_name: "subscriptions",
28
+ message: { "message": `Message triggered at ${Date.now()}` }
29
+ });
30
+ }
31
+ console.log("Total messages sent: ", 50)
35
32
 
36
33
  const consumer = new Consumer(
37
34
  {
38
35
  queueName: 'subscriptions',
39
- visibilityTime: 15,
36
+ visibilityTime: 30,
40
37
  consumeType: "read",
41
38
  poolSize: 8,
42
39
  timeMsWaitBeforeNextPolling: 1000,
43
- enabledPolling: false
40
+ enabledPolling: true,
41
+ queueNameDlq: "subscriptions_dlq",
42
+ totalRetriesBeforeSendToDlq: 2
44
43
  },
45
44
  async function (message: { [key: string]: any }, signal): Promise<void> {
46
45
  try {
47
- if (message.error) {
48
- throw new Error("Error in message")
49
- }
50
46
  console.log(message)
47
+ throw new Error("Error in message")
51
48
  } catch (error: any) {
52
49
  throw error
53
50
  }
@@ -59,6 +56,9 @@ async function start() {
59
56
  // console.log('Consumed message =>', message);
60
57
  // });
61
58
 
59
+ consumer.on("send-to-dlq", (message: { [key: string]: any }) => {
60
+ console.log("Send to DLQ =>", message)
61
+ })
62
62
  consumer.on("abort-error", (err) => {
63
63
  console.log("Abort error =>", err)
64
64
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "consumer-pgmq",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "The consumer of Supabase pgmq",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",
@@ -22,6 +22,7 @@
22
22
  "packageManager": "pnpm@10.7.1",
23
23
  "devDependencies": {
24
24
  "@types/jest": "^30.0.0",
25
+ "@types/pg": "^8.15.5",
25
26
  "ts-jest": "^29.4.1",
26
27
  "ts-node-dev": "^2.0.0",
27
28
  "typescript": "^5.9.2"
@@ -30,7 +31,6 @@
30
31
  "@supabase/supabase-js": "^2.56.0",
31
32
  "dotenv": "^17.2.1",
32
33
  "jest": "^30.1.1",
33
- "knex": "^3.1.0",
34
34
  "pg": "^8.16.3"
35
35
  },
36
36
  "engines": {
package/src/consumer.ts CHANGED
@@ -22,6 +22,8 @@ class Consumer extends EventEmitter {
22
22
  */
23
23
  private client: QueueDriver
24
24
 
25
+ private setTimeoutId: NodeJS.Timeout | null = null;
26
+
25
27
  constructor(
26
28
  options: Options,
27
29
  callback: HandlerCallback,
@@ -31,6 +33,14 @@ class Consumer extends EventEmitter {
31
33
  this.options = options;
32
34
  this.callback = callback;
33
35
  this.client = client;
36
+ this.valideOptions();
37
+ this.setTimeoutId = null;
38
+ }
39
+
40
+ private valideOptions() {
41
+ if (this.options.queueNameDlq && !this.options.totalRetriesBeforeSendToDlq) {
42
+ throw new Error("The option totalRetriesBeforeSendToDlq is required when queueNameDlq is set");
43
+ }
34
44
  }
35
45
 
36
46
  /**
@@ -84,6 +94,9 @@ class Consumer extends EventEmitter {
84
94
  * @private
85
95
  */
86
96
  private async pollMessage() {
97
+ if (this.setTimeoutId) {
98
+ clearTimeout(this.setTimeoutId);
99
+ }
87
100
  let promises: Promise<any>[] = [];
88
101
 
89
102
  try {
@@ -93,18 +106,39 @@ class Consumer extends EventEmitter {
93
106
  }
94
107
 
95
108
  if (data.length === 0 && this.options.enabledPolling) {
96
- setTimeout(
109
+ this.setTimeoutId = setTimeout(
97
110
  () => this.pollMessage(),
98
111
  (this.options.timeMsWaitBeforeNextPolling || 1000) * 10
99
112
  );
100
113
  return;
101
114
  }
102
115
 
103
-
104
116
  const controller = new AbortController();
105
117
  const signal = controller.signal;
106
118
 
107
- for (let i = 0; i < (data.length || 1); i++) {
119
+ for (let i = 0; i < data.length; i++) {
120
+ const hasSendToDlq = data[i] &&
121
+ this.options.queueNameDlq &&
122
+ this.options.totalRetriesBeforeSendToDlq &&
123
+ data[i].read_ct > this.options.totalRetriesBeforeSendToDlq
124
+ if (
125
+ hasSendToDlq
126
+ ) {
127
+ promises.push(
128
+ this.client.send(
129
+ // @ts-ignore
130
+ this.options.queueNameDlq,
131
+ data[i].message,
132
+ signal
133
+ ).then(async () => {
134
+ await this.deleteMessage(data[i], signal);
135
+ this.emit('send-to-dlq', data[i]);
136
+ })
137
+ )
138
+ continue;
139
+ }
140
+
141
+
108
142
  promises.push(
109
143
  this.callback(data[i].message, signal).then(async () => {
110
144
  await this.deleteMessage(data[i], signal);
@@ -113,8 +147,12 @@ class Consumer extends EventEmitter {
113
147
  );
114
148
  }
115
149
 
116
- setTimeout(() => controller.abort(), (this.options.visibilityTime || 1) * 1000);
117
- await Promise.allSettled(promises);
150
+ const timeoutId = setTimeout(() => controller.abort(), (this.options.visibilityTime || 1) * 1000);
151
+ if (promises.length > 0) {
152
+ await Promise.allSettled(promises);
153
+ }
154
+
155
+ clearTimeout(timeoutId);
118
156
  promises = [];
119
157
  } catch (err: any) {
120
158
  if (err.name === "AbortError") {
@@ -126,7 +164,7 @@ class Consumer extends EventEmitter {
126
164
  if (!this.options.enabledPolling) {
127
165
  return;
128
166
  }
129
- setTimeout(() => this.pollMessage(), this.options.timeMsWaitBeforeNextPolling || 1000);
167
+ this.setTimeoutId = setTimeout(() => this.pollMessage(), this.options.timeMsWaitBeforeNextPolling || 1000);
130
168
  }
131
169
  }
132
170
 
@@ -1,14 +1,40 @@
1
- import { Knex } from "knex";
2
1
  import { Message, QueueDriver } from "../type";
2
+ import { Client } from "pg";
3
3
 
4
4
  class PostgresQueueDriver implements QueueDriver {
5
5
 
6
6
 
7
7
  constructor(
8
- private connection: Knex,
9
- private schema: string = "public"
8
+ private connection: Client,
9
+ private schema: string = "public",
10
10
  ) { }
11
11
 
12
+ /**
13
+ * Send the message
14
+ * @param queueName The name of the queue
15
+ * @param message The message
16
+ * @returns Promise<{ error: any }>
17
+ */
18
+ async send(
19
+ queueName: string,
20
+ message: { [key: string]: any; },
21
+ ): Promise<{ error: any; }> {
22
+ try {
23
+ await this.connection.query(`
24
+ SELECT * FROM ${this.schema}.send(
25
+ queue_name => $1,
26
+ msg => $2,
27
+ delay => $3
28
+ );
29
+ `, [queueName, message, 1]
30
+ )
31
+
32
+ return { error: null };
33
+ } catch (error) {
34
+ return { error };
35
+ }
36
+ }
37
+
12
38
  /**
13
39
  * Get the message
14
40
  * @param queueName The name of the queue
@@ -18,11 +44,11 @@ class PostgresQueueDriver implements QueueDriver {
18
44
  */
19
45
  async get(queueName: string, visibilityTime: number, totalMessages: number): Promise<{ data: Message[]; error: any; }> {
20
46
  try {
21
- const register = await this.connection.raw(`
47
+ const register = await this.connection.query(`
22
48
  SELECT * FROM ${this.schema}.read(
23
- queue_name => ?,
24
- vt => ?,
25
- qty => ?
49
+ queue_name => $1,
50
+ vt => $2,
51
+ qty => $3
26
52
  );
27
53
  `, [queueName, visibilityTime, totalMessages]
28
54
  )
@@ -45,10 +71,10 @@ class PostgresQueueDriver implements QueueDriver {
45
71
  */
46
72
  async pop(queueName: string): Promise<{ data: Message[]; error: any; }> {
47
73
  try {
48
- const register = await this.connection.raw(`
74
+ const register = await this.connection.query(`
49
75
  SELECT * FROM ${this.schema}.pop(
50
- queue_name => ?
51
- );
76
+ queue_name => $1
77
+ )
52
78
  `, [queueName]
53
79
  )
54
80
 
@@ -71,12 +97,13 @@ class PostgresQueueDriver implements QueueDriver {
71
97
  */
72
98
  async delete(queueName: string, messageID: number): Promise<{ error: any; }> {
73
99
  try {
74
- await this.connection.raw(`
100
+ await this.connection.query(`
75
101
  SELECT * FROM ${this.schema}.delete(
76
- queue_name => ?,
77
- msg_id => ?
102
+ queue_name => $1,
103
+ msg_id => $2
78
104
  );
79
- `, [queueName, messageID]
105
+ `, [queueName, messageID],
106
+
80
107
  )
81
108
 
82
109
  return { error: null };
@@ -7,6 +7,21 @@ class SupabaseQueueDriver implements QueueDriver {
7
7
  private supabase: SupabaseClient
8
8
  ) { }
9
9
 
10
+ /**
11
+ * Send the message
12
+ * @param queueName The name of the queue
13
+ * @param message The message
14
+ * @returns Promise<{ error: any }>
15
+ */
16
+ async send(queueName: string, message: { [key: string]: any; }): Promise<{ error: any; }> {
17
+ const { error } = await this.supabase.rpc("send", {
18
+ queue_name: queueName,
19
+ message: message
20
+ });
21
+
22
+ return { error };
23
+ }
24
+
10
25
  /**
11
26
  * Get the message
12
27
  * @param queueName The name of the queue
package/src/type.ts CHANGED
@@ -30,6 +30,16 @@ interface Options {
30
30
  * The enabled polling. PS: if true, the consumer will poll the message
31
31
  */
32
32
  enabledPolling: boolean;
33
+
34
+ /**
35
+ * The name of the queue DLQ
36
+ */
37
+ queueNameDlq?: string;
38
+
39
+ /**
40
+ * The total retries before send the message to DLQ. PS: if set queueNameDlq, this option is required
41
+ */
42
+ totalRetriesBeforeSendToDlq?: number;
33
43
  }
34
44
 
35
45
  /**
@@ -65,6 +75,18 @@ interface Message {
65
75
 
66
76
  interface QueueDriver {
67
77
 
78
+ /**
79
+ * Send the message
80
+ * @param queueName The name of the queue
81
+ * @param message The message
82
+ * @returns Promise<{ error: any }>
83
+ */
84
+ send(
85
+ queueName: string,
86
+ message: { [key: string]: any },
87
+ signal: AbortSignal
88
+ ): Promise<{ error: any }>;
89
+
68
90
  /**
69
91
  * Get the message
70
92
  * @param queueName The name of the queue
@@ -1,6 +1,5 @@
1
1
  import Consumer from '../src/consumer'
2
2
  import { Message, QueueDriver, Options } from '../src/type'
3
- import timersPromises from 'node:timers/promises'
4
3
 
5
4
  describe('Consumer', () => {
6
5
  let queueDriver: jest.Mocked<QueueDriver>
@@ -18,6 +17,7 @@ describe('Consumer', () => {
18
17
  get: jest.fn(),
19
18
  pop: jest.fn(),
20
19
  delete: jest.fn(),
20
+ send: jest.fn(),
21
21
  }
22
22
  jest.useFakeTimers();
23
23
  })
@@ -28,6 +28,167 @@ describe('Consumer', () => {
28
28
  })
29
29
 
30
30
 
31
+ it('Should throw error if set dead letter queue and no set total retries before send to dlq', async () => {
32
+ try {
33
+ queueDriver.get.mockResolvedValueOnce({ data: [message], error: null })
34
+ queueDriver.delete.mockResolvedValueOnce({ error: null })
35
+ queueDriver.get.mockResolvedValueOnce({ data: [], error: null })
36
+
37
+ const handler = jest.fn(async () => { })
38
+ const consumer = new Consumer(
39
+ {
40
+ queueName: 'q',
41
+ consumeType: 'read',
42
+ visibilityTime: 1,
43
+ poolSize: 1,
44
+ timeMsWaitBeforeNextPolling: 1,
45
+ enabledPolling: true,
46
+ queueNameDlq: 'q_dlq',
47
+ },
48
+ handler,
49
+ queueDriver
50
+ )
51
+
52
+ const onFinish = jest.fn()
53
+ consumer.on('finish', onFinish)
54
+ await consumer.start()
55
+ } catch (error: any) {
56
+ expect(error).toBeInstanceOf(Error)
57
+ expect(error.message).toBe('The option totalRetriesBeforeSendToDlq is required when queueNameDlq is set')
58
+ }
59
+ })
60
+
61
+
62
+ it('Should send message to dlq if read count is greater than total retries before send to dlq', async () => {
63
+ const messageToSendDlq = { ...message }
64
+ messageToSendDlq.read_ct = 3
65
+ messageToSendDlq.msg_id = 2
66
+ queueDriver.get.mockResolvedValueOnce({ data: [messageToSendDlq], error: null })
67
+ queueDriver.delete.mockResolvedValueOnce({ error: null })
68
+ queueDriver.get.mockResolvedValueOnce({ data: [], error: null })
69
+ queueDriver.send.mockResolvedValueOnce({ error: null })
70
+
71
+ const handler = jest.fn(async () => { })
72
+ const consumer = new Consumer(
73
+ {
74
+ queueName: 'q',
75
+ consumeType: 'read',
76
+ visibilityTime: 1,
77
+ poolSize: 1,
78
+ timeMsWaitBeforeNextPolling: 1,
79
+ enabledPolling: true,
80
+ queueNameDlq: 'q_dlq',
81
+ totalRetriesBeforeSendToDlq: 2
82
+ },
83
+ handler,
84
+ queueDriver
85
+ )
86
+
87
+ const onFinish = jest.fn()
88
+ consumer.on('finish', onFinish)
89
+ await consumer.start()
90
+
91
+ expect(handler).toHaveBeenCalledTimes(0)
92
+ expect(onFinish).toHaveBeenCalledTimes(0)
93
+ expect(queueDriver.delete).toHaveBeenCalledTimes(1)
94
+ expect(queueDriver.send).toHaveBeenCalledTimes(1)
95
+ expect(queueDriver.send).toHaveBeenCalledWith(
96
+ 'q_dlq', messageToSendDlq.message, expect.any(AbortSignal)
97
+ )
98
+ })
99
+
100
+ it('Should send 2 messages to dlq if read count is greater than total retries before send to dlq', async () => {
101
+ const messageToSendDlq = { ...message }
102
+ messageToSendDlq.read_ct = 3
103
+ messageToSendDlq.msg_id = 2
104
+ queueDriver.get.mockResolvedValueOnce({
105
+ data: [
106
+ messageToSendDlq, messageToSendDlq
107
+ ], error: null
108
+ })
109
+ queueDriver.delete.mockResolvedValueOnce({ error: null })
110
+ queueDriver.get.mockResolvedValueOnce({ data: [], error: null })
111
+ queueDriver.send.mockResolvedValue({ error: null })
112
+
113
+ const handler = jest.fn(async () => { })
114
+ const consumer = new Consumer(
115
+ {
116
+ queueName: 'q',
117
+ consumeType: 'read',
118
+ visibilityTime: 1,
119
+ poolSize: 2,
120
+ timeMsWaitBeforeNextPolling: 1,
121
+ enabledPolling: true,
122
+ queueNameDlq: 'q_dlq',
123
+ totalRetriesBeforeSendToDlq: 2
124
+ },
125
+ handler,
126
+ queueDriver
127
+ )
128
+
129
+ const onFinish = jest.fn()
130
+ consumer.on('finish', onFinish)
131
+ await consumer.start()
132
+
133
+ expect(handler).toHaveBeenCalledTimes(0)
134
+ expect(onFinish).toHaveBeenCalledTimes(0)
135
+ expect(queueDriver.delete).toHaveBeenCalledTimes(2)
136
+ expect(queueDriver.send).toHaveBeenCalledTimes(2)
137
+ expect(queueDriver.send).toHaveBeenCalledWith(
138
+ 'q_dlq', messageToSendDlq.message, expect.any(AbortSignal)
139
+ )
140
+ })
141
+
142
+
143
+ it('Should send only 1 message to dlq if read count is greater than total retries before send to dlq', async () => {
144
+ const messageToSendDlq = { ...message }
145
+ messageToSendDlq.read_ct = 3
146
+ messageToSendDlq.msg_id = 2
147
+
148
+ const messageToSendDlq2 = { ...message }
149
+ messageToSendDlq2.read_ct = 1
150
+ messageToSendDlq2.msg_id = 3
151
+ queueDriver.get.mockResolvedValueOnce({
152
+ data: [
153
+ messageToSendDlq, messageToSendDlq2
154
+ ], error: null
155
+ })
156
+ queueDriver.delete.mockResolvedValue({ error: null })
157
+ queueDriver.get.mockResolvedValueOnce({ data: [], error: null })
158
+ queueDriver.send.mockResolvedValue({ error: null })
159
+
160
+ const handler = jest.fn(async () => {
161
+ return Promise.resolve()
162
+ })
163
+ const consumer = new Consumer(
164
+ {
165
+ queueName: 'q',
166
+ consumeType: 'read',
167
+ visibilityTime: 1,
168
+ poolSize: 2,
169
+ timeMsWaitBeforeNextPolling: 1,
170
+ enabledPolling: true,
171
+ queueNameDlq: 'q_dlq',
172
+ totalRetriesBeforeSendToDlq: 2
173
+ },
174
+ handler,
175
+ queueDriver
176
+ )
177
+
178
+ const onFinish = jest.fn()
179
+ consumer.on('finish', onFinish)
180
+ await consumer.start()
181
+
182
+ expect(handler).toHaveBeenCalledTimes(1)
183
+ expect(onFinish).toHaveBeenCalledTimes(1)
184
+ expect(queueDriver.delete).toHaveBeenCalledTimes(2)
185
+ expect(queueDriver.send).toHaveBeenCalledTimes(1)
186
+ expect(queueDriver.send).toHaveBeenCalledWith(
187
+ 'q_dlq', messageToSendDlq.message, expect.any(AbortSignal)
188
+ )
189
+ })
190
+
191
+
31
192
 
32
193
  it('Should not process message if read method does not return any message second polling', async () => {
33
194
  queueDriver.get.mockResolvedValueOnce({ data: [message], error: null })
@@ -41,7 +202,8 @@ describe('Consumer', () => {
41
202
  consumeType: 'read',
42
203
  visibilityTime: 1,
43
204
  poolSize: 1,
44
- timeMsWaitBeforeNextPolling: 1
205
+ timeMsWaitBeforeNextPolling: 1,
206
+ enabledPolling: true
45
207
  },
46
208
  handler,
47
209
  queueDriver
@@ -69,7 +231,8 @@ describe('Consumer', () => {
69
231
  consumeType: 'pop',
70
232
  visibilityTime: 1,
71
233
  poolSize: 1,
72
- timeMsWaitBeforeNextPolling: 1
234
+ timeMsWaitBeforeNextPolling: 1,
235
+ enabledPolling: true
73
236
  },
74
237
  handler,
75
238
  queueDriver
@@ -95,7 +258,8 @@ describe('Consumer', () => {
95
258
  consumeType: 'pop',
96
259
  visibilityTime: 1,
97
260
  poolSize: 1,
98
- timeMsWaitBeforeNextPolling: 1
261
+ timeMsWaitBeforeNextPolling: 1,
262
+ enabledPolling: true
99
263
  },
100
264
  handler,
101
265
  queueDriver
@@ -121,7 +285,8 @@ describe('Consumer', () => {
121
285
  consumeType: 'read',
122
286
  visibilityTime: 1,
123
287
  poolSize: 1,
124
- timeMsWaitBeforeNextPolling: 1
288
+ timeMsWaitBeforeNextPolling: 1,
289
+ enabledPolling: true
125
290
  },
126
291
  handler,
127
292
  queueDriver
@@ -150,7 +315,8 @@ describe('Consumer', () => {
150
315
  consumeType: 'read',
151
316
  visibilityTime: 1,
152
317
  poolSize: 1,
153
- timeMsWaitBeforeNextPolling: 1
318
+ timeMsWaitBeforeNextPolling: 1,
319
+ enabledPolling: true
154
320
  },
155
321
  handler,
156
322
  queueDriver
@@ -179,7 +345,8 @@ describe('Consumer', () => {
179
345
  consumeType: 'read',
180
346
  visibilityTime: 1,
181
347
  poolSize: 4,
182
- timeMsWaitBeforeNextPolling: 1
348
+ timeMsWaitBeforeNextPolling: 1,
349
+ enabledPolling: true
183
350
  },
184
351
  handler,
185
352
  queueDriver