@palmetto/pubsub 3.3.2 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,8 @@ It is up to the pub/sub implementations what to do with rejected messages. Optio
18
18
 
19
19
  ## Installation
20
20
 
21
+ Install pubsub and the basic peer dependencies. You'll also need to install a pubsub system like RabbitMq or BullMq. See below for links on how to use each supported pubsub system.
22
+
21
23
  ```sh
22
24
  yarn add @palmetto/pubsub zod @palmetto/trace dd-trace
23
25
  ```
@@ -189,6 +191,7 @@ The message is checked against the provided schema for every publish and subscri
189
191
  message: string, // the JSON string of the message
190
192
  context: MessageContext, // the context of the message provided by the SubscriptionProvider
191
193
  config: PubSubConfiguration, // the configuration for the message (eg: BullMqQueueConfiguration, RabbitQueueExchangeConfiguration)
194
+ error: z.ZodError, // the actual zod error from the schema
192
195
  ) => {
193
196
  // TODO
194
197
  },
@@ -209,3 +212,76 @@ It is up to the provider to obey the `retries` and `retryDelay` configuration pr
209
212
  ### Publisher errors
210
213
 
211
214
  When a message publish fails, it is up to the calling code to handle exceptions.
215
+
216
+ ## Events
217
+
218
+ Publisher and Subscribers fire events you can use to inspect messages.
219
+
220
+ ### Publisher events
221
+
222
+ - schemaError: fired when the message fails zod validation
223
+ - messagePublishing: fired before the message is published to the pubsub provider
224
+ - messagePublished: fired after the message was published via the pubsub provider
225
+
226
+ ```ts
227
+ import {
228
+ PublishingEventContext,
229
+ PublisherSchemaErrorEventContext,
230
+ PublishedEventContext,
231
+ } from "@palmetto/pubsub";
232
+
233
+ import { z } from "zod";
234
+
235
+ // when a schema validation fails none of the messages in a published batch are sent
236
+ publisher.on("schemaError", (ctx: PublisherSchemaErrorEventContext) => {
237
+ console.log(z.prettifyError(ctx.error));
238
+ });
239
+
240
+ // just before publishing the message
241
+ publisher.on("messagePublishing", (ctx: PublishingEventContext) => {
242
+ console.log(ctx.json);
243
+ });
244
+
245
+ // just after publishing the message
246
+ publisher.on("messagePublished", (ctx: PublishedEventContext) => {
247
+ console.log(ctx.json);
248
+ });
249
+ ```
250
+
251
+ ### Subscriber events
252
+
253
+ - messageReceived: fired when the plain JSON is received by the pubsub provider. You can modify the JSON at this time
254
+ - schemaError: fired when the message fails zod validation.
255
+ - messageHandling: fired before the message was published via the pubsub provider
256
+ - messageHandled: fired after the message was published via the pubsub provider
257
+
258
+ ```ts
259
+ import {
260
+ MessageReceivedEventContext,
261
+ SubscriberSchemaErrorEventContext,
262
+ HandlingEventContext,
263
+ HandledEventContext,
264
+ } from "@palmetto/pubsub";
265
+
266
+ import { z } from "zod";
267
+
268
+ // before any processing of the message
269
+ subscriber.on("messageReceived", (ctx: ReceivedEventContext) => {
270
+ console.log(ctx.json);
271
+ });
272
+
273
+ // when a schema validation fails
274
+ subscriber.on("schemaError", (ctx: SubscriberSchemaErrorEventContext) => {
275
+ console.log(z.prettifyError(ctx.error));
276
+ });
277
+
278
+ // before calling the message handler
279
+ subscriber.on("messageHandling", (ctx: HandlingEventContext) => {
280
+ console.log(ctx.json);
281
+ });
282
+
283
+ // after calling the message handler
284
+ subscriber.on("messageHandled", (ctx: HandledEventContext) => {
285
+ console.log(ctx.json);
286
+ });
287
+ ```
@@ -1,9 +1,61 @@
1
+ import { z } from "zod";
1
2
  import { BaseMessage, Logger, PublisherProvider, PublishMessageOptions, PubSubConfiguration } from "./interfaces.js";
