@palmetto/pubsub 1.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 +154 -0
- package/dist/bullmq/config.d.ts +21 -0
- package/dist/bullmq/config.js +2 -0
- package/dist/bullmq/connection.d.ts +2 -0
- package/dist/bullmq/connection.js +5 -0
- package/dist/bullmq/main.d.ts +6 -0
- package/dist/bullmq/main.js +21 -0
- package/dist/bullmq/publisher.d.ts +13 -0
- package/dist/bullmq/publisher.js +62 -0
- package/dist/bullmq/pubsub.d.ts +12 -0
- package/dist/bullmq/pubsub.js +35 -0
- package/dist/bullmq/subscriber.d.ts +27 -0
- package/dist/bullmq/subscriber.js +118 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +37 -0
- package/dist/interfaces.d.ts +120 -0
- package/dist/interfaces.js +28 -0
- package/dist/lazy-load.d.ts +11 -0
- package/dist/lazy-load.js +79 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +22 -0
- package/dist/publisher.d.ts +11 -0
- package/dist/publisher.js +87 -0
- package/dist/rabbitmq/config.d.ts +84 -0
- package/dist/rabbitmq/config.js +82 -0
- package/dist/rabbitmq/connection.d.ts +41 -0
- package/dist/rabbitmq/connection.js +140 -0
- package/dist/rabbitmq/main.d.ts +5 -0
- package/dist/rabbitmq/main.js +21 -0
- package/dist/rabbitmq/publisher.d.ts +29 -0
- package/dist/rabbitmq/publisher.js +98 -0
- package/dist/rabbitmq/pubsub.d.ts +15 -0
- package/dist/rabbitmq/pubsub.js +37 -0
- package/dist/rabbitmq/subscriber.d.ts +33 -0
- package/dist/rabbitmq/subscriber.js +197 -0
- package/dist/rabbitmq/utility.d.ts +6 -0
- package/dist/rabbitmq/utility.js +11 -0
- package/dist/subscriber.d.ts +28 -0
- package/dist/subscriber.js +140 -0
- package/package.json +54 -0
- package/src/bullmq/README.md +63 -0
- package/src/rabbitmq/README.md +184 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.RabbitMqPublisher = void 0;
|
|
13
|
+
const errors_1 = require("../errors");
|
|
14
|
+
const connection_1 = require("./connection");
|
|
15
|
+
const config_1 = require("./config");
|
|
16
|
+
const utility_1 = require("./utility");
|
|
17
|
+
class RabbitMqPublisher {
|
|
18
|
+
constructor(connection, logger) {
|
|
19
|
+
this.connection = connection;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
this.inited = new Set();
|
|
22
|
+
this.connected = false;
|
|
23
|
+
this.transport = connection_1.RABBITMQ_TRANSPORT;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Initializes the rabbit connection and asserts the exchange for the configuration.
|
|
27
|
+
*
|
|
28
|
+
* @param config
|
|
29
|
+
* @returns a Promise containing the ChannelWrapper to publish messages with
|
|
30
|
+
*/
|
|
31
|
+
init(config) {
|
|
32
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
var _a, _b, _c, _d;
|
|
34
|
+
const delayMs = this.connection.config.startupRetryDelayMs || 500;
|
|
35
|
+
const timeoutMs = this.connection.config.startupRetryTimeoutMs || 20000;
|
|
36
|
+
const timeoutAfter = new Date(new Date().getTime() + timeoutMs);
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
38
|
+
while (true) {
|
|
39
|
+
try {
|
|
40
|
+
if (!this.channel) {
|
|
41
|
+
this.channel = this.connection.connection.createChannel({
|
|
42
|
+
confirm: true,
|
|
43
|
+
json: false,
|
|
44
|
+
});
|
|
45
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, "RabbitMq created confirm channel for publisher");
|
|
46
|
+
}
|
|
47
|
+
if (!this.connected) {
|
|
48
|
+
yield this.channel.waitForConnect();
|
|
49
|
+
this.connected = true;
|
|
50
|
+
}
|
|
51
|
+
if (!this.inited.has(config)) {
|
|
52
|
+
yield this.channel.addSetup((channel) => __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
yield this.connection.assertExchange(channel, config);
|
|
54
|
+
}));
|
|
55
|
+
(_d = (_c = this.logger).debug) === null || _d === void 0 ? void 0 : _d.call(_c, `RabbitMq created setup for publisher for ${(0, config_1.getExchangeName)(config)}`);
|
|
56
|
+
this.inited.add(config);
|
|
57
|
+
}
|
|
58
|
+
return this.channel;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (new Date() >= timeoutAfter) {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
yield (0, utility_1.delay)(delayMs);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Publishes a message to Rabbit based on the configuration
|
|
71
|
+
*
|
|
72
|
+
* @param config The rabbit queue configuration
|
|
73
|
+
* @param message The JSON message to send
|
|
74
|
+
* @returns A promise that is completed when the message is published
|
|
75
|
+
*/
|
|
76
|
+
publish(config, message) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
var _a, _b, _c, _d;
|
|
79
|
+
const channel = yield this.init(config);
|
|
80
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Publishing message to ${(0, config_1.getExchangeName)(config)} - ${message} [starting]`);
|
|
81
|
+
const exchangeName = (0, config_1.getExchangeName)(config);
|
|
82
|
+
const ok = yield channel.publish(exchangeName, (0, config_1.getRoutingKey)(config), Buffer.from(message, "utf8"), {
|
|
83
|
+
contentType: "application/json",
|
|
84
|
+
timestamp: new Date().valueOf(),
|
|
85
|
+
persistent: true,
|
|
86
|
+
});
|
|
87
|
+
(_d = (_c = this.logger).debug) === null || _d === void 0 ? void 0 : _d.call(_c, `Published message to ${(0, config_1.getExchangeName)(config)} - ${message} [${ok}]`);
|
|
88
|
+
if (!ok) {
|
|
89
|
+
throw new errors_1.PublishError(`RabbitMq publish to ${exchangeName} failed`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
close() {
|
|
94
|
+
var _a;
|
|
95
|
+
return (_a = this.channel) === null || _a === void 0 ? void 0 : _a.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.RabbitMqPublisher = RabbitMqPublisher;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Logger, MessageContext, MessageResult, PubSubProvider, StopSubscribe } from "../interfaces";
|
|
2
|
+
import { RabbitQueueExchangeConfiguration } from "./config";
|
|
3
|
+
import { RabbitMqConnection } from "./connection";
|
|
4
|
+
/**
|
|
5
|
+
* Combines a RabbitMqPublisher and RabbitMqSubscriber into a single instance
|
|
6
|
+
*/
|
|
7
|
+
export declare class RabbitMqPubSubProvider implements PubSubProvider {
|
|
8
|
+
private readonly publisher;
|
|
9
|
+
private readonly subscriber;
|
|
10
|
+
constructor(connection: RabbitMqConnection, logger: Logger);
|
|
11
|
+
readonly transport: string;
|
|
12
|
+
publish(config: RabbitQueueExchangeConfiguration, message: string): Promise<void>;
|
|
13
|
+
startSubscribe(config: RabbitQueueExchangeConfiguration, onMessage: (s: string, context: MessageContext) => Promise<MessageResult> | MessageResult): StopSubscribe;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.RabbitMqPubSubProvider = void 0;
|
|
13
|
+
const connection_1 = require("./connection");
|
|
14
|
+
const publisher_1 = require("./publisher");
|
|
15
|
+
const subscriber_1 = require("./subscriber");
|
|
16
|
+
/**
|
|
17
|
+
* Combines a RabbitMqPublisher and RabbitMqSubscriber into a single instance
|
|
18
|
+
*/
|
|
19
|
+
class RabbitMqPubSubProvider {
|
|
20
|
+
constructor(connection, logger) {
|
|
21
|
+
this.transport = connection_1.RABBITMQ_TRANSPORT;
|
|
22
|
+
this.publisher = new publisher_1.RabbitMqPublisher(connection, logger);
|
|
23
|
+
this.subscriber = new subscriber_1.RabbitMqSubscriber(connection, logger);
|
|
24
|
+
}
|
|
25
|
+
publish(config, message) {
|
|
26
|
+
return this.publisher.publish(config, message);
|
|
27
|
+
}
|
|
28
|
+
startSubscribe(config, onMessage) {
|
|
29
|
+
return this.subscriber.startSubscribe(config, onMessage);
|
|
30
|
+
}
|
|
31
|
+
close() {
|
|
32
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
yield Promise.all([this.subscriber.close(), this.publisher.close()]);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.RabbitMqPubSubProvider = RabbitMqPubSubProvider;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ConsumeMessage } from "amqplib";
|
|
2
|
+
import type { ChannelWrapper } from "amqp-connection-manager";
|
|
3
|
+
import { Logger, MessageResult, StopSubscribe, SubscriberProvider } from "../interfaces";
|
|
4
|
+
import { RabbitMqMessageContext, RabbitQueueExchangeConfiguration } from "./config";
|
|
5
|
+
import { RabbitMqConnection } from "./connection";
|
|
6
|
+
declare class SubscribedMessage {
|
|
7
|
+
private readonly owner;
|
|
8
|
+
private readonly logger;
|
|
9
|
+
readonly config: RabbitQueueExchangeConfiguration;
|
|
10
|
+
readonly onMessage: (s: string, context: RabbitMqMessageContext) => Promise<MessageResult> | MessageResult;
|
|
11
|
+
stop?: StopSubscribe | undefined;
|
|
12
|
+
channel?: ChannelWrapper;
|
|
13
|
+
busy: number;
|
|
14
|
+
constructor(owner: RabbitMqSubscriber, logger: Logger, config: RabbitQueueExchangeConfiguration, onMessage: (s: string, context: RabbitMqMessageContext) => Promise<MessageResult> | MessageResult, stop?: StopSubscribe | undefined);
|
|
15
|
+
stopSubscribe(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare class RabbitMqSubscriber implements SubscriberProvider {
|
|
18
|
+
private readonly connection;
|
|
19
|
+
private readonly logger;
|
|
20
|
+
private readonly subscribers;
|
|
21
|
+
constructor(connection: RabbitMqConnection, logger: Logger);
|
|
22
|
+
readonly transport: string;
|
|
23
|
+
removeSubscriber(subscribedMessage: SubscribedMessage): void;
|
|
24
|
+
startSubscribe(config: RabbitQueueExchangeConfiguration, onMessage: (s: string, context: RabbitMqMessageContext) => Promise<MessageResult> | MessageResult): StopSubscribe;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
static getRetries(msg: ConsumeMessage): number;
|
|
27
|
+
static getSentDates(msg: ConsumeMessage): {
|
|
28
|
+
firstSent: Date | undefined;
|
|
29
|
+
lastSent: Date | undefined;
|
|
30
|
+
};
|
|
31
|
+
private actuallySubscribe;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.RabbitMqSubscriber = void 0;
|
|
13
|
+
const interfaces_1 = require("../interfaces");
|
|
14
|
+
const errors_1 = require("../errors");
|
|
15
|
+
const config_1 = require("./config");
|
|
16
|
+
const connection_1 = require("./connection");
|
|
17
|
+
class SubscribedMessage {
|
|
18
|
+
constructor(owner, logger, config, onMessage, stop) {
|
|
19
|
+
this.owner = owner;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.onMessage = onMessage;
|
|
23
|
+
this.stop = stop;
|
|
24
|
+
this.busy = 0;
|
|
25
|
+
}
|
|
26
|
+
stopSubscribe() {
|
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
var _a, _b, _c, _d;
|
|
29
|
+
if (this.stop) {
|
|
30
|
+
// avoid re-entry by swapping this.stop
|
|
31
|
+
const s = this.stop;
|
|
32
|
+
this.stop = undefined;
|
|
33
|
+
this.owner.removeSubscriber(this);
|
|
34
|
+
if (this.busy) {
|
|
35
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, "RabbitMq subscriber waiting for handler to finish");
|
|
36
|
+
const start = new Date().valueOf();
|
|
37
|
+
const waitDelay = 5;
|
|
38
|
+
let busyCheck = 1000 / waitDelay; // wait up to a second or so
|
|
39
|
+
while (this.busy && busyCheck > 0) {
|
|
40
|
+
yield new Promise((resolve) => setTimeout(resolve, waitDelay));
|
|
41
|
+
busyCheck -= waitDelay;
|
|
42
|
+
}
|
|
43
|
+
const delay = new Date().valueOf() - start;
|
|
44
|
+
(_d = (_c = this.logger).debug) === null || _d === void 0 ? void 0 : _d.call(_c, `RabbitMq subscriber waited ${delay}ms for handler to finish`);
|
|
45
|
+
}
|
|
46
|
+
yield s();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const RETRIES_HEADER = "x-retries";
|
|
52
|
+
const RETRYSENT_HEADER = "x-retry-sent";
|
|
53
|
+
class RabbitMqSubscriber {
|
|
54
|
+
constructor(connection, logger) {
|
|
55
|
+
this.connection = connection;
|
|
56
|
+
this.logger = logger;
|
|
57
|
+
this.subscribers = new Map();
|
|
58
|
+
this.transport = connection_1.RABBITMQ_TRANSPORT;
|
|
59
|
+
}
|
|
60
|
+
removeSubscriber(subscribedMessage) {
|
|
61
|
+
for (const [key, value] of this.subscribers) {
|
|
62
|
+
if (value === subscribedMessage) {
|
|
63
|
+
this.subscribers.delete(key);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
startSubscribe(config, onMessage) {
|
|
69
|
+
const queueName = (0, config_1.getQueueName)(config);
|
|
70
|
+
let subscribedMessage = this.subscribers.get(queueName);
|
|
71
|
+
if (subscribedMessage) {
|
|
72
|
+
throw new errors_1.AlreadySubscribingError(queueName);
|
|
73
|
+
}
|
|
74
|
+
subscribedMessage = new SubscribedMessage(this, this.logger, config, onMessage);
|
|
75
|
+
this.subscribers.set(queueName, subscribedMessage);
|
|
76
|
+
subscribedMessage.channel = this.connection.connection.createChannel({
|
|
77
|
+
setup: (channel) => __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
yield this.actuallySubscribe(channel, subscribedMessage);
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
return () => subscribedMessage.stopSubscribe();
|
|
82
|
+
}
|
|
83
|
+
close() {
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
for (const subscribedMessage of this.subscribers.values()) {
|
|
86
|
+
yield subscribedMessage.stopSubscribe();
|
|
87
|
+
}
|
|
88
|
+
this.subscribers.clear();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
static getRetries(msg) {
|
|
92
|
+
var _a;
|
|
93
|
+
return typeof ((_a = msg.properties.headers) === null || _a === void 0 ? void 0 : _a[RETRIES_HEADER]) === "number"
|
|
94
|
+
? msg.properties.headers[RETRIES_HEADER]
|
|
95
|
+
: msg.fields.redelivered
|
|
96
|
+
? 1
|
|
97
|
+
: 0;
|
|
98
|
+
}
|
|
99
|
+
static getSentDates(msg) {
|
|
100
|
+
var _a;
|
|
101
|
+
const firstSent = typeof msg.properties.timestamp === "number"
|
|
102
|
+
? new Date(msg.properties.timestamp)
|
|
103
|
+
: undefined;
|
|
104
|
+
const lastSent = typeof ((_a = msg.properties.headers) === null || _a === void 0 ? void 0 : _a[RETRYSENT_HEADER]) === "number"
|
|
105
|
+
? new Date(msg.properties.headers[RETRYSENT_HEADER])
|
|
106
|
+
: firstSent;
|
|
107
|
+
return { firstSent, lastSent };
|
|
108
|
+
}
|
|
109
|
+
actuallySubscribe(channel, subscribedMessage) {
|
|
110
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
+
const retryDelay = subscribedMessage.config.retryDelay || 30000; // default 30 second retry delay
|
|
112
|
+
const onRabbitMessage = (msg) => __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
114
|
+
const json = msg.content.toString("utf8");
|
|
115
|
+
const retries = RabbitMqSubscriber.getRetries(msg);
|
|
116
|
+
const sentDates = RabbitMqSubscriber.getSentDates(msg);
|
|
117
|
+
const context = Object.assign(Object.assign({ retries }, sentDates), { exchangeName: msg.fields.exchange, routingKey: msg.fields.routingKey });
|
|
118
|
+
let messageResult;
|
|
119
|
+
try {
|
|
120
|
+
messageResult = yield subscribedMessage.onMessage(json, context);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
messageResult = interfaces_1.MessageResult.Retry;
|
|
124
|
+
this.logger.error(`RabbitMq consumer unhandled exception, retrying message to ${retryExchangeName} ${msg.fields.routingKey}`, err);
|
|
125
|
+
}
|
|
126
|
+
if (messageResult === interfaces_1.MessageResult.Ok) {
|
|
127
|
+
channel.ack(msg);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (messageResult === interfaces_1.MessageResult.Retry &&
|
|
131
|
+
subscribedMessage.config.retries &&
|
|
132
|
+
retries >= subscribedMessage.config.retries) {
|
|
133
|
+
messageResult = interfaces_1.MessageResult.Fail;
|
|
134
|
+
}
|
|
135
|
+
if (messageResult === interfaces_1.MessageResult.Fail) {
|
|
136
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `RabbitMq retry count exceeded - sending message to DLQ`);
|
|
137
|
+
channel.nack(msg, undefined, false);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const ok = yield ((_c = subscribedMessage.channel) === null || _c === void 0 ? void 0 : _c.publish(retryExchangeName, msg.fields.routingKey, msg.content, {
|
|
141
|
+
contentType: msg.properties.contentType,
|
|
142
|
+
expiration: retryDelay,
|
|
143
|
+
timestamp: (_d = sentDates.firstSent) === null || _d === void 0 ? void 0 : _d.valueOf(),
|
|
144
|
+
headers: {
|
|
145
|
+
[RETRIES_HEADER]: retries + 1,
|
|
146
|
+
[RETRYSENT_HEADER]: new Date().valueOf(),
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
if (ok) {
|
|
150
|
+
(_f = (_e = this.logger).debug) === null || _f === void 0 ? void 0 : _f.call(_e, `RabbitMq retry queue success`);
|
|
151
|
+
channel.ack(msg);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
(_h = (_g = this.logger).debug) === null || _h === void 0 ? void 0 : _h.call(_g, `RabbitMq retry queue failure - nack-ing message instead`);
|
|
155
|
+
channel.nack(msg, undefined, true);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
const config = subscribedMessage.config;
|
|
159
|
+
const setupResult = yield this.connection.assertQueueAndBindings(channel, config);
|
|
160
|
+
yield channel.prefetch(config.prefetch === undefined ? 5 : config.prefetch);
|
|
161
|
+
const queueName = config.queueType === "dead-letter"
|
|
162
|
+
? setupResult.dlQueueName
|
|
163
|
+
: setupResult.queueName;
|
|
164
|
+
const retryExchangeName = setupResult.retryExchangeName;
|
|
165
|
+
yield channel.consume(queueName, (m) => {
|
|
166
|
+
if (m) {
|
|
167
|
+
subscribedMessage.busy++;
|
|
168
|
+
onRabbitMessage(m)
|
|
169
|
+
.catch((err) => {
|
|
170
|
+
this.logger.error("Unexpected error handling RabbitMq message", err);
|
|
171
|
+
try {
|
|
172
|
+
channel.nack(m, undefined, true);
|
|
173
|
+
}
|
|
174
|
+
catch (err2) {
|
|
175
|
+
this.logger.error("Unexpected error handling RabbitMq message during NACK attempt", err2);
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
.finally(() => {
|
|
179
|
+
subscribedMessage.busy--;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
channel.on("error", (err) => {
|
|
184
|
+
var _a, _b;
|
|
185
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `RabbitMQ consumer error for ${queueName}: ${err}`);
|
|
186
|
+
});
|
|
187
|
+
this.logger.log(`RabbitMQ consumer started for ${queueName}`);
|
|
188
|
+
// stop will cancel the subscriber
|
|
189
|
+
subscribedMessage.stop = () => __awaiter(this, void 0, void 0, function* () {
|
|
190
|
+
var _a, _b;
|
|
191
|
+
yield channel.close();
|
|
192
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `RabbitMQ consumer stopped for ${queueName}`);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.RabbitMqSubscriber = RabbitMqSubscriber;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.delay = delay;
|
|
4
|
+
/**
|
|
5
|
+
* Utility function for delay
|
|
6
|
+
* @param ms
|
|
7
|
+
* @returns
|
|
8
|
+
*/
|
|
9
|
+
function delay(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BaseMessage, Logger, MessageContext, MessageResult, PubSubConfiguration, SubscriberProvider } from "./interfaces";
|
|
2
|
+
export interface SubscriberEventContext {
|
|
3
|
+
message: string;
|
|
4
|
+
context: MessageContext;
|
|
5
|
+
config: PubSubConfiguration;
|
|
6
|
+
}
|
|
7
|
+
export interface MessageHandledEventContext extends SubscriberEventContext {
|
|
8
|
+
durationMs: number;
|
|
9
|
+
result?: MessageResult;
|
|
10
|
+
err?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface SubscriberEvents {
|
|
13
|
+
schemaError: (context: SubscriberEventContext) => void;
|
|
14
|
+
messageHandled: (context: MessageHandledEventContext) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare class Subscriber {
|
|
17
|
+
private readonly logger;
|
|
18
|
+
private subscriberProvdiers;
|
|
19
|
+
private readonly events;
|
|
20
|
+
private readonly subscribedMessages;
|
|
21
|
+
constructor(logger: Logger, providers?: SubscriberProvider[]);
|
|
22
|
+
on<TU extends keyof SubscriberEvents>(event: TU, listener: SubscriberEvents[TU]): this;
|
|
23
|
+
subscribe<TMessage extends BaseMessage>(config: PubSubConfiguration, onMessage: (msg: TMessage, context: MessageContext) => Promise<MessageResult> | MessageResult): Promise<void>;
|
|
24
|
+
unsubscribe(): Promise<void>;
|
|
25
|
+
addProvider(provider: SubscriberProvider): void;
|
|
26
|
+
removeProvider(providerOrTransport: SubscriberProvider | string): boolean;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.Subscriber = void 0;
|
|
13
|
+
const interfaces_1 = require("./interfaces");
|
|
14
|
+
const errors_1 = require("./errors");
|
|
15
|
+
const events_1 = require("events");
|
|
16
|
+
class SubscribedMessage {
|
|
17
|
+
constructor(stop) {
|
|
18
|
+
this.stop = stop;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class Subscriber {
|
|
22
|
+
constructor(logger, providers) {
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
this.subscriberProvdiers = new Map();
|
|
25
|
+
this.events = new events_1.EventEmitter();
|
|
26
|
+
this.subscribedMessages = new Map();
|
|
27
|
+
if (providers) {
|
|
28
|
+
providers.forEach((provider) => this.subscriberProvdiers.set(provider.transport, provider));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
on(event, listener) {
|
|
32
|
+
this.events.on(event, listener);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
36
|
+
subscribe(config, onMessage) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
var _a, _b;
|
|
39
|
+
let subscribedMessage = this.subscribedMessages.get(config);
|
|
40
|
+
const schema = config.schema;
|
|
41
|
+
if (subscribedMessage) {
|
|
42
|
+
throw new errors_1.AlreadySubscribingError(typeof config);
|
|
43
|
+
}
|
|
44
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Initializing subscriber for provider ${config.transport}:${config.name}`);
|
|
45
|
+
const provider = this.subscriberProvdiers.get(config.transport);
|
|
46
|
+
if (!provider) {
|
|
47
|
+
throw new errors_1.MissingPubSubProviderError(`No provider configured for ${config.transport}`);
|
|
48
|
+
}
|
|
49
|
+
const transform = (jsonStr, context) => __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
var _a;
|
|
51
|
+
const start = new Date().valueOf();
|
|
52
|
+
const jsonObject = JSON.parse(jsonStr);
|
|
53
|
+
const r = schema.safeParse(jsonObject);
|
|
54
|
+
if (!r.success) {
|
|
55
|
+
const id = interfaces_1.IdMetaSchema.safeParse(jsonObject);
|
|
56
|
+
if (id.success) {
|
|
57
|
+
this.logger.warn(`Unable to parse message ${id.data.id} created at ${(_a = id.data.meta) === null || _a === void 0 ? void 0 : _a.createdAt} for ${config.transport}:${config.name} - schemaError`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.logger.warn(`Unable to parse message for ${config.transport}:${config.name} - schemaError`);
|
|
61
|
+
}
|
|
62
|
+
const handledEventContext = {
|
|
63
|
+
message: jsonStr,
|
|
64
|
+
context,
|
|
65
|
+
config,
|
|
66
|
+
durationMs: new Date().valueOf() - start,
|
|
67
|
+
result: interfaces_1.MessageResult.Fail,
|
|
68
|
+
};
|
|
69
|
+
this.events.emit("schemaError", handledEventContext);
|
|
70
|
+
this.events.emit("messageHandled", handledEventContext);
|
|
71
|
+
return interfaces_1.MessageResult.Fail;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const result = yield onMessage(r.data, context);
|
|
75
|
+
const eventContext = {
|
|
76
|
+
message: jsonStr,
|
|
77
|
+
context,
|
|
78
|
+
config,
|
|
79
|
+
durationMs: new Date().valueOf() - start,
|
|
80
|
+
result,
|
|
81
|
+
};
|
|
82
|
+
this.events.emit("messageHandled", eventContext);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const eventContext = {
|
|
87
|
+
message: jsonStr,
|
|
88
|
+
context,
|
|
89
|
+
config,
|
|
90
|
+
durationMs: new Date().valueOf() - start,
|
|
91
|
+
err,
|
|
92
|
+
};
|
|
93
|
+
this.events.emit("messageHandled", eventContext);
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
this.logger.log(`Starting subscriber for ${config.transport}:${config.name}`);
|
|
98
|
+
subscribedMessage = new SubscribedMessage(yield provider.startSubscribe(config, transform));
|
|
99
|
+
this.subscribedMessages.set(config, subscribedMessage);
|
|
100
|
+
this.logger.log(`Started subscriber for ${config.transport}:${config.name}`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
unsubscribe() {
|
|
104
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
105
|
+
this.logger.log(`Stopping all ${this.subscribedMessages.size} subscribers`);
|
|
106
|
+
for (const subscriber of this.subscribedMessages.values()) {
|
|
107
|
+
yield subscriber.stop();
|
|
108
|
+
}
|
|
109
|
+
this.logger.log("Stopped all subscribers");
|
|
110
|
+
this.subscribedMessages.clear();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
addProvider(provider) {
|
|
114
|
+
var _a, _b;
|
|
115
|
+
this.subscriberProvdiers.set(provider.transport, provider);
|
|
116
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Subscriber added provider for ${provider.transport}`);
|
|
117
|
+
}
|
|
118
|
+
removeProvider(providerOrTransport) {
|
|
119
|
+
var _a, _b;
|
|
120
|
+
let transport;
|
|
121
|
+
if (typeof providerOrTransport === "string") {
|
|
122
|
+
transport = providerOrTransport;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
transport = providerOrTransport.transport;
|
|
126
|
+
}
|
|
127
|
+
const result = this.subscriberProvdiers.delete(transport);
|
|
128
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Subscriber removed provider for ${transport}`);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
close() {
|
|
132
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
for (const provider of this.subscriberProvdiers.values()) {
|
|
134
|
+
yield provider.close();
|
|
135
|
+
}
|
|
136
|
+
this.subscriberProvdiers.clear();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.Subscriber = Subscriber;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@palmetto/pubsub",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./dist/main.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"lint": "yarn run -T eslint --fix ./src",
|
|
7
|
+
"format": "yarn run -T prettier --write --loglevel warn .",
|
|
8
|
+
"tc": "tsc --noEmit",
|
|
9
|
+
"build": "yarn clean && tsc -p tsconfig.build.json",
|
|
10
|
+
"clean": "rm -rf ./dist/",
|
|
11
|
+
"ci:build": "tsc -p tsconfig.build.json",
|
|
12
|
+
"ci:lint": "yarn run -T eslint . && yarn run -T prettier --check --loglevel warn .",
|
|
13
|
+
"ci:tc": "yarn tc",
|
|
14
|
+
"hook:lint": "eslint --cache --fix",
|
|
15
|
+
"hook:format": "prettier --write --loglevel warn",
|
|
16
|
+
"hook:tc": "yarn tc",
|
|
17
|
+
"prepublishOnly": "yarn build",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest watch",
|
|
20
|
+
"redis-up": "docker compose -f redis-compose.yml up -d",
|
|
21
|
+
"rabbitmq-up": "docker compose -f rabbitmq-compose.yml up -d"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/amqplib": "^0",
|
|
25
|
+
"@types/node": "^24.2.1",
|
|
26
|
+
"amqp-connection-manager": "^4.1.14",
|
|
27
|
+
"amqplib": "^0.10.8",
|
|
28
|
+
"bullmq": "^5.58.0",
|
|
29
|
+
"ts-node": "^10.9.2",
|
|
30
|
+
"typescript": "^5.8.3",
|
|
31
|
+
"vitest": "^3.2.4",
|
|
32
|
+
"zod": "^4.0.5"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist/**/*",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"crypto-hash": "^3.1.0",
|
|
46
|
+
"uuid": "^11.1.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"amqp-connection-manager": "^4.1.14",
|
|
50
|
+
"amqplib": "^0.10.8",
|
|
51
|
+
"bullmq": "^5.58.0",
|
|
52
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|