@palmetto/pubsub 2.1.2 → 2.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/README.md CHANGED
@@ -61,6 +61,10 @@ yarn add @palmetto/pubsub zod
61
61
  };
62
62
 
63
63
  await publisher.publish(schemaConfig, message);
64
+
65
+ // publish a batch of messages quicker than sending the one at a time:
66
+ const messages: Model[] = [...];
67
+ await publisher.publish(schemaConfig, messages);
64
68
  ```
65
69
 
66
70
  1. Create subscriber and subscriber to a message
@@ -108,6 +112,15 @@ yarn add @palmetto/pubsub zod
108
112
  await subscriber.startSubscribe(schemaConfig, onMessage);
109
113
  ```
110
114
 
115
+ 1. Register with dd-trace for improved trace logs
116
+
117
+ ```ts
118
+ import tracer from "dd-trace";
119
+ import { registerDdTrace } from "@palmetto/pubsub";
120
+
121
+ registerDdTrace(tracer);
122
+ ```
123
+
111
124
  ### Message logging
112
125
 
113
126
  For each message you can indicate how to log messages when they are published or handled by a subscriber. Use the `messageLogLevel` property of the queue configuration object.
@@ -8,7 +8,7 @@ export declare class BullMqPublisher implements PublisherProvider {
8
8
  constructor(connection: bullmq.ConnectionOptions, logger: Logger);
9
9
  readonly transport: string;
10
10
  private getQueue;
11
- publish(config: BullMqQueueConfiguration, message: string): Promise<void>;
11
+ publish(config: BullMqQueueConfiguration, messages: string[]): Promise<void>;
12
12
  init(config: BullMqQueueConfiguration): Promise<void>;
13
13
  close(): Promise<void>;
14
14
  enrichPublishedMesssageLog(config: BullMqQueueConfiguration): Record<string, unknown>;
@@ -36,18 +36,20 @@ class BullMqPublisher {
36
36
  return queue;
37
37
  });
38
38
  }
39
- publish(config, message) {
39
+ publish(config, messages) {
40
40
  return __awaiter(this, void 0, void 0, function* () {
41
- var _a, _b;
42
41
  const queue = yield this.getQueue(config);
43
- (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Publishing message to ${queue.name} - ${message}`);
44
- yield queue.add(config.job || connection_js_1.BULLMQ_DEFAULTJOB, message, {
45
- attempts: config.retries,
46
- backoff: {
47
- type: "fixed",
48
- delay: config.retryDelay || 30000,
49
- },
50
- });
42
+ yield Promise.all(messages.map((message) => __awaiter(this, void 0, void 0, function* () {
43
+ var _a, _b;
44
+ (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Publishing message to ${queue.name} - ${message}`);
45
+ yield queue.add(config.job || connection_js_1.BULLMQ_DEFAULTJOB, message, {
46
+ attempts: config.retries,
47
+ backoff: {
48
+ type: "fixed",
49
+ delay: config.retryDelay || 30000,
50
+ },
51
+ });
52
+ })));
51
53
  });
52
54
  }