3
+ export interface BasePublisherEventContext {
4
+ /**
5
+ * The original message sent into pubsub before it was parsed by the zod schema.
6
+ */
7
+ originalMessage: BaseMessage;
8
+ /**
9
+ * The message converted to JSON
10
+ */
11
+ json: string;
12
+ /**
13
+ * The configuration for the publisher that will publish this message.
14
+ */
15
+ config: PubSubConfiguration;
16
+ }
17
+ export interface PublishingEventContext extends BasePublisherEventContext {
18
+ /**
19
+ * The message being published. This is the message after it was parsed by the zod schema.
20
+ */
21
+ message: unknown;
22
+ }
23
+ export interface PublisherSchemaErrorEventContext extends BasePublisherEventContext {
24
+ /**
25
+ * The zod validation error that occurred when validating the message against the schema defined in the PubSubConfiguration.
26
+ */
27
+ error: z.ZodError;
28
+ }
29
+ export interface PublishedEventContext extends BasePublisherEventContext {
30
+ /**
31
+ * The message that was published. This is the message after it was parsed by the zod schema
32
+ */
33
+ message: unknown;
34
+ }
35
+ export interface PublisherEventMap {
36
+ /**
37
+ * Fired before a message is published. This can be used to log the published message or to perform additional actions before a message is published.
38
+ * @param context The context of the published message
39
+ */
40
+ messagePublishing: [context: PublishingEventContext];
41
+ /**
42
+ * Fired when a message fails validation. This can be used to log the validation error or to perform additional actions when a message fails validation.
43
+ * @param context The context of the validation error
44
+ */
45
+ schemaError: [context: PublisherSchemaErrorEventContext];
46
+ /**
47
+ * Fired after a message is published. This can be used to log the published message or to perform additional actions after a message is published.
48
+ * @param context The context of the published message
49
+ */
50
+ messagePublished: [context: PublishedEventContext];
51
+ }
2
52
  export declare class Publisher {
3
53
  private readonly logger;
4
54
  private readonly hashes;
5
55
  private readonly publisherProviders;
56
+ private readonly events;
6
57
  constructor(logger: Logger, providers?: PublisherProvider[]);
58
+ on<TK extends keyof PublisherEventMap>(event: TK, listener: (...args: PublisherEventMap[TK]) => void): this;
7
59
  addProvider(provider: PublisherProvider): void;
8
60
  removeProvider(providerOrTransport: PublisherProvider | string): boolean;
9
61
  init(config: PubSubConfiguration): Promise<void>;
package/dist/publisher.js CHANGED
@@ -16,12 +16,14 @@ const crypto_1 = require("crypto");
16
16
  const trace_1 = require("@palmetto/trace");
17
17
  const errors_js_1 = require("./errors.js");
18
18
  const message_logger_js_1 = require("./message-logger.js");
19
+ const events_1 = require("events");
19
20
  class Publisher {
20
21
  constructor(logger, providers) {
21
22
  var _a, _b;
22
23
  this.logger = logger;
23
24
  this.hashes = new Map();
24
25
  this.publisherProviders = new Map();
26
+ this.events = new events_1.EventEmitter();
25
27
  if (providers) {
26
28
  providers.forEach((provider) => this.publisherProviders.set(provider.transport, provider));
27
29
  }
@@ -29,6 +31,14 @@ class Publisher {
29
31
  ...this.publisherProviders.keys(),
30
32
  ].join(", ")}`);
31
33
  }
34
+ on(event, listener) {
35
+ this.events.on("messagePublished", (context) => {
36
+ var _a, _b;
37
+ (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `Event 'messagePublished' emitted for ${context.config.name} on transport ${context.config.transport}`);
38
+ });
39
+ this.events.on(event, listener);
40
+ return this;
41
+ }
32
42
  addProvider(provider) {
33
43
  var _a, _b;
34
44
  this.publisherProviders.set(provider.transport, provider);
@@ -59,9 +69,7 @@ class Publisher {
59
69
  }
60
70
  return (0, trace_1.getTracer)().trace("pubsub.publish", {
61
71
  resource: `publish ${config.transport} ${config.name}`,
62
- }, (span) => {
63
- return this.publishImpl(config, messages, options, span);
64
- });
72
+ }, (span) => this.publishImpl(config, messages, options, span));
65
73
  }
66
74
  publishImpl(config, messages, options, span) {
67
75
  return __awaiter(this, void 0, void 0, function* () {
@@ -91,10 +99,17 @@ class Publisher {
91
99
  logger: this.logger,
92
100
  extra: Object.assign({ transport: provider.transport, name: config.name }, provider.enrichPublishedMesssageLog(config)),
93
101
  });
94
- return { json: JSON.stringify(msg), error: check.error };
102
+ const json = JSON.stringify(msg);
103
+ this.events.emit("schemaError", {
104
+ originalMessage: msg,
105
+ error: check.error,
106
+ json,
107
+ config,
108
+ });
109
+ return { json, msg, error: check.error };
95
110
  }
96
111
  const json = JSON.stringify(check.data);
97
- return { json, data: check.data };
112
+ return { json, msg, data: check.data };
98
113
  });
99
114
  const errors = mappedMessages.filter((mm) => mm.error);
100
115
  if (errors.length > 0) {
@@ -102,9 +117,20 @@ class Publisher {
102
117
  throw new errors_js_1.SchemaValidationError(`Schema did not accept ${errors.length} of ${messages.length} message(s): ${errorMessage}`);
103
118
  }
104
119
  const jsons = mappedMessages.filter((j) => j.data).map((j) => j.json);
120
+ if (this.events.listenerCount("messagePublishing") > 0) {
121
+ mappedMessages.forEach((msg) => {
122
+ this.events.emit("messagePublishing", {
123
+ message: msg.data,
124
+ originalMessage: msg.msg,
125
+ json: msg.json,
126
+ config,
127
+ });
128
+ });
129
+ }
105
130
  const start = (0, message_logger_js_1.startTiming)();
106
131
  yield provider.publish(config, jsons, options);
107
132
  const duration = (0, message_logger_js_1.getDuration)(start);
133
+ const hasPublishedListener = this.events.listenerCount("messagePublished") > 0;
108
134
  mappedMessages.forEach((msg) => {
109
135
  (0, message_logger_js_1.logMessage)({
110
136
  note: "Published message",
@@ -113,6 +139,14 @@ class Publisher {
113
139
  logger: this.logger,
114
140
  extra: Object.assign({ transport: provider.transport, name: config.name, durationMs: duration, batchSize: messages.length }, provider.enrichPublishedMesssageLog(config)),
115
141
  });
142
+ if (hasPublishedListener) {
143
+ this.events.emit("messagePublished", {
144
+ message: msg.data,
145
+ originalMessage: msg.msg,
146
+ json: msg.json,
147
+ config,
148
+ });
149
+ }
116
150
  });
117
151
  if (span) {
118
152
  span.setTag("pubsub.duration_ms", duration);
@@ -1,17 +1,59 @@
1
1
  import { BaseMessage, Logger, MessageContext, MessageResult, PubSubConfiguration, SubscriberProvider } from "./interfaces.js";
2
- export interface SubscriberEventContext {
3
- message: string;
2
+ import { z } from "zod";
3
+ export interface BaseSubscriberEventContext {
4
+ /**
5
+ * The plain JSON message as received from the provider.
6
+ */
7
+ json: string;
8
+ /**
9
+ * The message context as provided by the provider. This includes metadata about the message such as attemptsMade and willAttemptRetry which can be used to determine if the message is being retried and how many times it has been attempted before.
10
+ */
4
11
  context: MessageContext;
12
+ /**
13
+ * The configuration for the subscriber that received this message. This can be used in the event listeners to get information about the subscriber such as the transport and name, and can be used in the enrichHandledMesssageLog to add additional information to the logs.
14
+ */
5
15
  config: PubSubConfiguration;
6
16
  }
7
- export interface MessageHandledEventContext extends SubscriberEventContext {
17
+ export interface ReceivedEventContext extends BaseSubscriberEventContext {
18
+ }
19
+ export interface SubscriberSchemaErrorEventContext extends BaseSubscriberEventContext {
20
+ /**
21
+ * The zod validation error that occurred when validating the message against the schema defined in the PubSubConfiguration.
22
+ */
23
+ error: z.ZodError;
24
+ }
25
+ export interface HandlingEventContext extends BaseSubscriberEventContext {
26
+ /**
27
+ * The message after it has been parsed by the zod schema.
28
+ */
29
+ message: unknown;
30
+ }
31
+ export interface HandledEventContext extends HandlingEventContext {
8
32
  durationMs: number;
9
33
  result?: MessageResult;
10
34
  err?: unknown;
11
35
  }
12
- export interface SubscriberEvents {
13
- schemaError: (context: SubscriberEventContext) => void;
14
- messageHandled: (context: MessageHandledEventContext) => void;
36
+ export interface SubscriberEventMap {
37
+ /**
38
+ * Fired when a message is received, before schema validation. This can be used to modify the raw message before validation, for example to add missing properties or to log the raw message.
39
+ * @param context The context of the received message
40
+ */
41
+ messageReceived: [context: ReceivedEventContext];
42
+ /**
43
+ * This is fired when a message fails schema validation. Note that the messageHandled event is also fired after this event, so you can use this event to log schema validation errors separately from processing errors in the onMessage callback.
44
+ * @param context The context of the message that failed schema validation
45
+ */
46
+ schemaError: [context: SubscriberSchemaErrorEventContext];
47
+ /**
48
+ * Fired before a message is being sent to the onMessage callback.
49
+ * @param context The context of the message that is being processed
50
+ */
51
+ messageHandling: [context: HandlingEventContext];
52
+ /**
53
+ * Fired after a message was processed and a result is available, or when an error has been caught in the onMessage callback.
54
+ * @param context The context of the message that was handled
55
+ */
56
+ messageHandled: [context: HandledEventContext];
15
57
  }
16
58
  export declare class Subscriber {
17
59
  private readonly logger;
@@ -19,7 +61,7 @@ export declare class Subscriber {
19
61
  private readonly events;
20
62
  private readonly subscribedMessages;
21
63
  constructor(logger: Logger, providers?: SubscriberProvider[]);
22
- on<TU extends keyof SubscriberEvents>(event: TU, listener: SubscriberEvents[TU]): this;
64
+ on<TU extends keyof SubscriberEventMap>(event: TU, listener: (...args: SubscriberEventMap[TU]) => void): this;
23
65
  /**
24
66
  * Starts the subscriber for the given configuration. Depending on the protocol, the subscriber may not be started immediately.
25
67
  *
@@ -58,6 +58,12 @@ class Subscriber {
58
58
  const enrichedConfig = provider.enrichHandledMesssageLog(config);
59
59
  const handleMessage = (jsonStr, context) => __awaiter(this, void 0, void 0, function* () {
60
60
  const start = (0, message_logger_js_1.startTiming)();
61
+ const messageReceivedEventContext = {
62
+ json: jsonStr,
63
+ context,
64
+ config,
65
+ };
66
+ this.events.emit("messageReceived", messageReceivedEventContext);
61
67
  const jsonObject = JSON.parse(jsonStr);
62
68
  const decodeResult = schema.safeDecode(jsonObject);
63
69
  if (!decodeResult.success) {
@@ -70,27 +76,37 @@ class Subscriber {
70
76
  extra: Object.assign({ transport: provider.transport, name: config.name, durationMs,
71
77
  context }, enrichedConfig),
72
78
  });
79
+ const schemaErrorEventContext = {
80
+ json: jsonStr,
81
+ context,
82
+ config,
83
+ error: decodeResult.error,
84
+ };
85
+ this.events.emit("schemaError", schemaErrorEventContext);
73
86
  const handledEventContext = {
74
- message: jsonStr,
87
+ json: jsonStr,
88
+ message: undefined,
75
89
  context,
76
90
  config,
77
91
  durationMs,
78
92
  result: interfaces_js_1.MessageResult.Fail,
93
+ err: decodeResult.error,
79
94
  };
80
- this.events.emit("schemaError", handledEventContext);
81
95
  this.events.emit("messageHandled", handledEventContext);
82
96
  return interfaces_js_1.MessageResult.Fail;
83
97
  }
84
98
  try {
85
- const result = yield onMessage(decodeResult.data, context);
86
- const durationMs = (0, message_logger_js_1.getDuration)(start);
87
- const eventContext = {
88
- message: jsonStr,
99
+ const messageHandlingEventContext = {
100
+ json: jsonStr,
101
+ message: decodeResult.data,
89
102
  context,
90
103
  config,
91
- durationMs,
92
- result,
93
104
  };
105
+ this.events.emit("messageHandling", messageHandlingEventContext);
106
+ const result = yield onMessage(decodeResult.data, context);
107
+ const durationMs = (0, message_logger_js_1.getDuration)(start);
108
+ const eventContext = Object.assign(Object.assign({}, messageHandlingEventContext), { durationMs,
109
+ result });
94
110
  (0, message_logger_js_1.logMessage)({
95
111
  note: "Subscriber processed message",
96
112
  message: decodeResult.data,
@@ -118,7 +134,8 @@ class Subscriber {
118
134
  ? interfaces_js_1.MessageResult.Retry
119
135
  : interfaces_js_1.MessageResult.Fail;
120
136
  const eventContext = {
121
- message: jsonStr,
137
+ json: jsonStr,
138
+ message: decodeResult.data,
122
139
  result,
123
140
  context,
124
141
  config,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palmetto/pubsub",
3
- "version": "3.3.2",
3
+ "version": "3.4.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/palmetto/galaxy"
@@ -56,5 +56,19 @@
56
56
  "amqplib": "^0.10.8",
57
57
  "bullmq": "^5.70.2",
58
58
  "zod": "^4.1"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@google-cloud/pubsub": {
62
+ "optional": true
63
+ },
64
+ "amqp-connection-manager": {
65
+ "optional": true
66
+ },
67
+ "amqplib": {
68
+ "optional": true
69
+ },
70
+ "bullmq": {
71
+ "optional": true
72
+ }
59
73
  }
60
74
  }