@fedify/amqp 0.3.0-dev.16 → 0.3.0-dev.17

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
@@ -30,6 +30,17 @@ const federation = createFederation({
30
30
  });
31
31
  ~~~~
32
32
 
33
+ The `AmqpMessageQueue` constructor accepts options as the second
34
+ parameter, which can be used to configure the message queue:
35
+
36
+ ~~~~ typescript
37
+ new AmqpMessageQueue(await connect("amqp://localhost"), {
38
+ queue: "my_queue",
39
+ })
40
+ ~~~~
41
+
42
+ For more details, please refer to the docs of [`AmqpMessageQueueOptions`].
43
+
33
44
  [JSR]: https://jsr.io/@fedify/amqp
34
45
  [JSR badge]: https://jsr.io/badges/@fedify/amqp
35
46
  [npm]: https://www.npmjs.com/package/@fedify/amqp
@@ -41,6 +52,7 @@ const federation = createFederation({
41
52
  [`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore
42
53
  [`MessageQueue`]: https://jsr.io/@fedify/fedify/doc/federation/~/MessageQueue
43
54
  [`AmqpMessageQueue`]: https://jsr.io/@fedify/amqp/doc/mq/~/AmqpMessageQueue
55
+ [`AmqpMessageQueueOptions`] https://jsr.io/@fedify/amqp/doc/mq/~/AmqpMessageQueueOptions
44
56
 
45
57
 
46
58
  Installation
package/deno.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@fedify/amqp",
3
- "version": "0.3.0-dev.16+bc8a5574",
3
+ "version": "0.3.0-dev.17+484159bd",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./mod.ts",
7
7
  "./mq": "./src/mq.ts"
8
8
  },
9
9
  "imports": {
10
- "@alinea/suite": "jsr:@alinea/suite@^0.6.2",
11
- "@fedify/fedify": "jsr:@fedify/fedify@^1.5.0",
10
+ "@fedify/fedify": "jsr:@fedify/fedify@^1.7.0-dev.887+013dabb5",
11
+ "@hongminhee/suite": "jsr:@hongminhee/suite@^0.6.3",
12
12
  "@std/assert": "jsr:@std/assert@^1.0.13",
13
13
  "@std/async": "jsr:@std/async@^1.0.13",
14
14
  "amqplib": "npm:amqplib@^0.10.8",
package/deno.lock CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "version": "5",
3
3
  "specifiers": {
4
- "jsr:@alinea/suite@~0.6.2": "0.6.2",
5
4
  "jsr:@es-toolkit/es-toolkit@^1.38.0": "1.39.3",
6
- "jsr:@fedify/fedify@^1.5.0": "1.6.1",
5
+ "jsr:@fedify/fedify@^1.7.0-dev.887+013dabb5": "1.7.0-pr.251.885+560fd10f",
6
+ "jsr:@hongminhee/suite@~0.6.3": "0.6.3",
7
7
  "jsr:@hugoalh/http-header-link@^1.0.2": "1.0.3",
8
8
  "jsr:@hugoalh/is-string-singleline@^1.0.4": "1.0.5",
9
9
  "jsr:@logtape/logtape@0.11": "0.11.0",
10
10
  "jsr:@std/assert@^1.0.13": "1.0.13",
11
11
  "jsr:@std/async@^1.0.13": "1.0.13",
12
12
  "jsr:@std/internal@^1.0.6": "1.0.8",
13
- "npm:@alinea/suite@~0.6.2": "0.6.2",
13
+ "npm:@hongminhee/suite@~0.6.3": "0.6.3",
14
14
  "npm:@js-temporal/polyfill@~0.5.1": "0.5.1",
15
15
  "npm:@multiformats/base-x@^4.0.1": "4.0.1",
16
16
  "npm:@opentelemetry/api@^1.9.0": "1.9.0",
@@ -33,14 +33,11 @@
33
33
  "npm:url-template@^3.1.1": "3.1.1"
34
34
  },
35
35
  "jsr": {
36
- "@alinea/suite@0.6.2": {
37
- "integrity": "067948595ee65876c3d76ae40fdf6f321a4afab3566da5ca259559490c95da23"
38
- },
39
36
  "@es-toolkit/es-toolkit@1.39.3": {
40
37
  "integrity": "edd98c3eac732730b61bac8952a31633a778a236e1be144aa32b220c152d1fbe"
41
38
  },
42
- "@fedify/fedify@1.6.1": {
43
- "integrity": "e5712a15c50d8836e6ad617aefa69bb601b12ac335526c54a7ffe339b0128e0c",
39
+ "@fedify/fedify@1.7.0-pr.251.885+560fd10f": {
40
+ "integrity": "fb60eaac1b902f32bddbc1ea0171b2bf4903345a497d3aff693ea290f1abc585",
44
41
  "dependencies": [
45
42
  "jsr:@es-toolkit/es-toolkit",
46
43
  "jsr:@hugoalh/http-header-link",
@@ -60,6 +57,9 @@
60
57
  "npm:url-template"
61
58
  ]
62
59
  },
60
+ "@hongminhee/suite@0.6.3": {
61
+ "integrity": "429be25d44f30525fcc7ee60e4879e8abc2e09ab625d006c5bd0605e9e8e393c"
62
+ },
63
63
  "@hugoalh/http-header-link@1.0.3": {
64
64
  "integrity": "3372096a73d755e3351f7fbd7155db7725874c2682a594a655580e3866563024",
65
65
  "dependencies": [
@@ -86,9 +86,6 @@
86
86
  }
87
87
  },
88
88
  "npm": {
89
- "@alinea/suite@0.6.2": {
90
- "integrity": "sha512-UEYs2OLLb5DCMVE/0C8AAhP+xjKdOujbfL+NIXtcCW0dyeQqxjRVVFu+JH5VsjNxkx9ZNgId0fU4HkoKRU49CQ=="
91
- },
92
89
  "@babel/generator@7.27.5": {
93
90
  "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
94
91
  "dependencies": [
@@ -149,6 +146,9 @@
149
146
  "@fastify/busboy@2.1.1": {
150
147
  "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="
151
148
  },
149
+ "@hongminhee/suite@0.6.3": {
150
+ "integrity": "sha512-jW0scPczSP5Mksq49c9stYIApHGijodCFvpKRcFAWImn5WRs4kVlzNwz9QpblaAvoErN8UsfP7OZl3mIk0VoLA=="
151
+ },
152
152
  "@jridgewell/gen-mapping@0.3.8": {
153
153
  "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
154
154
  "dependencies": [
@@ -664,8 +664,8 @@
664
664
  },
665
665
  "workspace": {
666
666
  "dependencies": [
667
- "jsr:@alinea/suite@~0.6.2",
668
- "jsr:@fedify/fedify@^1.5.0",
667
+ "jsr:@fedify/fedify@^1.7.0-dev.887+013dabb5",
668
+ "jsr:@hongminhee/suite@~0.6.3",
669
669
  "jsr:@std/assert@^1.0.13",
670
670
  "jsr:@std/async@^1.0.13",
671
671
  "npm:amqplib@~0.10.8",
@@ -673,7 +673,7 @@
673
673
  ],
674
674
  "packageJson": {
675
675
  "dependencies": [
676
- "npm:@alinea/suite@~0.6.2",
676
+ "npm:@hongminhee/suite@~0.6.3",
677
677
  "npm:@js-temporal/polyfill@~0.5.1",
678
678
  "npm:@types/amqplib@~0.10.7",
679
679
  "npm:tsdown@~0.12.7",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/amqp",
3
- "version": "0.3.0-dev.16+bc8a5574",
3
+ "version": "0.3.0-dev.17+484159bd",
4
4
  "description": "AMQP/RabbitMQ driver for Fedify",
5
5
  "keywords": [
6
6
  "fedify",
@@ -35,8 +35,12 @@
35
35
  },
36
36
  "./package.json": "./package.json"
37
37
  },
38
+ "peerDependencies": {
39
+ "@fedify/fedify": "^1.7.0-dev.887",
40
+ "amqplib": "^0.10.8"
41
+ },
38
42
  "devDependencies": {
39
- "@alinea/suite": "^0.6.2",
43
+ "@hongminhee/suite": "^0.6.3",
40
44
  "@js-temporal/polyfill": "^0.5.1",
41
45
  "@std/assert": "npm:@jsr/std__assert@^1.0.13",
42
46
  "@std/async": "npm:@jsr/std__async@^1.0.13",
@@ -44,10 +48,6 @@
44
48
  "tsdown": "^0.12.7",
45
49
  "typescript": "^5.8.3"
46
50
  },
47
- "peerDependencies": {
48
- "@fedify/fedify": "^1.5.0",
49
- "amqplib": "^0.10.8"
50
- },
51
51
  "scripts": {
52
52
  "build": "tsdown",
53
53
  "prepublish": "tsdown",
package/src/mq.test.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { suite } from "@alinea/suite";
1
+ import { suite } from "@hongminhee/suite";
2
2
  import * as temporal from "@js-temporal/polyfill";
3
- import { assertEquals, assertGreater } from "@std/assert";
3
+ import { assert, assertEquals, assertFalse, assertGreater } from "@std/assert";
4
4
  import { delay } from "@std/async/delay";
5
5
  // @deno-types="npm:@types/amqplib"
6
6
  import { type ChannelModel, connect } from "amqplib";
@@ -21,13 +21,16 @@ function getConnection(): Promise<ChannelModel> {
21
21
  return connect(url ?? "amqp://localhost");
22
22
  }
23
23
 
24
- test("AmqpMessageQueue", async () => {
24
+ test("AmqpMessageQueue", {
25
+ sanitizeOps: false,
26
+ sanitizeExit: false,
27
+ sanitizeResources: false,
28
+ }, async () => {
25
29
  const conn = await getConnection();
26
30
  const conn2 = await getConnection();
27
- const queue = `fedify_queue_${Math.random().toString(36).slice(5)}`;
28
- const delayedQueuePrefix = `fedify_delayed_${
29
- Math.random().toString(36).slice(5)
30
- }_`;
31
+ const randomSuffix = Math.random().toString(36).substring(2);
32
+ const queue = `fedify_queue_${randomSuffix}`;
33
+ const delayedQueuePrefix = `fedify_delayed_${randomSuffix}_`;
31
34
  const mq = new AmqpMessageQueue(conn, { queue, delayedQueuePrefix });
32
35
  const mq2 = new AmqpMessageQueue(conn2, { queue, delayedQueuePrefix });
33
36
 
@@ -99,6 +102,76 @@ test("AmqpMessageQueue", async () => {
99
102
  await conn2.close();
100
103
  });
101
104
 
105
+ test(
106
+ "AmqpMessageQueue [nativeRetrial: false]",
107
+ { sanitizeOps: false, sanitizeExit: false, sanitizeResources: false },
108
+ async () => {
109
+ const conn = await getConnection();
110
+ const randomSuffix = Math.random().toString(36).substring(2);
111
+ const queue = `fedify_queue_${randomSuffix}`;
112
+ const delayedQueuePrefix = `fedify_delayed_${randomSuffix}_`;
113
+ const mq = new AmqpMessageQueue(conn, { queue, delayedQueuePrefix });
114
+ assertFalse(mq.nativeRetrial);
115
+
116
+ const controller = new AbortController();
117
+ let i = 0;
118
+ const listening = mq.listen((message: string) => {
119
+ if (message !== "Hello, world!") return;
120
+ if (i++ < 1) {
121
+ throw new Error("Test error to check native retrial");
122
+ }
123
+ }, { signal: controller.signal });
124
+
125
+ await mq.enqueue("Hello, world!");
126
+
127
+ await waitFor(() => i >= 1, 15_000);
128
+ assertEquals(i, 1);
129
+ await delay(5_000);
130
+
131
+ controller.abort();
132
+ await listening;
133
+ await conn.close();
134
+
135
+ assertEquals(i, 1);
136
+ },
137
+ );
138
+
139
+ test(
140
+ "AmqpMessageQueue [nativeRetrial: true]",
141
+ { sanitizeOps: false, sanitizeExit: false, sanitizeResources: false },
142
+ async () => {
143
+ const conn = await getConnection();
144
+ const randomSuffix = Math.random().toString(36).substring(2);
145
+ const queue = `fedify_queue_${randomSuffix}`;
146
+ const delayedQueuePrefix = `fedify_delayed_${randomSuffix}_`;
147
+ const mq = new AmqpMessageQueue(conn, {
148
+ queue,
149
+ delayedQueuePrefix,
150
+ nativeRetrial: true,
151
+ });
152
+ assert(mq.nativeRetrial);
153
+
154
+ const controller = new AbortController();
155
+ let i = 0;
156
+ const listening = mq.listen((message: string) => {
157
+ if (message !== "Hello, world!") return;
158
+ if (i++ < 1) {
159
+ throw new Error("Test error to check native retrial");
160
+ }
161
+ }, { signal: controller.signal });
162
+
163
+ await mq.enqueue("Hello, world!");
164
+
165
+ await waitFor(() => i > 1, 15_000);
166
+
167
+ controller.abort();
168
+ await listening;
169
+ await conn.close();
170
+
171
+ assertGreater(i, 1);
172
+ },
173
+ );
174
+
102
175
  async function waitFor(
103
176
  predicate: () => boolean,
104
177
  timeoutMs: number,
package/src/mq.ts CHANGED
@@ -29,6 +29,22 @@ export interface AmqpMessageQueueOptions {
29
29
  * @default `true`
30
30
  */
31
31
  durable?: boolean;
32
+
33
+ /**
34
+ * Whether to use native retrial mechanism. If set to `true`, the queue will
35
+ * not acknowledge messages that are not processed successfully, allowing
36
+ * them to be retried later. If set to `false`, messages will be acknowledged
37
+ * whether they are processed successfully or not.
38
+ *
39
+ * Both approaches have their own advantages and disadvantages. With native
40
+ * retrials, much less chance of losing messages, but timing of retrials is
41
+ * less predictable. With non-native retrials, retrials are handled by Fedify
42
+ * itself, which allows for more control over the timing and behavior of
43
+ * retrials, but may result in lost messages if the process crashes before
44
+ * acknowledging the message.
45
+ * @default `false`
46
+ */
47
+ nativeRetrial?: boolean;
32
48
  }
33
49
 
34
50
  /**
@@ -53,6 +69,8 @@ export class AmqpMessageQueue implements MessageQueue {
53
69
  #durable: boolean;
54
70
  #senderChannel?: Channel;
55
71
 
72
+ readonly nativeRetrial: boolean;
73
+
56
74
  /**
57
75
  * Creates a new `AmqpMessageQueue`.
58
76
  * @param connection A connection to the AMQP server.
@@ -66,6 +84,7 @@ export class AmqpMessageQueue implements MessageQueue {
66
84
  this.#queue = options.queue ?? "fedify_queue";
67
85
  this.#delayedQueuePrefix = options.delayedQueuePrefix ?? "fedify_delayed_";
68
86
  this.#durable = options.durable ?? true;
87
+ this.nativeRetrial = options.nativeRetrial ?? false;
69
88
  }
70
89
 
71
90
  async #prepareQueue(channel: Channel): Promise<void> {
@@ -158,16 +177,33 @@ export class AmqpMessageQueue implements MessageQueue {
158
177
  const reply = await channel.consume(this.#queue, (msg) => {
159
178
  if (msg == null) return;
160
179
  const message = JSON.parse(msg.content.toString("utf-8"));
161
- const result = handler(message);
162
- if (result instanceof Promise) {
163
- result.finally(() => channel.ack(msg));
164
- } else {
165
- channel.ack(msg);
180
+ try {
181
+ const result = handler(message);
182
+ if (result instanceof Promise) {
183
+ if (this.nativeRetrial) {
184
+ result
185
+ .then(() => channel.ack(msg))
186
+ .catch(() => channel.nack(msg, undefined, true));
187
+ } else {
188
+ result.finally(() => channel.ack(msg));
189
+ }
190
+ } else if (this.nativeRetrial) {
191
+ channel.ack(msg);
192
+ }
193
+ } catch {
194
+ if (this.nativeRetrial) {
195
+ channel.nack(msg, undefined, true);
196
+ }
197
+ } finally {
198
+ if (!this.nativeRetrial) {
199
+ channel.ack(msg);
200
+ }
166
201
  }
167
202
  }, {
168
203
  noAck: false,
169
204
  });
170
205
  return await new Promise((resolve) => {
206
+ if (options.signal?.aborted) resolve();
171
207
  options.signal?.addEventListener("abort", () => {
172
208
  channel.cancel(reply.consumerTag).then(() => {
173
209
  channel.close().then(() => resolve());