@palmetto/pubsub 3.1.0 → 3.2.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/dist/gcppubsub/config.d.ts +34 -0
- package/dist/gcppubsub/config.js +59 -0
- package/dist/gcppubsub/connection.d.ts +14 -0
- package/dist/gcppubsub/connection.js +38 -0
- package/dist/gcppubsub/main.d.ts +5 -0
- package/dist/gcppubsub/main.js +21 -0
- package/dist/gcppubsub/publisher.d.ts +14 -0
- package/dist/gcppubsub/publisher.js +66 -0
- package/dist/gcppubsub/pubsub.d.ts +18 -0
- package/dist/gcppubsub/pubsub.js +49 -0
- package/dist/gcppubsub/subscriber.d.ts +19 -0
- package/dist/gcppubsub/subscriber.js +193 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +1 -0
- package/package.json +4 -2
- package/src/gcppubsub/README.md +134 -0
- package/src/rabbitmq/README.md +16 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type PubSub } from "@google-cloud/pubsub";
|
|
2
|
+
import { MessageContext, PubSubConfiguration } from "../interfaces.js";
|
|
3
|
+
import { GCP_PUBSUB_TRANSPORT } from "./connection.js";
|
|
4
|
+
export interface GcpPubSubConfiguration extends PubSubConfiguration {
|
|
5
|
+
/**
|
|
6
|
+
* Transport is always GCP_PUBSUB_TRANSPORT
|
|
7
|
+
*/
|
|
8
|
+
transport: typeof GCP_PUBSUB_TRANSPORT;
|
|
9
|
+
/**
|
|
10
|
+
* The Pub/Sub topic name. Defaults to `name` if omitted.
|
|
11
|
+
*/
|
|
12
|
+
topicName?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The Pub/Sub subscription name. Required for subscribers. Defaults to `name` if omitted.
|
|
15
|
+
*/
|
|
16
|
+
subscriptionName?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Flow control: max outstanding messages (default: 5)
|
|
19
|
+
*/
|
|
20
|
+
maxMessages?: number;
|
|
21
|
+
}
|
|
22
|
+
export type TopicType = "default" | "dead-letter";
|
|
23
|
+
export declare const TopicNameExtensions: Record<TopicType, string | undefined>;
|
|
24
|
+
export interface GcpPubSubMessageContext extends MessageContext {
|
|
25
|
+
messageId: string;
|
|
26
|
+
subscriptionName: string;
|
|
27
|
+
topicName: string;
|
|
28
|
+
orderingKey?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function getTopicName(config: GcpPubSubConfiguration, topicType: TopicType): string;
|
|
31
|
+
export declare function getSubscriptionName(config: GcpPubSubConfiguration): string;
|
|
32
|
+
export declare function isGcpPubSubConfiguration(config: PubSubConfiguration): config is GcpPubSubConfiguration;
|
|
33
|
+
export declare function ensureTopic(client: PubSub, topicName: string): Promise<void>;
|
|
34
|
+
export declare function ensureSubscription(client: PubSub, topicName: string, subscriptionName: string, retryDelay: number): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
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.TopicNameExtensions = void 0;
|
|
13
|
+
exports.getTopicName = getTopicName;
|
|
14
|
+
exports.getSubscriptionName = getSubscriptionName;
|
|
15
|
+
exports.isGcpPubSubConfiguration = isGcpPubSubConfiguration;
|
|
16
|
+
exports.ensureTopic = ensureTopic;
|
|
17
|
+
exports.ensureSubscription = ensureSubscription;
|
|
18
|
+
const connection_js_1 = require("./connection.js");
|
|
19
|
+
exports.TopicNameExtensions = {
|
|
20
|
+
"dead-letter": ".dl",
|
|
21
|
+
default: undefined,
|
|
22
|
+
};
|
|
23
|
+
function getTopicName(config, topicType) {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
const baseName = (_a = config.topicName) !== null && _a !== void 0 ? _a : config.name;
|
|
26
|
+
const extension = (_b = exports.TopicNameExtensions[topicType]) !== null && _b !== void 0 ? _b : "";
|
|
27
|
+
return `${baseName}${extension}`;
|
|
28
|
+
}
|
|
29
|
+
function getSubscriptionName(config) {
|
|
30
|
+
var _a;
|
|
31
|
+
return (_a = config.subscriptionName) !== null && _a !== void 0 ? _a : getTopicName(config, "default");
|
|
32
|
+
}
|
|
33
|
+
function isGcpPubSubConfiguration(config) {
|
|
34
|
+
return config.transport === connection_js_1.GCP_PUBSUB_TRANSPORT;
|
|
35
|
+
}
|
|
36
|
+
function ensureTopic(client, topicName) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
const topics = (yield client.getTopics())[0];
|
|
39
|
+
if (!topics.some((t) => t.name.endsWith(`/${topicName}`))) {
|
|
40
|
+
yield client.createTopic(topicName);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function ensureSubscription(client, topicName, subscriptionName, retryDelay) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
yield ensureTopic(client, topicName);
|
|
47
|
+
const subscriptions = (yield client.getSubscriptions())[0];
|
|
48
|
+
if (!subscriptions.some((s) => s.name.endsWith(`/${subscriptionName}`))) {
|
|
49
|
+
yield client.createSubscription(topicName, subscriptionName, {
|
|
50
|
+
retryPolicy: {
|
|
51
|
+
minimumBackoff: {
|
|
52
|
+
nanos: (retryDelay % 1000) * 1000000,
|
|
53
|
+
seconds: Math.floor(retryDelay / 1000),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type * as gcppubsub from "@google-cloud/pubsub";
|
|
2
|
+
import { Logger } from "../interfaces.js";
|
|
3
|
+
export declare const GCP_PUBSUB_TRANSPORT = "gcp-pubsub";
|
|
4
|
+
export interface GcpPubSubConnectionConfig {
|
|
5
|
+
projectId?: string;
|
|
6
|
+
clientConfig?: gcppubsub.ClientConfig;
|
|
7
|
+
}
|
|
8
|
+
export declare class GcpPubSubConnection {
|
|
9
|
+
readonly client: gcppubsub.PubSub;
|
|
10
|
+
private readonly logger;
|
|
11
|
+
static create(config: GcpPubSubConnectionConfig, logger: Logger): Promise<GcpPubSubConnection>;
|
|
12
|
+
private constructor();
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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.GcpPubSubConnection = exports.GCP_PUBSUB_TRANSPORT = void 0;
|
|
13
|
+
const lazy_load_js_1 = require("../lazy-load.js");
|
|
14
|
+
exports.GCP_PUBSUB_TRANSPORT = "gcp-pubsub";
|
|
15
|
+
class GcpPubSubConnection {
|
|
16
|
+
static create(config, logger) {
|
|
17
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
18
|
+
const GcpPubSubPackage = yield (0, lazy_load_js_1.lazyLoad)({
|
|
19
|
+
packageName: "@google-cloud/pubsub",
|
|
20
|
+
context: "GcpPubSubConnection",
|
|
21
|
+
});
|
|
22
|
+
const client = new GcpPubSubPackage.PubSub(Object.assign({ projectId: config.projectId }, config.clientConfig));
|
|
23
|
+
return new GcpPubSubConnection(client, logger);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
constructor(client, logger) {
|
|
27
|
+
this.client = client;
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
}
|
|
30
|
+
close() {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, "GCP Pub/Sub closing connection");
|
|
34
|
+
yield this.client.close();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.GcpPubSubConnection = GcpPubSubConnection;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./config.js"), exports);
|
|
18
|
+
__exportStar(require("./connection.js"), exports);
|
|
19
|
+
__exportStar(require("./publisher.js"), exports);
|
|
20
|
+
__exportStar(require("./subscriber.js"), exports);
|
|
21
|
+
__exportStar(require("./pubsub.js"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Logger, PublisherProvider, PublishMessageOptions } from "../interfaces.js";
|
|
2
|
+
import { GcpPubSubConnection } from "./connection.js";
|
|
3
|
+
import { GcpPubSubConfiguration } from "./config.js";
|
|
4
|
+
export declare class GcpPubSubPublisher implements PublisherProvider {
|
|
5
|
+
private readonly connection;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
private readonly topics;
|
|
8
|
+
constructor(connection: GcpPubSubConnection, logger: Logger);
|
|
9
|
+
readonly transport: string;
|
|
10
|
+
init(config: GcpPubSubConfiguration): Promise<void>;
|
|
11
|
+
publish(config: GcpPubSubConfiguration, messages: string[], options?: PublishMessageOptions): Promise<void>;
|
|
12
|
+
enrichPublishedMesssageLog(config: GcpPubSubConfiguration): Record<string, unknown>;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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.GcpPubSubPublisher = void 0;
|
|
13
|
+
const errors_js_1 = require("../errors.js");
|
|
14
|
+
const connection_js_1 = require("./connection.js");
|
|
15
|
+
const config_js_1 = require("./config.js");
|
|
16
|
+
class GcpPubSubPublisher {
|
|
17
|
+
constructor(connection, logger) {
|
|
18
|
+
this.connection = connection;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
this.topics = new Map();
|
|
21
|
+
this.transport = connection_js_1.GCP_PUBSUB_TRANSPORT;
|
|
22
|
+
}
|
|
23
|
+
init(config) {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
const topicName = (0, config_js_1.getTopicName)(config, "default");
|
|
27
|
+
yield (0, config_js_1.ensureTopic)(this.connection.client, topicName);
|
|
28
|
+
const topic = this.connection.client.topic(topicName);
|
|
29
|
+
this.topics.set(topicName, topic);
|
|
30
|
+
(_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `GCP Pub/Sub publisher initialized for topic ${topicName}`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
publish(config, messages, options) {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
if (options === null || options === void 0 ? void 0 : options.scheduledAt) {
|
|
36
|
+
throw new errors_js_1.PublishError("Scheduled publishing is not supported for GCP Pub/Sub", messages);
|
|
37
|
+
}
|
|
38
|
+
const topicName = (0, config_js_1.getTopicName)(config, "default");
|
|
39
|
+
let topic = this.topics.get(topicName);
|
|
40
|
+
if (!topic) {
|
|
41
|
+
topic = this.connection.client.topic(topicName);
|
|
42
|
+
this.topics.set(topicName, topic);
|
|
43
|
+
}
|
|
44
|
+
const oks = yield Promise.allSettled(messages.map((message) => topic.publishMessage({
|
|
45
|
+
data: Buffer.from(message, "utf8"),
|
|
46
|
+
})));
|
|
47
|
+
const failedMessages = oks
|
|
48
|
+
.map((ok, index) => ({ ok, index }))
|
|
49
|
+
.filter(({ ok }) => ok.status === "rejected")
|
|
50
|
+
.map(({ index }) => messages[index]);
|
|
51
|
+
if (failedMessages.length > 0) {
|
|
52
|
+
const allOrSome = failedMessages.length === messages.length ? "all" : "some";
|
|
53
|
+
throw new errors_js_1.PublishError(`GCP Pub/Sub publish to topic ${topicName} failed for ${allOrSome} messages`, failedMessages);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
enrichPublishedMesssageLog(config) {
|
|
58
|
+
return {
|
|
59
|
+
topicName: (0, config_js_1.getTopicName)(config, "default"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
close() {
|
|
63
|
+
this.topics.clear();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.GcpPubSubPublisher = GcpPubSubPublisher;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Logger, MessageContext, MessageResult, PublishMessageOptions, PubSubProvider, StopSubscribe } from "../interfaces.js";
|
|
2
|
+
import { GcpPubSubConfiguration } from "./config.js";
|
|
3
|
+
import { GcpPubSubConnection } from "./connection.js";
|
|
4
|
+
/**
|
|
5
|
+
* Combines a GcpPubSubPublisher and GcpPubSubSubscriber into a single instance
|
|
6
|
+
*/
|
|
7
|
+
export declare class GcpPubSubProvider implements PubSubProvider {
|
|
8
|
+
private readonly publisher;
|
|
9
|
+
private readonly subscriber;
|
|
10
|
+
constructor(connection: GcpPubSubConnection, logger: Logger);
|
|
11
|
+
readonly transport: string;
|
|
12
|
+
publish(config: GcpPubSubConfiguration, messages: string[], options?: PublishMessageOptions): Promise<void>;
|
|
13
|
+
startSubscribe(config: GcpPubSubConfiguration, onMessage: (s: string, context: MessageContext) => Promise<MessageResult> | MessageResult): Promise<StopSubscribe>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
init(config: GcpPubSubConfiguration): Promise<void>;
|
|
16
|
+
enrichPublishedMesssageLog(config: GcpPubSubConfiguration): Record<string, unknown>;
|
|
17
|
+
enrichHandledMesssageLog(config: GcpPubSubConfiguration): Record<string, unknown>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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.GcpPubSubProvider = void 0;
|
|
13
|
+
const connection_js_1 = require("./connection.js");
|
|
14
|
+
const publisher_js_1 = require("./publisher.js");
|
|
15
|
+
const subscriber_js_1 = require("./subscriber.js");
|
|
16
|
+
/**
|
|
17
|
+
* Combines a GcpPubSubPublisher and GcpPubSubSubscriber into a single instance
|
|
18
|
+
*/
|
|
19
|
+
class GcpPubSubProvider {
|
|
20
|
+
constructor(connection, logger) {
|
|
21
|
+
this.transport = connection_js_1.GCP_PUBSUB_TRANSPORT;
|
|
22
|
+
this.publisher = new publisher_js_1.GcpPubSubPublisher(connection, logger);
|
|
23
|
+
this.subscriber = new subscriber_js_1.GcpPubSubSubscriber(connection, logger);
|
|
24
|
+
}
|
|
25
|
+
publish(config, messages, options) {
|
|
26
|
+
return this.publisher.publish(config, messages, options);
|
|
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 this.subscriber.close();
|
|
34
|
+
this.publisher.close();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
init(config) {
|
|
38
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
yield this.publisher.init(config);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
enrichPublishedMesssageLog(config) {
|
|
43
|
+
return this.publisher.enrichPublishedMesssageLog(config);
|
|
44
|
+
}
|
|
45
|
+
enrichHandledMesssageLog(config) {
|
|
46
|
+
return this.subscriber.enrichHandledMesssageLog(config);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.GcpPubSubProvider = GcpPubSubProvider;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Logger, MessageResult, StopSubscribe, SubscriberProvider } from "../interfaces.js";
|
|
2
|
+
import { GcpPubSubConfiguration, GcpPubSubMessageContext } from "./config.js";
|
|
3
|
+
import { GcpPubSubConnection } from "./connection.js";
|
|
4
|
+
export declare class GcpPubSubSubscriber implements SubscriberProvider {
|
|
5
|
+
private readonly connection;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
private readonly subscriptions;
|
|
8
|
+
private lastMessageDate;
|
|
9
|
+
constructor(connection: GcpPubSubConnection, logger: Logger);
|
|
10
|
+
readonly transport: string;
|
|
11
|
+
startSubscribe(config: GcpPubSubConfiguration, onMessage: (s: string, context: GcpPubSubMessageContext) => Promise<MessageResult> | MessageResult): Promise<StopSubscribe>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
enrichHandledMesssageLog(config: GcpPubSubConfiguration): Record<string, unknown>;
|
|
14
|
+
private consumeMessage;
|
|
15
|
+
/**
|
|
16
|
+
* Wait for in-flight message handling to complete before shutting down
|
|
17
|
+
*/
|
|
18
|
+
private waitForMessagesToComplete;
|
|
19
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
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.GcpPubSubSubscriber = void 0;
|
|
13
|
+
const trace_1 = require("@palmetto/trace");
|
|
14
|
+
const interfaces_js_1 = require("../interfaces.js");
|
|
15
|
+
const errors_js_1 = require("../errors.js");
|
|
16
|
+
const create_log_error_payload_js_1 = require("../create-log-error-payload.js");
|
|
17
|
+
const config_js_1 = require("./config.js");
|
|
18
|
+
const connection_js_1 = require("./connection.js");
|
|
19
|
+
const RETRY_COUNT_ATTR = "x-retry-count";
|
|
20
|
+
const FIRST_PUBLISHED_ATTR = "x-first-published";
|
|
21
|
+
const SHUTDOWN_DELAY = 1000;
|
|
22
|
+
class GcpPubSubSubscriber {
|
|
23
|
+
constructor(connection, logger) {
|
|
24
|
+
this.connection = connection;
|
|
25
|
+
this.logger = logger;
|
|
26
|
+
this.subscriptions = new Map();
|
|
27
|
+
this.lastMessageDate = 0;
|
|
28
|
+
this.transport = connection_js_1.GCP_PUBSUB_TRANSPORT;
|
|
29
|
+
}
|
|
30
|
+
startSubscribe(config, onMessage) {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
const subscriptionName = (0, config_js_1.getSubscriptionName)(config);
|
|
34
|
+
if (this.subscriptions.has(subscriptionName)) {
|
|
35
|
+
throw new errors_js_1.AlreadySubscribingError(subscriptionName);
|
|
36
|
+
}
|
|
37
|
+
const topicName = (0, config_js_1.getTopicName)(config, "default");
|
|
38
|
+
const dlqTopicName = (0, config_js_1.getTopicName)(config, "dead-letter");
|
|
39
|
+
const retryDelay = (_a = config.retryDelay) !== null && _a !== void 0 ? _a : 30000;
|
|
40
|
+
yield (0, config_js_1.ensureSubscription)(this.connection.client, topicName, subscriptionName, retryDelay);
|
|
41
|
+
yield (0, config_js_1.ensureTopic)(this.connection.client, dlqTopicName);
|
|
42
|
+
const subscription = this.connection.client.subscription(subscriptionName, {
|
|
43
|
+
flowControl: {
|
|
44
|
+
maxMessages: (_b = config.maxMessages) !== null && _b !== void 0 ? _b : 5,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const dlTopic = this.connection.client.topic(dlqTopicName);
|
|
48
|
+
const messageHandler = (message) => {
|
|
49
|
+
void this.consumeMessage({
|
|
50
|
+
config,
|
|
51
|
+
message,
|
|
52
|
+
onMessage,
|
|
53
|
+
subscriptionName,
|
|
54
|
+
topicName,
|
|
55
|
+
dlTopic,
|
|
56
|
+
retryDelay,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const errorHandler = (err) => {
|
|
60
|
+
const log = {
|
|
61
|
+
message: `GCP Pub/Sub subscriber error for ${subscriptionName}`,
|
|
62
|
+
error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err),
|
|
63
|
+
};
|
|
64
|
+
this.logger.error(log);
|
|
65
|
+
};
|
|
66
|
+
subscription.on("message", messageHandler);
|
|
67
|
+
subscription.on("error", errorHandler);
|
|
68
|
+
this.subscriptions.set(subscriptionName, subscription);
|
|
69
|
+
this.logger.log(`GCP Pub/Sub consumer started for ${subscriptionName}`);
|
|
70
|
+
return () => __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
subscription.removeListener("message", messageHandler);
|
|
72
|
+
subscription.removeListener("error", errorHandler);
|
|
73
|
+
yield this.waitForMessagesToComplete();
|
|
74
|
+
yield subscription.close();
|
|
75
|
+
this.subscriptions.delete(subscriptionName);
|
|
76
|
+
this.logger.log(`GCP Pub/Sub consumer stopped for ${subscriptionName}`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
close() {
|
|
81
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
82
|
+
yield Promise.all([...this.subscriptions.entries()].map((_a) => __awaiter(this, [_a], void 0, function* ([subscriptionName, sub]) {
|
|
83
|
+
yield this.waitForMessagesToComplete();
|
|
84
|
+
yield sub.close();
|
|
85
|
+
this.logger.log(`GCP Pub/Sub consumer stopped for ${subscriptionName}`);
|
|
86
|
+
})));
|
|
87
|
+
this.subscriptions.clear();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
enrichHandledMesssageLog(config) {
|
|
91
|
+
return {
|
|
92
|
+
subscriptionName: (0, config_js_1.getSubscriptionName)(config),
|
|
93
|
+
topicName: (0, config_js_1.getTopicName)(config, "default"),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
consumeMessage(_a) {
|
|
97
|
+
return __awaiter(this, arguments, void 0, function* ({ config, message, onMessage, subscriptionName, topicName, dlTopic, }) {
|
|
98
|
+
yield (0, trace_1.getTracer)().trace("pubsub.gcppubsub.consume", {
|
|
99
|
+
resource: `consume ${config.transport} ${subscriptionName}`,
|
|
100
|
+
}, (span) => __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
102
|
+
try {
|
|
103
|
+
const firstPublished = message.publishTime;
|
|
104
|
+
let retryCount = message.deliveryAttempt - 1;
|
|
105
|
+
let willRetryOnFailure;
|
|
106
|
+
if (retryCount < 0) {
|
|
107
|
+
retryCount = 0;
|
|
108
|
+
willRetryOnFailure = false;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
willRetryOnFailure = retryCount < ((_a = config.retries) !== null && _a !== void 0 ? _a : 0);
|
|
112
|
+
}
|
|
113
|
+
const context = {
|
|
114
|
+
attemptsMade: retryCount,
|
|
115
|
+
firstSent: firstPublished,
|
|
116
|
+
lastSent: undefined,
|
|
117
|
+
willRetryOnFailure,
|
|
118
|
+
messageId: message.id,
|
|
119
|
+
subscriptionName,
|
|
120
|
+
topicName,
|
|
121
|
+
orderingKey: message.orderingKey,
|
|
122
|
+
};
|
|
123
|
+
let messageResult;
|
|
124
|
+
let logPayload;
|
|
125
|
+
try {
|
|
126
|
+
messageResult = yield onMessage(message.data.toString("utf8"), context);
|
|
127
|
+
logPayload = {
|
|
128
|
+
message: `GCP Pub/Sub consumer handled message.`,
|
|
129
|
+
extra: Object.assign({}, context),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
messageResult = interfaces_js_1.MessageResult.Retry;
|
|
134
|
+
logPayload = {
|
|
135
|
+
message: `GCP Pub/Sub consumer unhandled exception.`,
|
|
136
|
+
error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err),
|
|
137
|
+
extra: Object.assign({}, context),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
this.lastMessageDate = Date.now();
|
|
141
|
+
if (messageResult === interfaces_js_1.MessageResult.Ok) {
|
|
142
|
+
(_c = (_b = this.logger).debug) === null || _c === void 0 ? void 0 : _c.call(_b, logPayload);
|
|
143
|
+
message.ack();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (messageResult === interfaces_js_1.MessageResult.Retry && willRetryOnFailure) {
|
|
147
|
+
message.nack();
|
|
148
|
+
logPayload.message += " Retrying message via Pubsub.";
|
|
149
|
+
(_e = (_d = this.logger).debug) === null || _e === void 0 ? void 0 : _e.call(_d, logPayload);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Fail: either explicit Fail or Retry with no retries remaining
|
|
153
|
+
if (messageResult === interfaces_js_1.MessageResult.Retry) {
|
|
154
|
+
logPayload.message += " No more retries.";
|
|
155
|
+
}
|
|
156
|
+
yield dlTopic.publishMessage({
|
|
157
|
+
data: message.data,
|
|
158
|
+
attributes: Object.assign(Object.assign({}, message.attributes), { [RETRY_COUNT_ATTR]: retryCount.toString(), [FIRST_PUBLISHED_ATTR]: firstPublished.toISOString() }),
|
|
159
|
+
});
|
|
160
|
+
logPayload.message += ` Published to dead-letter topic ${dlTopic.name}.`;
|
|
161
|
+
(_g = (_f = this.logger).debug) === null || _g === void 0 ? void 0 : _g.call(_f, logPayload);
|
|
162
|
+
message.ack();
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const logPayload = {
|
|
166
|
+
message: "Unexpected error handling GCP Pub/Sub message",
|
|
167
|
+
error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err),
|
|
168
|
+
extra: {
|
|
169
|
+
subscriptionName,
|
|
170
|
+
message: message.data.toString("utf8"),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
this.logger.error(logPayload);
|
|
174
|
+
span === null || span === void 0 ? void 0 : span.setTag("error", err);
|
|
175
|
+
message.nack();
|
|
176
|
+
}
|
|
177
|
+
}));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Wait for in-flight message handling to complete before shutting down
|
|
182
|
+
*/
|
|
183
|
+
waitForMessagesToComplete() {
|
|
184
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
185
|
+
const waitUntil = this.lastMessageDate + SHUTDOWN_DELAY;
|
|
186
|
+
const waitDuration = waitUntil - Date.now();
|
|
187
|
+
if (waitDuration > 0) {
|
|
188
|
+
yield new Promise((resolve) => setTimeout(resolve, waitDuration));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
exports.GcpPubSubSubscriber = GcpPubSubSubscriber;
|
package/dist/main.d.ts
CHANGED
package/dist/main.js
CHANGED
|
@@ -20,3 +20,4 @@ __exportStar(require("./publisher.js"), exports);
|
|
|
20
20
|
__exportStar(require("./subscriber.js"), exports);
|
|
21
21
|
__exportStar(require("./bullmq/main.js"), exports);
|
|
22
22
|
__exportStar(require("./rabbitmq/main.js"), exports);
|
|
23
|
+
__exportStar(require("./gcppubsub/main.js"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@palmetto/pubsub",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/palmetto/galaxy"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"test:watch": "yarn run test-runner vitest watch"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
+
"@google-cloud/pubsub": "^5.2.3",
|
|
27
28
|
"@palmetto/trace": "^0.1.0",
|
|
28
29
|
"@types/amqplib": "^0",
|
|
29
30
|
"@types/node": "^24.2.1",
|
|
@@ -49,10 +50,11 @@
|
|
|
49
50
|
"uuid": "^11.1.0"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
53
|
+
"@google-cloud/pubsub": "^5.2",
|
|
52
54
|
"@palmetto/trace": "^0.1.0",
|
|
53
55
|
"amqp-connection-manager": "^4.1.14",
|
|
54
56
|
"amqplib": "^0.10.8",
|
|
55
57
|
"bullmq": "^5.58.0",
|
|
56
|
-
"zod": "^4.1
|
|
58
|
+
"zod": "^4.1"
|
|
57
59
|
}
|
|
58
60
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @palmetto/pubsub
|
|
2
|
+
|
|
3
|
+
The GoogleCloud pubsub transport provider for @palmetto/pubsub
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
yarn add @palmetto/pubsub @google-cloud/pubsub zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
1. Define your connection string
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { GcpPubSubConnectionConfig } from "@palmetto/pubsub";
|
|
17
|
+
|
|
18
|
+
const config: GcpPubSubConnectionConfig = {
|
|
19
|
+
projectId: "GOOGLE_PROJECT_ID",
|
|
20
|
+
};
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. Create the connection
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { GcpPubSubConnection } from "@palmetto/pubsub";
|
|
27
|
+
|
|
28
|
+
const connection = await GcpPubSubConnection.create(config, console);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
3. Create the publisher
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Publisher, GcpPubSubPublisher } from "@palmetto/pubsub";
|
|
35
|
+
|
|
36
|
+
const publisher = new Publisher(console, [
|
|
37
|
+
new GcpPubSubPublisher(connection, console),
|
|
38
|
+
]);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
4. Create the subscriber
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Subscriber, GcpPubSubSubscriber } from "@palmetto/pubsub";
|
|
45
|
+
|
|
46
|
+
const subscriber = new Subscriber(console, [
|
|
47
|
+
new GcpPubSubSubscriber(connection, console),
|
|
48
|
+
]);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
5. Create the queue configuration
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import {
|
|
55
|
+
GcpPubSubConfiguration,
|
|
56
|
+
GCP_PUBSUB_TRANSPORT,
|
|
57
|
+
} from "@palmetto/pubsub";
|
|
58
|
+
|
|
59
|
+
import { z } from "zod";
|
|
60
|
+
|
|
61
|
+
const MyModelSchema = z.object({...});
|
|
62
|
+
|
|
63
|
+
const config: GcpPubSubConfiguration = {
|
|
64
|
+
name: "my-name",
|
|
65
|
+
topicName: "my-topic-name", // default: name
|
|
66
|
+
subscriptionName: "my-subscription-name", // default: topicName or name
|
|
67
|
+
schema: MyModelSchema,
|
|
68
|
+
transport: GCP_PUBSUB_TRANSPORT,
|
|
69
|
+
retries: 10,
|
|
70
|
+
retryDelay: 1_000, // Note: retryDelay must be configured as a retry policy of the subscription. pubsub will create the subscription if it does not exist, but won't update an existing subscription.
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
6. Initialize the publisher
|
|
75
|
+
|
|
76
|
+
For performance reasons, it's a good idea to initialize the publisher on your API startup. This can reduce the latency for the first message published.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await publisher.init(config);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
7. Publish a message
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const message: MyModel = {... };
|
|
86
|
+
|
|
87
|
+
await publisher.publish(config, message);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
8. Subscribe to a queue
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { GcpPubSubMessageContext } from "@palmetto/pubsub";
|
|
94
|
+
|
|
95
|
+
subscriber.addSubscriber(queue, (message: MyModel, context: GcpPubSubMessageContext) => {
|
|
96
|
+
...
|
|
97
|
+
return MessageResult.Ok;
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
9. Subscribe to the dead-letter topic if you want to re-process completely failed messages. Pass the dead-letter topicName to the config.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { getTopicName } from "@palmetto/pubsub";
|
|
105
|
+
|
|
106
|
+
const dlConfig = {
|
|
107
|
+
...config,
|
|
108
|
+
topicName: getTopicName(config, "dead-letter")
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
subscriber.addSubscriber(dlConfig, (message: MyModel, context: GcpPubSubMessageContext) => {
|
|
112
|
+
...
|
|
113
|
+
return MessageResult.Ok; // returning Ok will remove the message from the dead-letter topic
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Message failures
|
|
118
|
+
|
|
119
|
+
Message failures in Google Cloud pubsub subscribers are handled by pubsub's retry policies. Messages are `nack()`'ed and retried (or discarded) based on the GCP subscription's retry policy.
|
|
120
|
+
|
|
121
|
+
### Retrying messages
|
|
122
|
+
|
|
123
|
+
These properties configure message retries when `@palmetto/pubsub` creates the subscription. If `@palmetto/pubsub` didn't create the subscription, whatever retry policy in place is what will be used (by default there are no retries).
|
|
124
|
+
|
|
125
|
+
- `retries` : specifies the number of retries after which the message is sent to the dead-letter topic. When this value is undefined, the message will not retry.
|
|
126
|
+
- `retryDelay`: specifies the number of milliseconds between retries.
|
|
127
|
+
|
|
128
|
+
Failed messages are `nack()`'ed to pubsub and they are retried with the delay configured for the subscription.
|
|
129
|
+
Once the `retries` are exhausted, the message is `ack()`'ed and re-published to the dead-letter topic.
|
|
130
|
+
The dead-letter topic is the same as the subscription topic, but ends in `.dl`.
|
|
131
|
+
|
|
132
|
+
### Failing messages
|
|
133
|
+
|
|
134
|
+
A dead-letter topic is created using the topic name and ends with `.dl`. Messages that fail after the configured `retries` are re-published to the dead-letter topic.
|
package/src/rabbitmq/README.md
CHANGED
|
@@ -13,6 +13,8 @@ yarn add @palmetto/pubsub amqp-connection-manager amqplib zod
|
|
|
13
13
|
1. Define your connection string
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
+
import { RabbitMqConnectionConfig } from "@palmetto/pubsub";
|
|
17
|
+
|
|
16
18
|
const config: RabbitMqConnectionConfig = {
|
|
17
19
|
host: "amqp://guest:guest@localhost:5672/",
|
|
18
20
|
};
|
|
@@ -21,20 +23,26 @@ yarn add @palmetto/pubsub amqp-connection-manager amqplib zod
|
|
|
21
23
|
2. Create the connection
|
|
22
24
|
|
|
23
25
|
```ts
|
|
24
|
-
|
|
26
|
+
import { RabbitMqConnection } from "@palmetto/pubsub";
|
|
27
|
+
|
|
28
|
+
const connection = await RabbitMqConnection.create(config, console);
|
|
25
29
|
```
|
|
26
30
|
|
|
27
31
|
3. Create the publisher
|
|
28
32
|
|
|
29
33
|
```ts
|
|
34
|
+
import { Publisher, RabbitMqPublisher } from "@palmetto/pubsub";
|
|
35
|
+
|
|
30
36
|
const publisher = new Publisher(console, [
|
|
31
|
-
RabbitMqPublisher(connection, console),
|
|
37
|
+
new RabbitMqPublisher(connection, console),
|
|
32
38
|
]);
|
|
33
39
|
```
|
|
34
40
|
|
|
35
41
|
4. Create the subscriber
|
|
36
42
|
|
|
37
43
|
```ts
|
|
44
|
+
import { Subscriber, RabbitMqSubscriber } from "@palmetto/pubsub";
|
|
45
|
+
|
|
38
46
|
const subscriber = new Subscriber(console, [
|
|
39
47
|
new RabbitMqSubscriber(connection, console),
|
|
40
48
|
]);
|
|
@@ -43,6 +51,11 @@ yarn add @palmetto/pubsub amqp-connection-manager amqplib zod
|
|
|
43
51
|
5. Create the queue configuration
|
|
44
52
|
|
|
45
53
|
```ts
|
|
54
|
+
import { RabbitQueueExchangeConfiguration } from "@palmetto/pubsub";
|
|
55
|
+
import { z } from "zod";
|
|
56
|
+
|
|
57
|
+
const MyModelSchema = z.object({...});
|
|
58
|
+
|
|
46
59
|
const queue: RabbitQueueExchangeConfiguration = {
|
|
47
60
|
name: "my-queue-name",
|
|
48
61
|
schema: MyModelSchema,
|
|
@@ -101,7 +114,7 @@ Message failures in RabbitMq subscribers are handled using special queues and ex
|
|
|
101
114
|
|
|
102
115
|
These properties configure message retries:
|
|
103
116
|
|
|
104
|
-
- `retries` : specifies the number of retries after which the message is discarded. When this value is undefined, the message will retry
|
|
117
|
+
- `retries` : specifies the number of retries after which the message is discarded. When this value is undefined, the message will not retry.
|
|
105
118
|
- `retryDelay`: specifies the number of milliseconds between retries.
|
|
106
119
|
|
|
107
120
|
#### The retry queue
|