@bitspacerlabs/rabbit-relay 0.5.2 → 0.6.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/.github/ISSUE_TEMPLATE/bug_report.md +101 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/dependabot.yml +11 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +145 -0
- package/README.md +89 -33
- package/SECURITY.md +8 -0
- package/dist/cjs/backpressure.d.ts +4 -0
- package/dist/cjs/backpressure.js +31 -0
- package/dist/cjs/consumer.d.ts +19 -0
- package/dist/cjs/consumer.js +111 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/publisher.d.ts +13 -0
- package/dist/cjs/publisher.js +141 -0
- package/dist/cjs/rabbitmqBroker.d.ts +2 -50
- package/dist/cjs/rabbitmqBroker.js +30 -344
- package/dist/cjs/reconnect.d.ts +17 -0
- package/dist/cjs/reconnect.js +64 -0
- package/dist/cjs/topology.d.ts +9 -0
- package/dist/cjs/topology.js +58 -0
- package/dist/cjs/types.d.ts +49 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/uuid.d.ts +1 -0
- package/dist/cjs/uuid.js +6 -0
- package/dist/esm/backpressure.d.ts +4 -0
- package/dist/esm/backpressure.js +31 -0
- package/dist/esm/consumer.d.ts +19 -0
- package/dist/esm/consumer.js +111 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/publisher.d.ts +13 -0
- package/dist/esm/publisher.js +141 -0
- package/dist/esm/rabbitmqBroker.d.ts +2 -50
- package/dist/esm/rabbitmqBroker.js +30 -344
- package/dist/esm/reconnect.d.ts +17 -0
- package/dist/esm/reconnect.js +64 -0
- package/dist/esm/topology.d.ts +9 -0
- package/dist/esm/topology.js +58 -0
- package/dist/esm/types.d.ts +49 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/uuid.d.ts +1 -0
- package/dist/esm/uuid.js +6 -0
- package/package.json +1 -1
- /package/assets/{logo.svg → rabbit-relay.svg} +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createConsumer = createConsumer;
|
|
4
|
+
const pluginManager_1 = require("./pluginManager");
|
|
5
|
+
const backpressure_1 = require("./backpressure");
|
|
6
|
+
function createConsumer(params) {
|
|
7
|
+
const { queueName, handlers } = params;
|
|
8
|
+
let consumerTag;
|
|
9
|
+
let isConsuming = false;
|
|
10
|
+
let consumeCh = null;
|
|
11
|
+
let prefetchCount = 1;
|
|
12
|
+
// Note: concurrency is *effectively* enforced via prefetch as in the original file.
|
|
13
|
+
let concurrency = 1;
|
|
14
|
+
let onError = "ack";
|
|
15
|
+
const onMessage = async (msg) => {
|
|
16
|
+
if (!msg)
|
|
17
|
+
return;
|
|
18
|
+
const ch = consumeCh;
|
|
19
|
+
if (!ch)
|
|
20
|
+
return;
|
|
21
|
+
const id = msg.fields.deliveryTag;
|
|
22
|
+
const payload = JSON.parse(msg.content.toString());
|
|
23
|
+
const handler = handlers.get(payload.name) || handlers.get("*");
|
|
24
|
+
let result = null;
|
|
25
|
+
let errored = false;
|
|
26
|
+
try {
|
|
27
|
+
await pluginManager_1.pluginManager.executeHook("beforeProcess", id, payload);
|
|
28
|
+
if (handler) {
|
|
29
|
+
result = await handler(id, payload);
|
|
30
|
+
}
|
|
31
|
+
await pluginManager_1.pluginManager.executeHook("afterProcess", id, payload, result);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
errored = true;
|
|
35
|
+
console.error("Handler error:", err);
|
|
36
|
+
}
|
|
37
|
+
// RPC reply path (even if handler errored, you might still want a reply)
|
|
38
|
+
if (msg.properties.replyTo) {
|
|
39
|
+
try {
|
|
40
|
+
await (0, backpressure_1.publishWithBackpressure)(ch, "", msg.properties.replyTo, Buffer.from(JSON.stringify({ reply: errored ? null : result })), { correlationId: msg.properties.correlationId });
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
console.error("Reply publish failed:", e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Ack/Nack decision
|
|
47
|
+
try {
|
|
48
|
+
if (errored) {
|
|
49
|
+
if (onError === "requeue") {
|
|
50
|
+
ch.nack(msg, false, true); // requeue back to same queue
|
|
51
|
+
}
|
|
52
|
+
else if (onError === "dead-letter") {
|
|
53
|
+
ch.nack(msg, false, false); // route to DLX (if queue is DLX-configured)
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
ch.ack(msg); // swallow the error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
ch.ack(msg);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
console.error("Ack/Nack failed:", e);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
async function startConsume(getChannel, opts) {
|
|
68
|
+
var _a, _b, _c, _d;
|
|
69
|
+
prefetchCount = (_b = (_a = opts === null || opts === void 0 ? void 0 : opts.prefetch) !== null && _a !== void 0 ? _a : opts === null || opts === void 0 ? void 0 : opts.concurrency) !== null && _b !== void 0 ? _b : 1;
|
|
70
|
+
concurrency = (_c = opts === null || opts === void 0 ? void 0 : opts.concurrency) !== null && _c !== void 0 ? _c : prefetchCount;
|
|
71
|
+
// Back-compat: if requeueOnError is set and onError not explicitly provided, use "requeue"
|
|
72
|
+
onError = (_d = opts === null || opts === void 0 ? void 0 : opts.onError) !== null && _d !== void 0 ? _d : ((opts === null || opts === void 0 ? void 0 : opts.requeueOnError) ? "requeue" : "ack");
|
|
73
|
+
const ch = await getChannel();
|
|
74
|
+
consumeCh = ch;
|
|
75
|
+
if (prefetchCount > 0)
|
|
76
|
+
await ch.prefetch(prefetchCount, false);
|
|
77
|
+
const ok = await ch.consume(queueName, onMessage);
|
|
78
|
+
consumerTag = ok.consumerTag;
|
|
79
|
+
isConsuming = true;
|
|
80
|
+
return {
|
|
81
|
+
stop: async () => {
|
|
82
|
+
isConsuming = false;
|
|
83
|
+
try {
|
|
84
|
+
const c = consumeCh;
|
|
85
|
+
if (consumerTag && c)
|
|
86
|
+
await c.cancel(consumerTag);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// channel may be closed, ignore
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function resumeOnReconnect(ch) {
|
|
95
|
+
if (!isConsuming)
|
|
96
|
+
return;
|
|
97
|
+
if (prefetchCount > 0)
|
|
98
|
+
await ch.prefetch(prefetchCount, false);
|
|
99
|
+
consumeCh = ch;
|
|
100
|
+
const ok = await ch.consume(queueName, onMessage);
|
|
101
|
+
consumerTag = ok.consumerTag;
|
|
102
|
+
}
|
|
103
|
+
function getState() {
|
|
104
|
+
return { isConsuming, prefetchCount, concurrency, onError };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
startConsume,
|
|
108
|
+
resumeOnReconnect,
|
|
109
|
+
getState,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/cjs/index.d.ts
CHANGED
package/dist/cjs/index.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Channel } from "amqplib";
|
|
2
|
+
import { EventEnvelope } from "./eventFactories";
|
|
3
|
+
import { ExchangeConfig, InternalCfg } from "./types";
|
|
4
|
+
export declare function createPublisher(params: {
|
|
5
|
+
exchangeName: string;
|
|
6
|
+
exchangeConfig: ExchangeConfig;
|
|
7
|
+
defaultCfg: InternalCfg;
|
|
8
|
+
getChannel: () => Promise<Channel>;
|
|
9
|
+
getBackoffMs: () => number;
|
|
10
|
+
}): {
|
|
11
|
+
produce: <TEvents extends Record<string, EventEnvelope>, K extends keyof TEvents>(...events: TEvents[K][]) => Promise<void | unknown>;
|
|
12
|
+
produceMany: <TEvents extends Record<string, EventEnvelope>, K extends keyof TEvents>(...events: TEvents[K][]) => Promise<void>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPublisher = createPublisher;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const pluginManager_1 = require("./pluginManager");
|
|
6
|
+
const backpressure_1 = require("./backpressure");
|
|
7
|
+
const uuid_1 = require("./uuid");
|
|
8
|
+
function createPublisher(params) {
|
|
9
|
+
const { exchangeName, exchangeConfig, defaultCfg, getChannel, getBackoffMs } = params;
|
|
10
|
+
const getPubChannel = async () => {
|
|
11
|
+
var _a;
|
|
12
|
+
if ((_a = exchangeConfig.publisherConfirms) !== null && _a !== void 0 ? _a : defaultCfg.publisherConfirms) {
|
|
13
|
+
return (0, config_1.getRabbitMQConfirmChannel)();
|
|
14
|
+
}
|
|
15
|
+
return getChannel();
|
|
16
|
+
};
|
|
17
|
+
const safePublish = async (publish) => {
|
|
18
|
+
try {
|
|
19
|
+
const ch = await getPubChannel();
|
|
20
|
+
await publish(ch);
|
|
21
|
+
await (0, backpressure_1.maybeWaitForConfirms)(ch);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Broker is likely reconnecting. Briefly wait, then retry once.
|
|
25
|
+
const delay = Math.min(getBackoffMs() * 2, 2000);
|
|
26
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
27
|
+
const ch2 = await getPubChannel();
|
|
28
|
+
await publish(ch2);
|
|
29
|
+
await (0, backpressure_1.maybeWaitForConfirms)(ch2);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const produceMany = async (...events) => {
|
|
33
|
+
for (const evt of events) {
|
|
34
|
+
await pluginManager_1.pluginManager.executeHook("beforeProduce", evt);
|
|
35
|
+
await safePublish((ch) => {
|
|
36
|
+
var _a, _b, _c;
|
|
37
|
+
const e = evt;
|
|
38
|
+
const props = {
|
|
39
|
+
messageId: e.id, // idempotency key
|
|
40
|
+
type: e.name, // event name
|
|
41
|
+
timestamp: Math.floor(((_a = e.time) !== null && _a !== void 0 ? _a : Date.now()) / 1000),
|
|
42
|
+
correlationId: (_b = e.meta) === null || _b === void 0 ? void 0 : _b.corrId,
|
|
43
|
+
headers: (_c = e.meta) === null || _c === void 0 ? void 0 : _c.headers,
|
|
44
|
+
};
|
|
45
|
+
return (0, backpressure_1.publishWithBackpressure)(ch, exchangeName, e.name, Buffer.from(JSON.stringify(e)), props);
|
|
46
|
+
});
|
|
47
|
+
await pluginManager_1.pluginManager.executeHook("afterProduce", evt, null);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const produce = async (...events) => {
|
|
51
|
+
var _a, _b, _c, _d, _e;
|
|
52
|
+
// Back-compat: upgrade legacy "wait" (if present) to meta fields
|
|
53
|
+
if (events.length === 1 && ((_a = events[0]) === null || _a === void 0 ? void 0 : _a.wait)) {
|
|
54
|
+
const first = events[0];
|
|
55
|
+
const w = first.wait;
|
|
56
|
+
first.meta = first.meta || {};
|
|
57
|
+
if (first.meta.expectsReply !== true)
|
|
58
|
+
first.meta.expectsReply = true;
|
|
59
|
+
if ((w === null || w === void 0 ? void 0 : w.timeout) != null && first.meta.timeoutMs == null)
|
|
60
|
+
first.meta.timeoutMs = w.timeout;
|
|
61
|
+
if (w === null || w === void 0 ? void 0 : w.source) {
|
|
62
|
+
first.meta.headers = { ...(first.meta.headers || {}), source: w.source };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// RPC request path
|
|
66
|
+
if (events.length === 1 && ((_c = (_b = events[0]) === null || _b === void 0 ? void 0 : _b.meta) === null || _c === void 0 ? void 0 : _c.expectsReply) === true) {
|
|
67
|
+
const evt = events[0];
|
|
68
|
+
const correlationId = (0, uuid_1.generateUuid)();
|
|
69
|
+
const rpcCh = await getChannel(); // pin for reply consumer/ack
|
|
70
|
+
const temp = await rpcCh.assertQueue("", { exclusive: true, autoDelete: true });
|
|
71
|
+
await pluginManager_1.pluginManager.executeHook("beforeProduce", evt);
|
|
72
|
+
await safePublish(async () => {
|
|
73
|
+
var _a, _b;
|
|
74
|
+
// use (confirm) pub channel for the request publish
|
|
75
|
+
const pubCh = await getPubChannel();
|
|
76
|
+
const props = {
|
|
77
|
+
messageId: evt.id,
|
|
78
|
+
type: evt.name,
|
|
79
|
+
timestamp: Math.floor(((_a = evt.time) !== null && _a !== void 0 ? _a : Date.now()) / 1000),
|
|
80
|
+
correlationId,
|
|
81
|
+
headers: (_b = evt.meta) === null || _b === void 0 ? void 0 : _b.headers,
|
|
82
|
+
replyTo: temp.queue,
|
|
83
|
+
};
|
|
84
|
+
await (0, backpressure_1.publishWithBackpressure)(pubCh, exchangeName, evt.name, Buffer.from(JSON.stringify(evt)), props);
|
|
85
|
+
});
|
|
86
|
+
const timeoutMs = (_e = (_d = evt.meta) === null || _d === void 0 ? void 0 : _d.timeoutMs) !== null && _e !== void 0 ? _e : 5000;
|
|
87
|
+
return await new Promise((resolve, reject) => {
|
|
88
|
+
let ctag;
|
|
89
|
+
const timer = setTimeout(async () => {
|
|
90
|
+
try {
|
|
91
|
+
if (ctag)
|
|
92
|
+
await rpcCh.cancel(ctag);
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
try {
|
|
96
|
+
await rpcCh.deleteQueue(temp.queue);
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
reject(new Error("Timeout waiting for reply"));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
rpcCh
|
|
102
|
+
.consume(temp.queue, (msg) => {
|
|
103
|
+
if (!msg)
|
|
104
|
+
return;
|
|
105
|
+
if (msg.properties.correlationId !== correlationId)
|
|
106
|
+
return;
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
try {
|
|
109
|
+
const reply = JSON.parse(msg.content.toString()).reply;
|
|
110
|
+
pluginManager_1.pluginManager.executeHook("afterProduce", evt, reply);
|
|
111
|
+
resolve(reply);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
Promise.resolve()
|
|
115
|
+
.then(async () => {
|
|
116
|
+
try {
|
|
117
|
+
if (ctag)
|
|
118
|
+
await rpcCh.cancel(ctag);
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
try {
|
|
122
|
+
await rpcCh.deleteQueue(temp.queue);
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
})
|
|
126
|
+
.catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
}, { noAck: true })
|
|
129
|
+
.then((ok) => {
|
|
130
|
+
ctag = ok.consumerTag;
|
|
131
|
+
})
|
|
132
|
+
.catch((err) => {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
reject(err);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return produceMany(...events);
|
|
139
|
+
};
|
|
140
|
+
return { produce, produceMany };
|
|
141
|
+
}
|
|
@@ -1,58 +1,10 @@
|
|
|
1
|
-
import { Options } from "amqplib";
|
|
2
1
|
import { EventEnvelope } from "./eventFactories";
|
|
3
|
-
|
|
4
|
-
exchangeType?: "topic" | "direct" | "fanout";
|
|
5
|
-
routingKey?: string;
|
|
6
|
-
durable?: boolean;
|
|
7
|
-
publisherConfirms?: boolean;
|
|
8
|
-
queueArgs?: Options.AssertQueue["arguments"];
|
|
9
|
-
/**
|
|
10
|
-
* If true, do NOT declare the queue; only check it exists.
|
|
11
|
-
* Use this when a separate setup step has already created the queue with specific args.
|
|
12
|
-
*/
|
|
13
|
-
passiveQueue?: boolean;
|
|
14
|
-
}
|
|
15
|
-
export interface ConsumeOptions {
|
|
16
|
-
/** Max unacked messages this consumer can hold. Also default concurrency. */
|
|
17
|
-
prefetch?: number;
|
|
18
|
-
/** Parallel handler executions. Defaults to prefetch (or 1). */
|
|
19
|
-
concurrency?: number;
|
|
20
|
-
/** If true, nack+requeue on handler error; else ack even on error. (back-compat) */
|
|
21
|
-
requeueOnError?: boolean;
|
|
22
|
-
/** What to do when the handler throws. Default "ack". */
|
|
23
|
-
onError?: "ack" | "requeue" | "dead-letter";
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Generic Broker Interface:
|
|
27
|
-
* TEvents maps event name keys -> EventEnvelope types.
|
|
28
|
-
*/
|
|
29
|
-
export interface BrokerInterface<TEvents extends Record<string, EventEnvelope>> {
|
|
30
|
-
handle<K extends keyof TEvents>(eventName: K | "*", handler: (id: string | number, event: TEvents[K]) => Promise<unknown>): BrokerInterface<TEvents>;
|
|
31
|
-
consume(opts?: ConsumeOptions): Promise<{
|
|
32
|
-
stop(): Promise<void>;
|
|
33
|
-
}>;
|
|
34
|
-
produce<K extends keyof TEvents>(...events: TEvents[K][]): Promise<void | unknown>;
|
|
35
|
-
produceMany<K extends keyof TEvents>(...events: TEvents[K][]): Promise<void>;
|
|
36
|
-
with<U extends Record<string, (...args: any[]) => EventEnvelope>>(events: U): BrokerInterface<{
|
|
37
|
-
[K in keyof U]: ReturnType<U[K]>;
|
|
38
|
-
}> & {
|
|
39
|
-
[K in keyof U]: (...args: Parameters<U[K]>) => ReturnType<U[K]>;
|
|
40
|
-
};
|
|
41
|
-
}
|
|
2
|
+
import { ExchangeConfig, BrokerInterface } from "./types";
|
|
42
3
|
export declare class RabbitMQBroker {
|
|
43
4
|
private peerName;
|
|
44
5
|
private defaultCfg;
|
|
45
|
-
|
|
46
|
-
private channelPromise;
|
|
47
|
-
/** Reconnect state */
|
|
48
|
-
private reconnecting;
|
|
49
|
-
private backoffMs;
|
|
50
|
-
private readonly maxBackoffMs;
|
|
51
|
-
/** Callbacks to run after a successful reconnect (like re-assert topology, resume consume). */
|
|
52
|
-
private onReconnectCbs;
|
|
6
|
+
private reconnect;
|
|
53
7
|
constructor(peerName: string, config?: ExchangeConfig);
|
|
54
|
-
private initChannel;
|
|
55
|
-
private scheduleReconnect;
|
|
56
8
|
private getChannel;
|
|
57
9
|
private onReconnect;
|
|
58
10
|
queue(queueName: string): {
|