@palmetto/pubsub 3.0.2 → 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.
@@ -41,18 +41,19 @@ class BullMqPublisher {
41
41
  var _a, _b;
42
42
  const queue = yield this.getQueue(config);
43
43
  (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Publishing ${messages.length} messages to ${queue.name}`);
44
+ const opts = {
45
+ delay: (options === null || options === void 0 ? void 0 : options.scheduledAt)
46
+ ? Math.max(0, options.scheduledAt.getTime() - Date.now())
47
+ : undefined,
48
+ attempts: config.retries !== undefined ? config.retries + 1 : undefined,
49
+ backoff: {
50
+ type: "fixed",
51
+ delay: config.retryDelay || 30000,
52
+ },
53
+ };
44
54
  const job = {
45
55
  name: config.job || connection_js_1.BULLMQ_DEFAULTJOB,
46
- opts: {
47
- delay: (options === null || options === void 0 ? void 0 : options.scheduledAt)
48
- ? Math.max(0, options.scheduledAt.getTime() - Date.now())
49
- : undefined,
50
- attempts: config.retries,
51
- backoff: {
52
- type: "fixed",
53
- delay: config.retryDelay || 30000,
54
- },
55
- },
56
+ opts,
56
57
  };
57
58
  const jobs = messages.map((data) => {
58
59
  return Object.assign(Object.assign({}, job), { data });
@@ -9,9 +9,11 @@ declare class SubscribedMessage {
9
9
  stopSubscribe(): Promise<void>;
10
10
  }
11
11
  export declare class MessageFailError extends Error {
12
+ static readonly failedReason = "Handler asked to fail this job";
12
13
  constructor();
13
14
  }
14
15
  export declare class MessageRetryError extends Error {
16
+ static readonly failedReason = "Handler asked to retry this job";
15
17
  constructor();
16
18
  }
17
19
  export declare class BullMqSubscriber implements SubscriberProvider {
@@ -24,5 +26,6 @@ export declare class BullMqSubscriber implements SubscriberProvider {
24
26
  startSubscribe(config: BullMqQueueConfiguration, onMessage: (s: string, context: BullMqMessageContext) => Promise<MessageResult> | MessageResult): Promise<StopSubscribe>;
25
27
  close(): Promise<void>;
26
28
  enrichHandledMesssageLog(config: BullMqQueueConfiguration): Record<string, unknown>;
29
+ willRetryJobOnFailure(job: bullmq.Job<string>): boolean;
27
30
  }
28
31
  export {};
@@ -32,16 +32,18 @@ class SubscribedMessage {
32
32
  }
33
33
  class MessageFailError extends Error {
34
34
  constructor() {
35
- super("Handler asked to fail this job");
35
+ super(MessageFailError.failedReason);
36
36
  }
37
37
  }
38
38
  exports.MessageFailError = MessageFailError;
39
+ MessageFailError.failedReason = "Handler asked to fail this job";
39
40
  class MessageRetryError extends Error {
40
41
  constructor() {
41
- super("Handler asked to retry this job");
42
+ super(MessageRetryError.failedReason);
42
43
  }
43
44
  }
44
45
  exports.MessageRetryError = MessageRetryError;
46
+ MessageRetryError.failedReason = "Handler asked to retry this job";
45
47
  class BullMqSubscriber {
46
48
  constructor(connection, logger) {
47
49
  this.connection = connection;
@@ -70,9 +72,10 @@ class BullMqSubscriber {
70
72
  }, () => __awaiter(this, void 0, void 0, function* () {
71
73
  const context = {
72
74
  job: job.name,
73
- retries: job.attemptsMade,
75
+ attemptsMade: job.attemptsMade,
74
76
  firstSent: new Date(job.timestamp),
75
- // lastSent: new Date(job.attemptsStarted),
77
+ lastSent: undefined,
78
+ willRetryOnFailure: this.willRetryJobOnFailure(job),
76
79
  };
77
80
  const result = yield onMessage(job.data, context);
78
81
  if (result === interfaces_js_1.MessageResult.Ok) {
@@ -81,7 +84,8 @@ class BullMqSubscriber {
81
84
  if (result === interfaces_js_1.MessageResult.Retry) {
82
85
  throw new MessageRetryError();
83
86
  }
84
- throw new BullMqPackage.UnrecoverableError("Handler asked to fail this job");
87
+ // result === MessageResult.Fail
88
+ throw new BullMqPackage.UnrecoverableError(MessageFailError.failedReason);
85
89
  }));
86
90
  }), {
87
91
  connection: this.connection,
@@ -91,10 +95,30 @@ class BullMqSubscriber {
91
95
  if (!job) {
92
96
  return;
93
97
  }
94
- this.logger.log(`Job failed ${err}`);
98
+ if (job.failedReason !== MessageRetryError.failedReason &&
99
+ job.failedReason !== MessageFailError.failedReason) {
100
+ const log = {
101
+ message: "BullMq PubSub job failed",
102
+ error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err),
103
+ extra: {
104
+ job: config.name,
105
+ message: job.data,
106
+ attemptsMade: job.attemptsMade,
107
+ failedReason: job.failedReason,
108
+ },
109
+ };
110
+ this.logger.error(log);
111
+ }
95
112
  });
96
113
  worker.on("error", (err) => {
97
- this.logger.error(`BullMq PubSub handler exception: ${err}`);
114
+ const log = {
115
+ message: "BullMq PubSub handler exception",
116
+ error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err),
117
+ extra: {
118
+ job: config.name,
119
+ },
120
+ };
121
+ this.logger.error(log);
98
122
  });
99
123
  (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `BullMq PubSub subscriber starting for ${worker.name}`);
100
124
  worker
@@ -128,5 +152,9 @@ class BullMqSubscriber {
128
152
  job: config.job,
129
153
  };
130
154
  }
155
+ willRetryJobOnFailure(job) {
156
+ var _a;
157
+ return job.attemptsMade + 1 < ((_a = job.opts.attempts) !== null && _a !== void 0 ? _a : 1);
158
+ }
131
159
  }
132
160
  exports.BullMqSubscriber = BullMqSubscriber;
@@ -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,5 @@
1
+ export * from "./config.js";
2
+ export * from "./connection.js";
3
+ export * from "./publisher.js";
4
+ export * from "./subscriber.js";
5
+ export * from "./pubsub.js";
@@ -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;
@@ -52,7 +52,7 @@ export interface PubSubConfiguration {
52
52
  */
53
53
  transport: string;
54
54
  /**
55
- * Maximum number of times to retry a message
55
+ * Maximum number of times to retry a message (default: 0, no retries)
56
56
  */
57
57
  retries?: number;
58
58
  /**
@@ -129,7 +129,8 @@ export type PubSubProvider = PublisherProvider & SubscriberProvider;
129
129
  */
130
130
  export type PubOrSubProvider = PubSubProvider | PublisherProvider | SubscriberProvider;
131
131
  export interface MessageContext {
132
- firstSent?: Date;
133
- lastSent?: Date;
134
- retries?: number;
132
+ firstSent: Date | undefined;
133
+ lastSent: Date | undefined;
134
+ attemptsMade: number | undefined;
135
+ willRetryOnFailure: boolean;
135
136
  }
package/dist/main.d.ts CHANGED
@@ -4,3 +4,4 @@ export * from "./publisher.js";
4
4
  export * from "./subscriber.js";
5
5
  export * from "./bullmq/main.js";
6
6
  export * from "./rabbitmq/main.js";
7
+ export * from "./gcppubsub/main.js";
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);
@@ -84,8 +84,10 @@ export interface RabbitQueueExchangeCustomConfiguration {
84
84
  retryQueueExchangeName?: string;
85
85
  }
86
86
  export interface RabbitMqMessageContext extends MessageContext {
87
- exchangeName?: string;
88
- routingKey?: string;
87
+ queueName: string;
88
+ exchangeName: string;
89
+ routingKey: string;
90
+ retryExchangeName: string;
89
91
  }
90
92
  /**
91
93
  * Returns the queue name based on the configuration
@@ -25,7 +25,7 @@ export declare class RabbitMqSubscriber implements SubscriberProvider {
25
25
  startSubscribe(config: RabbitQueueExchangeConfiguration, onMessage: (s: string, context: RabbitMqMessageContext) => Promise<MessageResult> | MessageResult): StopSubscribe;
26
26
  close(): Promise<void>;
27
27
  enrichHandledMesssageLog(config: RabbitQueueExchangeConfiguration): Record<string, unknown>;
28
- static getRetries(msg: ConsumeMessage): number;
28
+ static getAttemptsMade(msg: ConsumeMessage): number;
29
29
  static getSentDates(msg: ConsumeMessage): {
30
30
  firstSent: Date | undefined;
31
31
  lastSent: Date | undefined;
@@ -52,7 +52,7 @@ class SubscribedMessage {
52
52
  }
53
53
  }
54
54
  const RETRIES_HEADER = "x-retries";
55
- const RETRYSENT_HEADER = "x-retry-sent";
55
+ const RETRY_SENT_HEADER = "x-retry-sent";
56
56
  class RabbitMqSubscriber {
57
57
  constructor(connection, logger) {
58
58
  this.connection = connection;
@@ -94,9 +94,10 @@ class RabbitMqSubscriber {
94
94
  enrichHandledMesssageLog(config) {
95
95
  return {
96
96
  queueName: (0, config_js_1.getQueueName)(config),
97
+ exchangeName: (0, config_js_1.getExchangeName)(config),
97
98
  };
98
99
  }
99
- static getRetries(msg) {
100
+ static getAttemptsMade(msg) {
100
101
  var _a;
101
102
  return typeof ((_a = msg.properties.headers) === null || _a === void 0 ? void 0 : _a[RETRIES_HEADER]) === "number"
102
103
  ? msg.properties.headers[RETRIES_HEADER]
@@ -109,8 +110,8 @@ class RabbitMqSubscriber {
109
110
  const firstSent = typeof msg.properties.timestamp === "number"
110
111
  ? new Date(msg.properties.timestamp)
111
112
  : undefined;
112
- const lastSent = typeof ((_a = msg.properties.headers) === null || _a === void 0 ? void 0 : _a[RETRYSENT_HEADER]) === "number"
113
- ? new Date(msg.properties.headers[RETRYSENT_HEADER])
113
+ const lastSent = typeof ((_a = msg.properties.headers) === null || _a === void 0 ? void 0 : _a[RETRY_SENT_HEADER]) === "number"
114
+ ? new Date(msg.properties.headers[RETRY_SENT_HEADER])
114
115
  : firstSent;
115
116
  return { firstSent, lastSent };
116
117
  }
@@ -129,22 +130,39 @@ class RabbitMqSubscriber {
129
130
  }, (span) => __awaiter(this, void 0, void 0, function* () {
130
131
  try {
131
132
  subscribedMessage.busy++;
132
- yield this.consumeMessage(msg, channel, subscribedMessage, setupResult.retryExchangeName, retryDelay);
133
+ yield this.consumeMessage({
134
+ msg,
135
+ channel,
136
+ subscribedMessage,
137
+ retryExchangeName: setupResult.retryExchangeName,
138
+ retryDelay,
139
+ queueName,
140
+ });
133
141
  }
134
142
  catch (err) {
135
- this.logger.error({
143
+ const logPayload = {
136
144
  message: "Unexpected error handling RabbitMq message",
137
145
  error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err),
138
- });
146
+ extra: {
147
+ queueName,
148
+ message: msg.content.toString("utf8"),
149
+ },
150
+ };
151
+ this.logger.error(logPayload);
139
152
  span === null || span === void 0 ? void 0 : span.setTag("error", err);
140
153
  try {
141
154
  channel.nack(msg, undefined, true);
142
155
  }
143
156
  catch (err2) {
144
- this.logger.error({
157
+ const logPayload2 = {
145
158
  message: "Unexpected error handling RabbitMq message during NACK attempt",
146
159
  error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err2),
147
- });
160
+ extra: {
161
+ queueName,
162
+ message: msg.content.toString("utf8"),
163
+ },
164
+ };
165
+ this.logger.error(logPayload2);
148
166
  }
149
167
  }
150
168
  finally {
@@ -171,53 +189,68 @@ class RabbitMqSubscriber {
171
189
  });
172
190
  });
173
191
  }
174
- consumeMessage(msg, channel, subscribedMessage, retryExchangeName, retryDelay) {
175
- return __awaiter(this, void 0, void 0, function* () {
176
- var _a, _b, _c, _d, _e, _f, _g, _h;
192
+ consumeMessage(_a) {
193
+ return __awaiter(this, arguments, void 0, function* ({ msg, channel, subscribedMessage, retryExchangeName, retryDelay, queueName, }) {
194
+ var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
177
195
  const json = msg.content.toString("utf8");
178
- const retries = RabbitMqSubscriber.getRetries(msg);
196
+ const attemptsMade = RabbitMqSubscriber.getAttemptsMade(msg);
179
197
  const sentDates = RabbitMqSubscriber.getSentDates(msg);
180
- const context = Object.assign(Object.assign({ retries }, sentDates), { exchangeName: msg.fields.exchange, routingKey: msg.fields.routingKey });
198
+ const willRetryOnFailure = attemptsMade < ((_b = subscribedMessage.config.retries) !== null && _b !== void 0 ? _b : 0);
199
+ const context = Object.assign(Object.assign({ attemptsMade }, sentDates), { queueName, exchangeName: msg.fields.exchange, retryExchangeName, routingKey: msg.fields.routingKey, willRetryOnFailure });
181
200
  let messageResult;
201
+ let logPayload;
182
202
  try {
183
203
  messageResult = yield subscribedMessage.onMessage(json, context);
204
+ logPayload = {
205
+ message: `RabbitMq consumer handled message.`,
206
+ extra: Object.assign({}, context),
207
+ };
184
208
  }
185
209
  catch (err) {
186
210
  messageResult = interfaces_js_1.MessageResult.Retry;
187
- this.logger.error({
188
- message: `RabbitMq consumer unhandled exception, retrying message to ${retryExchangeName} ${msg.fields.routingKey}`,
211
+ logPayload = {
212
+ message: `RabbitMq consumer unhandled exception.`,
189
213
  error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err),
190
- });
214
+ extra: Object.assign({}, context),
215
+ };
191
216
  }
192
217
  if (messageResult === interfaces_js_1.MessageResult.Ok) {
218
+ (_d = (_c = this.logger).debug) === null || _d === void 0 ? void 0 : _d.call(_c, logPayload);
193
219
  channel.ack(msg);
194
220
  return;
195
221
  }
196
- if (messageResult === interfaces_js_1.MessageResult.Retry &&
197
- subscribedMessage.config.retries &&
198
- retries >= subscribedMessage.config.retries) {
199
- messageResult = interfaces_js_1.MessageResult.Fail;
222
+ if (messageResult === interfaces_js_1.MessageResult.Retry) {
223
+ if (willRetryOnFailure) {
224
+ logPayload.message += " Retrying message.";
225
+ }
226
+ else {
227
+ messageResult = interfaces_js_1.MessageResult.Fail;
228
+ logPayload.message += " No more retries.";
229
+ }
200
230
  }
201
231
  if (messageResult === interfaces_js_1.MessageResult.Fail) {
202
- (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `RabbitMq retry count exceeded - sending message to DLQ`);
232
+ (_f = (_e = this.logger).debug) === null || _f === void 0 ? void 0 : _f.call(_e, logPayload);
203
233
  channel.nack(msg, undefined, false);
204
234
  return;
205
235
  }
206
- const ok = yield ((_c = subscribedMessage.channel) === null || _c === void 0 ? void 0 : _c.publish(retryExchangeName, msg.fields.routingKey, msg.content, {
236
+ const ok = yield ((_g = subscribedMessage.channel) === null || _g === void 0 ? void 0 : _g.publish(retryExchangeName, msg.fields.routingKey, msg.content, {
207
237
  contentType: msg.properties.contentType,
208
238
  expiration: retryDelay,
209
- timestamp: (_d = sentDates.firstSent) === null || _d === void 0 ? void 0 : _d.valueOf(),
239
+ timestamp: (_h = sentDates.firstSent) === null || _h === void 0 ? void 0 : _h.valueOf(),
210
240
  headers: {
211
- [RETRIES_HEADER]: retries + 1,
212
- [RETRYSENT_HEADER]: Date.now(),
241
+ [RETRIES_HEADER]: attemptsMade + 1,
242
+ [RETRY_SENT_HEADER]: Date.now(),
213
243
  },
214
244
  }));
215
245
  if (ok) {
216
- (_f = (_e = this.logger).debug) === null || _f === void 0 ? void 0 : _f.call(_e, `RabbitMq retry queue success`);
246
+ (_k = (_j = this.logger).debug) === null || _k === void 0 ? void 0 : _k.call(_j, `RabbitMq retry queue success`);
247
+ (_m = (_l = this.logger).debug) === null || _m === void 0 ? void 0 : _m.call(_l, logPayload);
217
248
  channel.ack(msg);
218
249
  }
219
250
  else {
220
- (_h = (_g = this.logger).debug) === null || _h === void 0 ? void 0 : _h.call(_g, `RabbitMq retry queue failure - nack-ing message instead`);
251
+ (_p = (_o = this.logger).debug) === null || _p === void 0 ? void 0 : _p.call(_o, `RabbitMq retry queue failure - nack-ing message instead`);
252
+ logPayload.message += " Retry publish failed.";
253
+ this.logger.error(logPayload);
221
254
  channel.nack(msg, undefined, true);
222
255
  }
223
256
  });
@@ -55,18 +55,20 @@ class Subscriber {
55
55
  if (!provider) {
56
56
  throw new errors_js_1.MissingPubSubProviderError(`No provider configured for ${config.transport}`);
57
57
  }
58
- const transform = (jsonStr, context) => __awaiter(this, void 0, void 0, function* () {
58
+ const enrichedConfig = provider.enrichHandledMesssageLog(config);
59
+ const handleMessage = (jsonStr, context) => __awaiter(this, void 0, void 0, function* () {
59
60
  const start = (0, message_logger_js_1.startTiming)();
60
61
  const jsonObject = JSON.parse(jsonStr);
61
- const r = schema.safeDecode(jsonObject);
62
- if (!r.success) {
62
+ const decodeResult = schema.safeDecode(jsonObject);
63
+ if (!decodeResult.success) {
63
64
  const durationMs = (0, message_logger_js_1.getDuration)(start);
64
65
  (0, message_logger_js_1.logMessage)({
65
66
  note: "Subscribed message failed schema validation",
66
67
  message: jsonObject,
67
68
  level: "error",
68
69
  logger: this.logger,
69
- extra: Object.assign({ transport: provider.transport, name: config.name, durationMs }, provider.enrichHandledMesssageLog(config)),
70
+ extra: Object.assign({ transport: provider.transport, name: config.name, durationMs,
71
+ context }, enrichedConfig),
70
72
  });
71
73
  const handledEventContext = {
72
74
  message: jsonStr,
@@ -80,7 +82,7 @@ class Subscriber {
80
82
  return interfaces_js_1.MessageResult.Fail;
81
83
  }
82
84
  try {
83
- const result = yield onMessage(r.data, context);
85
+ const result = yield onMessage(decodeResult.data, context);
84
86
  const durationMs = (0, message_logger_js_1.getDuration)(start);
85
87
  const eventContext = {
86
88
  message: jsonStr,
@@ -91,11 +93,12 @@ class Subscriber {
91
93
  };
92
94
  (0, message_logger_js_1.logMessage)({
93
95
  note: "Subscriber processed message",
94
- message: r.data,
96
+ message: decodeResult.data,
95
97
  level: config.messageLogLevel,
96
98
  logger: this.logger,
97
99
  extra: Object.assign({ transport: provider.transport, name: config.name, durationMs,
98
- result }, provider.enrichHandledMesssageLog(config)),
100
+ result,
101
+ context }, enrichedConfig),
99
102
  });
100
103
  this.events.emit("messageHandled", eventContext);
101
104
  return result;
@@ -104,24 +107,31 @@ class Subscriber {
104
107
  const durationMs = (0, message_logger_js_1.getDuration)(start);
105
108
  (0, message_logger_js_1.logMessage)({
106
109
  note: "Subscriber error when processing message",
107
- message: r.data,
108
- level: "error",
110
+ message: decodeResult.data,
111
+ level: context.willRetryOnFailure ? "warn" : "error",
109
112
  logger: this.logger,
110
- extra: Object.assign(Object.assign({ transport: provider.transport, name: config.name, durationMs }, provider.enrichHandledMesssageLog(config)), { error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err) }),
113
+ extra: Object.assign(Object.assign({ transport: provider.transport, name: config.name, durationMs,
114
+ context }, enrichedConfig), { error: (0, create_log_error_payload_js_1.createLogErrorPayload)(err) }),
111
115
  });
116
+ // in the messageHandled event, we always log as retry or fail based on willRetryOnFailure
117
+ const result = context.willRetryOnFailure
118
+ ? interfaces_js_1.MessageResult.Retry
119
+ : interfaces_js_1.MessageResult.Fail;
112
120
  const eventContext = {
113
121
  message: jsonStr,
122
+ result,
114
123
  context,
115
124
  config,
116
125
  durationMs,
117
126
  err,
118
127
  };
119
128
  this.events.emit("messageHandled", eventContext);
120
- throw err;
129
+ // but, always return Retry on exception to allow for retry logic to work in the providers
130
+ return interfaces_js_1.MessageResult.Retry;
121
131
  }
122
132
  });
123
133
  this.logger.log(`Starting subscriber for ${config.transport}:${config.name}`);
124
- subscribedMessage = new SubscribedMessage(yield provider.startSubscribe(config, transform));
134
+ subscribedMessage = new SubscribedMessage(yield provider.startSubscribe(config, handleMessage));
125
135
  this.subscribedMessages.set(config, subscribedMessage);
126
136
  this.logger.log(`Started subscriber for ${config.transport}:${config.name}`);
127
137
  return () => __awaiter(this, void 0, void 0, function* () {
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@palmetto/pubsub",
3
- "version": "3.0.2",
3
+ "version": "3.2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/palmetto/galaxy"
7
+ },
4
8
  "main": "./dist/main.js",
5
9
  "scripts": {
6
10
  "lint": "yarn run -T eslint --fix ./src",
@@ -20,6 +24,7 @@
20
24
  "test:watch": "yarn run test-runner vitest watch"
21
25
  },
22
26
  "devDependencies": {
27
+ "@google-cloud/pubsub": "^5.2.3",
23
28
  "@palmetto/trace": "^0.1.0",
24
29
  "@types/amqplib": "^0",
25
30
  "@types/node": "^24.2.1",
@@ -45,10 +50,11 @@
45
50
  "uuid": "^11.1.0"
46
51
  },
47
52
  "peerDependencies": {
53
+ "@google-cloud/pubsub": "^5.2",
48
54
  "@palmetto/trace": "^0.1.0",
49
55
  "amqp-connection-manager": "^4.1.14",
50
56
  "amqplib": "^0.10.8",
51
57
  "bullmq": "^5.58.0",
52
- "zod": "^4.1.13"
58
+ "zod": "^4.1"
53
59
  }
54
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.
@@ -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
- const connection = new RabbitMqConnection(config, console);
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 forever.
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