@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.
Files changed (45) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +101 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  3. package/.github/dependabot.yml +11 -0
  4. package/CODE_OF_CONDUCT.md +128 -0
  5. package/CONTRIBUTING.md +145 -0
  6. package/README.md +89 -33
  7. package/SECURITY.md +8 -0
  8. package/dist/cjs/backpressure.d.ts +4 -0
  9. package/dist/cjs/backpressure.js +31 -0
  10. package/dist/cjs/consumer.d.ts +19 -0
  11. package/dist/cjs/consumer.js +111 -0
  12. package/dist/cjs/index.d.ts +1 -0
  13. package/dist/cjs/index.js +1 -0
  14. package/dist/cjs/publisher.d.ts +13 -0
  15. package/dist/cjs/publisher.js +141 -0
  16. package/dist/cjs/rabbitmqBroker.d.ts +2 -50
  17. package/dist/cjs/rabbitmqBroker.js +30 -344
  18. package/dist/cjs/reconnect.d.ts +17 -0
  19. package/dist/cjs/reconnect.js +64 -0
  20. package/dist/cjs/topology.d.ts +9 -0
  21. package/dist/cjs/topology.js +58 -0
  22. package/dist/cjs/types.d.ts +49 -0
  23. package/dist/cjs/types.js +2 -0
  24. package/dist/cjs/uuid.d.ts +1 -0
  25. package/dist/cjs/uuid.js +6 -0
  26. package/dist/esm/backpressure.d.ts +4 -0
  27. package/dist/esm/backpressure.js +31 -0
  28. package/dist/esm/consumer.d.ts +19 -0
  29. package/dist/esm/consumer.js +111 -0
  30. package/dist/esm/index.d.ts +1 -0
  31. package/dist/esm/index.js +1 -0
  32. package/dist/esm/publisher.d.ts +13 -0
  33. package/dist/esm/publisher.js +141 -0
  34. package/dist/esm/rabbitmqBroker.d.ts +2 -50
  35. package/dist/esm/rabbitmqBroker.js +30 -344
  36. package/dist/esm/reconnect.d.ts +17 -0
  37. package/dist/esm/reconnect.js +64 -0
  38. package/dist/esm/topology.d.ts +9 -0
  39. package/dist/esm/topology.js +58 -0
  40. package/dist/esm/types.d.ts +49 -0
  41. package/dist/esm/types.js +2 -0
  42. package/dist/esm/uuid.d.ts +1 -0
  43. package/dist/esm/uuid.js +6 -0
  44. package/package.json +1 -1
  45. /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
+ }
@@ -2,3 +2,4 @@ export * from "./rabbitmqBroker";
2
2
  export * from "./eventFactories";
3
3
  export * from "./pluginManager";
4
4
  export * from "./utils/dedupe";
5
+ export * from "./types";
package/dist/cjs/index.js CHANGED
@@ -18,3 +18,4 @@ __exportStar(require("./rabbitmqBroker"), exports);
18
18
  __exportStar(require("./eventFactories"), exports);
19
19
  __exportStar(require("./pluginManager"), exports);
20
20
  __exportStar(require("./utils/dedupe"), exports);
21
+ __exportStar(require("./types"), exports);
@@ -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
- export interface ExchangeConfig {
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
- /** The current live channel promise (replaced after reconnect). */
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): {