53
55
  init(config) {
@@ -6,7 +6,7 @@ export declare class BullMqPubSubProvider implements PubSubProvider {
6
6
  private readonly subscriber;
7
7
  constructor(connection: ConnectionOptions, logger: Logger);
8
8
  readonly transport: string;
9
- publish(config: BullMqQueueConfiguration, message: string): Promise<void>;
9
+ publish(config: BullMqQueueConfiguration, messages: string[]): Promise<void>;
10
10
  startSubscribe(config: BullMqQueueConfiguration, onMessage: (s: string, context: MessageContext) => Promise<MessageResult> | MessageResult): Promise<StopSubscribe>;
11
11
  close(): Promise<void>;
12
12
  init(config: BullMqQueueConfiguration): Promise<void>;
@@ -19,8 +19,8 @@ class BullMqPubSubProvider {
19
19
  this.publisher = new publisher_js_1.BullMqPublisher(connection, logger);
20
20
  this.subscriber = new subscriber_js_1.BullMqSubscriber(connection, logger);
21
21
  }
22
- publish(config, message) {
23
- return this.publisher.publish(config, message);
22
+ publish(config, messages) {
23
+ return this.publisher.publish(config, messages);
24
24
  }
25
25
  startSubscribe(config, onMessage) {
26
26
  return this.subscriber.startSubscribe(config, onMessage);
@@ -0,0 +1,10 @@
1
+ interface Span {
2
+ finish(): void;
3
+ setTag(key: string, value: unknown): this;
4
+ }
5
+ interface Tracer {
6
+ startSpan(name: string): Span;
7
+ }
8
+ export declare function getTracer(): Tracer;
9
+ export declare function registerDdTrace(tracer: Tracer): void;
10
+ export {};
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTracer = getTracer;
4
+ exports.registerDdTrace = registerDdTrace;
5
+ class NoOpSpan {
6
+ finish() {
7
+ // no-op
8
+ }
9
+ setTag(_key, _value) {
10
+ return this;
11
+ }
12
+ }
13
+ class NoOpTracer {
14
+ constructor() {
15
+ this.span = new NoOpSpan();
16
+ }
17
+ startSpan(_name) {
18
+ return this.span;
19
+ }
20
+ }
21
+ let tracerInstance = new NoOpTracer();
22
+ function getTracer() {
23
+ return tracerInstance;
24
+ }
25
+ function registerDdTrace(tracer) {
26
+ tracerInstance = tracer;
27
+ }
package/dist/errors.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare class MissingPubSubProviderError extends Error {
10
10
  static isMissingPubSubProviderError(err: unknown): err is MissingPubSubProviderError;
11
11
  }
12
12
  export declare class PublishError extends Error {
13
+ readonly failedMessages: string[];
14
+ constructor(message: string, failedMessages: string[]);
13
15
  static isPublishError(err: unknown): err is PublishError;
14
16
  }
15
17
  export declare class SchemaValidationError extends Error {
package/dist/errors.js CHANGED
@@ -24,6 +24,10 @@ class MissingPubSubProviderError extends Error {
24
24
  }
25
25
  exports.MissingPubSubProviderError = MissingPubSubProviderError;
26
26
  class PublishError extends Error {
27
+ constructor(message, failedMessages) {
28
+ super(message);
29
+ this.failedMessages = failedMessages;
30
+ }
27
31
  static isPublishError(err) {
28
32
  return err instanceof PublishError;
29
33
  }
@@ -23,7 +23,7 @@ export type BaseMessage = z.infer<typeof IdMetaSchema>;
23
23
  export interface PublisherProvider {
24
24
  transport: string;
25
25
  init(config: PubSubConfiguration): Promise<void> | void;
26
- publish(config: PubSubConfiguration, message: string): Promise<void> | void;
26
+ publish(config: PubSubConfiguration, messages: string[]): Promise<void> | void;
27
27
  close(): Promise<void> | void;
28
28
  enrichPublishedMesssageLog(config: PubSubConfiguration): Record<string, unknown>;
29
29
  }
package/dist/main.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./dd-trace.wrapper.js";
1
2
  export * from "./errors.js";
2
3
  export * from "./interfaces.js";
3
4
  export * from "./publisher.js";
package/dist/main.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./dd-trace.wrapper.js"), exports);
17
18
  __exportStar(require("./errors.js"), exports);
18
19
  __exportStar(require("./interfaces.js"), exports);
19
20
  __exportStar(require("./publisher.js"), exports);
@@ -7,7 +7,7 @@ export declare class Publisher {
7
7
  addProvider(provider: PublisherProvider): void;
8
8
  removeProvider(providerOrTransport: PublisherProvider | string): boolean;
9
9
  init(config: PubSubConfiguration): Promise<void>;
10
- publish(config: PubSubConfiguration, message: BaseMessage): Promise<void>;
10
+ publish(config: PubSubConfiguration, message: BaseMessage | BaseMessage[]): Promise<void>;
11
11
  close(): Promise<void>;
12
12
  private getProvider;
13
13
  private assertSchema;
package/dist/publisher.js CHANGED
@@ -15,6 +15,7 @@ const uuid_1 = require("uuid");
15
15
  const crypto_1 = require("crypto");
16
16
  const errors_js_1 = require("./errors.js");
17
17
  const message_logger_js_1 = require("./message-logger.js");
18
+ const dd_trace_wrapper_js_1 = require("./dd-trace.wrapper.js");
18
19
  class Publisher {
19
20
  constructor(logger, providers) {
20
21
  var _a, _b;
@@ -53,43 +54,67 @@ class Publisher {
53
54
  }
54
55
  publish(config, message) {
55
56
  return __awaiter(this, void 0, void 0, function* () {
56
- const provider = this.getProvider(config);
57
- if (!message.id) {
58
- message.id = (0, uuid_1.v4)();
57
+ const span = (0, dd_trace_wrapper_js_1.getTracer)()
58
+ .startSpan("Publisher.publish")
59
+ .setTag("pubsub.transport", config.transport)
60
+ .setTag("pubsub.name", config.name);
61
+ try {
62
+ const provider = this.getProvider(config);
63
+ const { schema, schemaId } = this.assertSchema(config);
64
+ if (!Array.isArray(message)) {
65
+ message = [message];
66
+ }
67
+ const jsons = message.map((msg) => {
68
+ if (!msg.id) {
69
+ msg.id = (0, uuid_1.v4)();
70
+ }
71
+ if (!msg.meta) {
72
+ msg.meta = {
73
+ createdAt: new Date().toISOString(),
74
+ publishedBy: "",
75
+ schemaId: schemaId,
76
+ };
77
+ }
78
+ else {
79
+ msg.meta.schemaId = schemaId;
80
+ }
81
+ const check = schema.safeEncode(msg);
82
+ if (!check.success) {
83
+ (0, message_logger_js_1.logMessage)({
84
+ note: "Publish message failed schema validation",
85
+ message: message,
86
+ level: "error",
87
+ logger: this.logger,
88
+ extra: Object.assign({ transport: provider.transport, name: config.name }, provider.enrichPublishedMesssageLog(config)),
89
+ });
90
+ throw new errors_js_1.SchemaValidationError(`Schema did not accept the published message: ${check.error.message}`);
91
+ }
92
+ const json = JSON.stringify(check.data);
93
+ return { json, data: check.data };
94
+ });
95
+ const start = (0, message_logger_js_1.startTiming)();
96
+ yield provider.publish(config, jsons.map((j) => j.json));
97
+ const duration = (0, message_logger_js_1.getDuration)(start);
98
+ jsons.forEach((msg) => {
99
+ (0, message_logger_js_1.logMessage)({
100
+ note: "Published message",
101
+ message: msg.data,
102
+ level: config.messageLogLevel,
103
+ logger: this.logger,
104
+ extra: Object.assign({ transport: provider.transport, name: config.name, durationMs: duration }, provider.enrichPublishedMesssageLog(config)),
105
+ });
106
+ });
107
+ span.setTag("pubsub.duration_ms", duration);
108
+ span.setTag("pubsub.message_count", message.length);
109
+ span.setTag("pubsub.success", true);
59
110
  }
60
- const { schema, schemaId } = this.assertSchema(config);
61
- if (!message.meta) {
62
- message.meta = {
63
- createdAt: new Date().toISOString(),
64
- publishedBy: "",
65
- schemaId: schemaId,
66
- };
111
+ catch (err) {
112
+ span.setTag("pubsub.success", false);
113
+ throw err;
67
114
  }
68
- else {
69
- message.meta.schemaId = schemaId;
115
+ finally {
116
+ span.finish();
70
117
  }
71
- const check = schema.safeEncode(message);
72
- if (!check.success) {
73
- (0, message_logger_js_1.logMessage)({
74
- note: "Publish message failed schema validation",
75
- message: message,
76
- level: "error",
77
- logger: this.logger,
78
- extra: Object.assign({ transport: provider.transport, name: config.name }, provider.enrichPublishedMesssageLog(config)),
79
- });
80
- throw new errors_js_1.SchemaValidationError(`Schema did not accept the published message: ${check.error.message}`);
81
- }
82
- const json = JSON.stringify(check.data);
83
- const start = (0, message_logger_js_1.startTiming)();
84
- yield provider.publish(config, json);
85
- const duration = (0, message_logger_js_1.getDuration)(start);
86
- (0, message_logger_js_1.logMessage)({
87
- note: "Published message",
88
- message: check.data,
89
- level: config.messageLogLevel,
90
- logger: this.logger,
91
- extra: Object.assign({ transport: provider.transport, name: config.name, durationMs: duration }, provider.enrichPublishedMesssageLog(config)),
92
- });
93
118
  });
94
119
  }
95
120
  close() {
@@ -108,10 +133,12 @@ class Publisher {
108
133
  return provider;
109
134
  }
110
135
  assertSchema(config) {
136
+ var _a, _b;
111
137
  const { schema } = config;
112
138
  let hash = this.hashes.get(schema);
113
139
  if (!hash) {
114
- const jsonSchema = JSON.stringify(zod_1.z.toJSONSchema(schema, { io: "input" }), null, 3);
140
+ (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Calculating schema id for ${config.name}`);
141
+ const jsonSchema = JSON.stringify(zod_1.z.toJSONSchema(schema, { io: "input" }), null, 2);
115
142
  hash = (0, crypto_1.createHash)("sha256").update(jsonSchema).digest("hex");
116
143
  this.hashes.set(schema, hash);
117
144
  }
@@ -21,7 +21,7 @@ export interface RabbitMqConnectionConfig {
21
21
  }
22
22
  export type QueueType = "default" | "dead-letter" | "retry";
23
23
  export type ExchangeType = "direct" | "topic" | "fanout";
24
- export interface RabbitQueueExchangeConfiguration extends PubSubConfiguration {
24
+ export interface RabbitQueueExchangeConfigurationBase extends PubSubConfiguration {
25
25
  /**
26
26
  * Transport is always RABBITMQ_TRANSPORT
27
27
  */
@@ -30,14 +30,6 @@ export interface RabbitQueueExchangeConfiguration extends PubSubConfiguration {
30
30
  * The queue name prefix
31
31
  */
32
32
  name: string;
33
- /**
34
- * For topic exchanges, the name of the subscriber is used here
35
- */
36
- topicSubscriberName?: string;
37
- /**
38
- * Support direct exchange where each message is delivered once, or topic/fanout exchange where each message is delivered to multiple queues
39
- */
40
- exchangeType: ExchangeType;
41
33
  /**
42
34
  * When true, the queue and exchange will be deleted after the consumers exit [note: dead-letter exchanges & queues may remain when there are no dead-letter messages or consumers]
43
35
  */
@@ -61,6 +53,23 @@ export interface RabbitQueueExchangeConfiguration extends PubSubConfiguration {
61
53
  */
62
54
  publishToSpecificQueue?: string;
63
55
  }
56
+ export interface RabbitQueueExchangeConfigurationTopicExchange extends RabbitQueueExchangeConfigurationBase {
57
+ /**
58
+ * For topic exchanges, the name of the subscriber is used here
59
+ */
60
+ topicSubscriberName: string;
61
+ /**
62
+ * Support topic/fanout exchange where each message is delivered to multiple queues
63
+ */
64
+ exchangeType: "topic" | "fanout";
65
+ }
66
+ export interface RabbitQueueExchangeConfigurationDirectExchange extends RabbitQueueExchangeConfigurationBase {
67
+ /**
68
+ * Support direct exchange where each message is delivered once
69
+ */
70
+ exchangeType: "direct";
71
+ }
72
+ export type RabbitQueueExchangeConfiguration = RabbitQueueExchangeConfigurationDirectExchange | RabbitQueueExchangeConfigurationTopicExchange;
64
73
  export declare function isRabbitQueueExchangeConfiguration(config: PubSubConfiguration): config is RabbitQueueExchangeConfiguration;
65
74
  export interface RabbitQueueExchangeNames {
66
75
  queueName?: string;
@@ -24,9 +24,10 @@ export declare class RabbitMqPublisher implements PublisherProvider {
24
24
  * @param message The JSON message to send
25
25
  * @returns A promise that is completed when the message is published
26
26
  */
27
- publish(config: RabbitQueueExchangeConfiguration, message: string): Promise<void>;
27
+ publish(config: RabbitQueueExchangeConfiguration, messages: string[]): Promise<void>;
28
28
  enrichPublishedMesssageLog(config: RabbitQueueExchangeConfiguration): Record<string, unknown>;
29
29
  private publishToExchange;
30
30
  private publishToQueue;
31
+ private sendMessages;
31
32
  close(): Promise<void> | undefined;
32
33
  }
@@ -78,14 +78,14 @@ class RabbitMqPublisher {
78
78
  * @param message The JSON message to send
79
79
  * @returns A promise that is completed when the message is published
80
80
  */
81
- publish(config, message) {
81
+ publish(config, messages) {
82
82
  return __awaiter(this, void 0, void 0, function* () {
83
83
  const channel = yield this.getChannel(config);
84
84
  if (config.publishToSpecificQueue) {
85
- yield this.publishToQueue(channel, config.publishToSpecificQueue, message);
85
+ yield this.publishToQueue(channel, config.publishToSpecificQueue, messages);
86
86
  }
87
87
  else {
88
- yield this.publishToExchange(channel, config, message);
88
+ yield this.publishToExchange(channel, config, messages);
89
89
  }
90
90
  });
91
91
  }
@@ -100,28 +100,31 @@ class RabbitMqPublisher {
100
100
  routingKey: (0, config_js_1.getRoutingKey)(config),
101
101
  };
102
102
  }
103
- publishToExchange(channel, config, message) {
103
+ publishToExchange(channel, config, messages) {
104
104
  return __awaiter(this, void 0, void 0, function* () {
105
105
  const exchangeName = (0, config_js_1.getExchangeName)(config);
106
- const ok = yield channel.publish(exchangeName, (0, config_js_1.getRoutingKey)(config), Buffer.from(message, "utf8"), {
107
- contentType: "application/json",
108
- timestamp: new Date().valueOf(),
109
- persistent: true,
110
- });
111
- if (!ok) {
112
- throw new errors_js_1.PublishError(`RabbitMq publish to ${exchangeName} failed`);
113
- }
106
+ const routingKey = (0, config_js_1.getRoutingKey)(config);
107
+ return this.sendMessages(messages, (buf, options) => channel.publish(exchangeName, routingKey, buf, options), "publish to exchange", exchangeName);
114
108
  });
115
109
  }
116
- publishToQueue(channel, queue, message) {
110
+ publishToQueue(channel, queue, messages) {
111
+ return this.sendMessages(messages, (buf, options) => channel.sendToQueue(queue, buf, options), "send to queue", queue);
112
+ }
113
+ sendMessages(messages, sender, action, name) {
117
114
  return __awaiter(this, void 0, void 0, function* () {
118
- const ok = yield channel.sendToQueue(queue, Buffer.from(message, "utf8"), {
115
+ const options = {
119
116
  contentType: "application/json",
120
117
  timestamp: new Date().valueOf(),
121
118
  persistent: true,
122
- });
123
- if (!ok) {
124
- throw new errors_js_1.PublishError(`RabbitMq publish to queue:${queue} failed`);
119
+ };
120
+ const oks = yield Promise.all(messages.map((message) => sender(Buffer.from(message, "utf8"), options)));
121
+ const failedMessages = oks
122
+ .map((ok, index) => ({ ok, index }))
123
+ .filter(({ ok }) => !ok)
124
+ .map(({ index }) => messages[index]);
125
+ if (failedMessages.length > 0) {
126
+ const allOrSome = failedMessages.length === messages.length ? "all" : "some";
127
+ throw new errors_js_1.PublishError(`RabbitMq ${action} ${name} failed for ${allOrSome} messages`, failedMessages);
125
128
  }
126
129
  });
127
130
  }
@@ -9,7 +9,7 @@ export declare class RabbitMqPubSubProvider implements PubSubProvider {
9
9
  private readonly subscriber;
10
10
  constructor(connection: RabbitMqConnection, logger: Logger);
11
11
  readonly transport: string;
12
- publish(config: RabbitQueueExchangeConfiguration, message: string): Promise<void>;
12
+ publish(config: RabbitQueueExchangeConfiguration, messages: string[]): Promise<void>;
13
13
  startSubscribe(config: RabbitQueueExchangeConfiguration, onMessage: (s: string, context: MessageContext) => Promise<MessageResult> | MessageResult): StopSubscribe;
14
14
  close(): Promise<void>;
15
15
  init(config: RabbitQueueExchangeConfiguration): Promise<void>;
@@ -22,8 +22,8 @@ class RabbitMqPubSubProvider {
22
22
  this.publisher = new publisher_js_1.RabbitMqPublisher(connection, logger);
23
23
  this.subscriber = new subscriber_js_1.RabbitMqSubscriber(connection, logger);
24
24
  }
25
- publish(config, message) {
26
- return this.publisher.publish(config, message);
25
+ publish(config, messages) {
26
+ return this.publisher.publish(config, messages);
27
27
  }
28
28
  startSubscribe(config, onMessage) {
29
29
  return this.subscriber.startSubscribe(config, onMessage);
@@ -15,6 +15,7 @@ const errors_js_1 = require("./errors.js");
15
15
  const events_1 = require("events");
16
16
  const message_logger_js_1 = require("./message-logger.js");
17
17
  const create_log_error_payload_js_1 = require("./create-log-error-payload.js");
18
+ const dd_trace_wrapper_js_1 = require("./dd-trace.wrapper.js");
18
19
  class SubscribedMessage {
19
20
  constructor(stop) {
20
21
  this.stop = stop;
@@ -56,6 +57,10 @@ class Subscriber {
56
57
  throw new errors_js_1.MissingPubSubProviderError(`No provider configured for ${config.transport}`);
57
58
  }
58
59
  const transform = (jsonStr, context) => __awaiter(this, void 0, void 0, function* () {
60
+ const span = (0, dd_trace_wrapper_js_1.getTracer)()
61
+ .startSpan("Subscriber.handleMessage")
62
+ .setTag("pubsub.transport", config.transport)
63
+ .setTag("pubsub.name", config.name);
59
64
  const start = (0, message_logger_js_1.startTiming)();
60
65
  const jsonObject = JSON.parse(jsonStr);
61
66
  const r = schema.safeDecode(jsonObject);
@@ -77,6 +82,8 @@ class Subscriber {
77
82
  };
78
83
  this.events.emit("schemaError", handledEventContext);
79
84
  this.events.emit("messageHandled", handledEventContext);
85
+ span.setTag("pubsub.success", false);
86
+ span.finish();
80
87
  return interfaces_js_1.MessageResult.Fail;
81
88
  }
82
89
  try {
@@ -98,6 +105,9 @@ class Subscriber {
98
105
  result }, provider.enrichHandledMesssageLog(config)),
99
106
  });
100
107
  this.events.emit("messageHandled", eventContext);
108
+ span.setTag("pubsub.duration_ms", durationMs);
109
+ span.setTag("pubsub.success", true);
110
+ span.finish();
101
111
  return result;
102
112
  }
103
113
  catch (err) {
@@ -117,6 +127,8 @@ class Subscriber {
117
127
  err,
118
128
  };
119
129
  this.events.emit("messageHandled", eventContext);
130
+ span.setTag("pubsub.success", false);
131
+ span.finish();
120
132
  throw err;
121
133
  }
122
134
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palmetto/pubsub",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "main": "./dist/main.js",
5
5
  "scripts": {
6
6
  "lint": "yarn run -T eslint --fix ./src",