@gomessaging/messaging 0.0.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.
Files changed (47) hide show
  1. package/README.md +185 -0
  2. package/dist/cloudevents.d.ts +73 -0
  3. package/dist/cloudevents.d.ts.map +1 -0
  4. package/dist/cloudevents.js +148 -0
  5. package/dist/cloudevents.js.map +1 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +15 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/metrics.d.ts +40 -0
  11. package/dist/metrics.d.ts.map +1 -0
  12. package/dist/metrics.js +11 -0
  13. package/dist/metrics.js.map +1 -0
  14. package/dist/naming.d.ts +54 -0
  15. package/dist/naming.d.ts.map +1 -0
  16. package/dist/naming.js +78 -0
  17. package/dist/naming.js.map +1 -0
  18. package/dist/routing.d.ts +10 -0
  19. package/dist/routing.d.ts.map +1 -0
  20. package/dist/routing.js +41 -0
  21. package/dist/routing.js.map +1 -0
  22. package/dist/topology.d.ts +2 -0
  23. package/dist/topology.d.ts.map +1 -0
  24. package/dist/topology.js +4 -0
  25. package/dist/topology.js.map +1 -0
  26. package/dist/types.d.ts +73 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +5 -0
  29. package/dist/types.js.map +1 -0
  30. package/dist/validate.d.ts +12 -0
  31. package/dist/validate.d.ts.map +1 -0
  32. package/dist/validate.js +138 -0
  33. package/dist/validate.js.map +1 -0
  34. package/dist/visualize.d.ts +10 -0
  35. package/dist/visualize.d.ts.map +1 -0
  36. package/dist/visualize.js +134 -0
  37. package/dist/visualize.js.map +1 -0
  38. package/package.json +34 -0
  39. package/src/cloudevents.ts +166 -0
  40. package/src/index.ts +86 -0
  41. package/src/metrics.ts +58 -0
  42. package/src/naming.ts +94 -0
  43. package/src/routing.ts +39 -0
  44. package/src/topology.ts +13 -0
  45. package/src/types.ts +101 -0
  46. package/src/validate.ts +183 -0
  47. package/src/visualize.ts +167 -0
@@ -0,0 +1,166 @@
1
+ // MIT License
2
+ // Copyright (c) 2026 sparetimecoders
3
+
4
+ import type { DeliveryInfo, Headers, Metadata } from "./types.js";
5
+
6
+ /** CloudEvents attribute header keys for binary content mode (canonical "ce-" prefix). */
7
+ export const CESpecVersion = "ce-specversion";
8
+ export const CEType = "ce-type";
9
+ export const CESource = "ce-source";
10
+ export const CEID = "ce-id";
11
+ export const CETime = "ce-time";
12
+ export const CESpecVersionValue = "1.0";
13
+
14
+ /** Optional CE attributes */
15
+ export const CEDataContentType = "ce-datacontenttype";
16
+ export const CESubject = "ce-subject";
17
+ export const CEDataSchema = "ce-dataschema";
18
+
19
+ /** Extension attribute for correlation */
20
+ export const CECorrelationID = "ce-correlationid";
21
+
22
+ /** Bare CE attribute names (without prefix). */
23
+ export const CEAttrSpecVersion = "specversion";
24
+ export const CEAttrType = "type";
25
+ export const CEAttrSource = "source";
26
+ export const CEAttrID = "id";
27
+ export const CEAttrTime = "time";
28
+ export const CEAttrDataContentType = "datacontenttype";
29
+ export const CEAttrSubject = "subject";
30
+ export const CEAttrCorrelationID = "correlationid";
31
+
32
+ /** CERequiredAttributes lists header keys required by CE 1.0. */
33
+ export const CERequiredAttributes = [
34
+ CESpecVersion,
35
+ CEType,
36
+ CESource,
37
+ CEID,
38
+ CETime,
39
+ ];
40
+
41
+ /**
42
+ * AMQPCEHeaderKey returns the AMQP application-properties key for a bare
43
+ * CloudEvents attribute name, using the "cloudEvents:" prefix per the
44
+ * CloudEvents AMQP Protocol Binding specification.
45
+ * Example: AMQPCEHeaderKey("specversion") -> "cloudEvents:specversion"
46
+ */
47
+ export function AMQPCEHeaderKey(attr: string): string {
48
+ return "cloudEvents:" + attr;
49
+ }
50
+
51
+ /**
52
+ * NormalizeCEHeaders rewrites incoming transport headers so that all
53
+ * CloudEvents attributes use the canonical "ce-" prefix. This allows
54
+ * consumers to accept messages with any known prefix variant:
55
+ * - "cloudEvents:specversion" -> "ce-specversion"
56
+ * - "cloudEvents_specversion" -> "ce-specversion" (JMS compat)
57
+ * - "ce-specversion" -> unchanged
58
+ *
59
+ * Non-CE headers are preserved unchanged. A new object is returned.
60
+ */
61
+ export function normalizeCEHeaders(h: Headers): Headers {
62
+ const out: Headers = {};
63
+ for (const [k, v] of Object.entries(h)) {
64
+ if (k.startsWith("cloudEvents:")) {
65
+ out["ce-" + k.slice("cloudEvents:".length)] = v;
66
+ } else if (k.startsWith("cloudEvents_")) {
67
+ out["ce-" + k.slice("cloudEvents_".length)] = v;
68
+ } else {
69
+ out[k] = v;
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /**
76
+ * HasCEHeaders reports whether h contains at least one CE required attribute,
77
+ * checking for "ce-", "cloudEvents:", and "cloudEvents_" prefixes.
78
+ * Use this to distinguish legacy (pre-CloudEvents) messages from malformed CE messages.
79
+ */
80
+ export function hasCEHeaders(h: Headers): boolean {
81
+ for (const key of Object.keys(h)) {
82
+ if (
83
+ key.startsWith("ce-") ||
84
+ key.startsWith("cloudEvents:") ||
85
+ key.startsWith("cloudEvents_")
86
+ ) {
87
+ return true;
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * EnrichLegacyMetadata populates empty Metadata fields with synthetic
95
+ * CloudEvents attributes derived from transport delivery information.
96
+ *
97
+ * Fields set when empty: id (randomUUID), timestamp (now), type (from key),
98
+ * source (from source), dataContentType ("application/json"), specVersion ("1.0").
99
+ *
100
+ * Fields NOT set: correlationId, subject (cannot be inferred).
101
+ * Any non-empty fields in m are preserved (not overwritten).
102
+ * Returns a new Metadata object; the input is not mutated.
103
+ */
104
+ export function enrichLegacyMetadata(
105
+ m: Metadata,
106
+ info: DeliveryInfo,
107
+ ): Metadata {
108
+ return {
109
+ ...m,
110
+ id: m.id || crypto.randomUUID(),
111
+ timestamp: m.timestamp || new Date().toISOString(),
112
+ type: m.type || info.key,
113
+ source: m.source || info.source,
114
+ dataContentType: m.dataContentType || "application/json",
115
+ specVersion: m.specVersion || CESpecVersionValue,
116
+ };
117
+ }
118
+
119
+ /** RFC 3339 regex for timestamp validation. */
120
+ const RFC3339_RE =
121
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
122
+
123
+ function headerString(h: Headers, key: string): string {
124
+ const v = h[key];
125
+ if (typeof v === "string") {
126
+ return v;
127
+ }
128
+ return "";
129
+ }
130
+
131
+ /**
132
+ * MetadataFromHeaders extracts CloudEvents metadata from message headers
133
+ * into a Metadata struct. Invalid (non-RFC3339) timestamps return empty string.
134
+ */
135
+ export function metadataFromHeaders(h: Headers): Metadata {
136
+ const timeStr = headerString(h, CETime);
137
+ return {
138
+ id: headerString(h, CEID),
139
+ source: headerString(h, CESource),
140
+ type: headerString(h, CEType),
141
+ subject: headerString(h, CESubject),
142
+ dataContentType: headerString(h, CEDataContentType),
143
+ specVersion: headerString(h, CESpecVersion),
144
+ correlationId: headerString(h, CECorrelationID),
145
+ timestamp: RFC3339_RE.test(timeStr) ? timeStr : "",
146
+ };
147
+ }
148
+
149
+ /**
150
+ * ValidateCEHeaders checks that all required CloudEvents 1.0 attributes
151
+ * are present and are strings. Returns a list of warnings for any
152
+ * missing or non-string attributes.
153
+ */
154
+ export function validateCEHeaders(h: Headers): string[] {
155
+ const warnings: string[] = [];
156
+ for (const key of CERequiredAttributes) {
157
+ if (!(key in h)) {
158
+ warnings.push(`missing required attribute "${key}"`);
159
+ continue;
160
+ }
161
+ if (typeof h[key] !== "string") {
162
+ warnings.push(`attribute "${key}" is not a string`);
163
+ }
164
+ }
165
+ return warnings;
166
+ }
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ // MIT License
2
+ // Copyright (c) 2026 sparetimecoders
3
+
4
+ // Types
5
+ export type {
6
+ Transport,
7
+ EndpointDirection,
8
+ ExchangeKind,
9
+ Pattern,
10
+ Headers,
11
+ Metadata,
12
+ DeliveryInfo,
13
+ ConsumableEvent,
14
+ Endpoint,
15
+ Topology,
16
+ EventHandler,
17
+ RequestResponseEventHandler,
18
+ NotificationSource,
19
+ Notification,
20
+ ErrorNotification,
21
+ NotificationHandler,
22
+ ErrorNotificationHandler,
23
+ } from "./types.js";
24
+ export { ErrParseJSON } from "./types.js";
25
+
26
+ // Naming
27
+ export {
28
+ DefaultEventExchangeName,
29
+ KindTopic,
30
+ KindDirect,
31
+ KindHeaders,
32
+ topicExchangeName,
33
+ serviceEventQueueName,
34
+ serviceRequestExchangeName,
35
+ serviceResponseExchangeName,
36
+ serviceRequestQueueName,
37
+ serviceResponseQueueName,
38
+ natsStreamName,
39
+ natsSubject,
40
+ translateWildcard,
41
+ } from "./naming.js";
42
+
43
+ // CloudEvents
44
+ export {
45
+ CESpecVersion,
46
+ CEType,
47
+ CESource,
48
+ CEID,
49
+ CETime,
50
+ CESpecVersionValue,
51
+ CEDataContentType,
52
+ CESubject,
53
+ CEDataSchema,
54
+ CECorrelationID,
55
+ CEAttrSpecVersion,
56
+ CEAttrType,
57
+ CEAttrSource,
58
+ CEAttrID,
59
+ CEAttrTime,
60
+ CEAttrDataContentType,
61
+ CEAttrSubject,
62
+ CEAttrCorrelationID,
63
+ CERequiredAttributes,
64
+ AMQPCEHeaderKey,
65
+ normalizeCEHeaders,
66
+ hasCEHeaders,
67
+ enrichLegacyMetadata,
68
+ metadataFromHeaders,
69
+ validateCEHeaders,
70
+ } from "./cloudevents.js";
71
+
72
+ // Routing
73
+ export { matchRoutingKey, routingKeyOverlaps } from "./routing.js";
74
+
75
+ // Validation
76
+ export { validate, validateTopologies } from "./validate.js";
77
+
78
+ // Visualization
79
+ export { mermaid } from "./visualize.js";
80
+
81
+ // Topology (re-exports)
82
+ export {} from "./topology.js";
83
+
84
+ // Metrics
85
+ export type { MetricsRecorder, RoutingKeyMapper, MetricsOptions } from "./metrics.js";
86
+ export { mapRoutingKey } from "./metrics.js";
package/src/metrics.ts ADDED
@@ -0,0 +1,58 @@
1
+ // MIT License
2
+ // Copyright (c) 2026 sparetimecoders
3
+
4
+ /**
5
+ * Pluggable metrics interface for messaging adapters.
6
+ *
7
+ * Users wire this to any metrics backend (prom-client, OpenTelemetry,
8
+ * StatsD, etc.) by implementing MetricsRecorder and passing it to
9
+ * ConnectionOptions.
10
+ */
11
+
12
+ /** Records messaging metrics. Implement this interface to wire any metrics backend. */
13
+ export interface MetricsRecorder {
14
+ /** An event was received from the broker. */
15
+ eventReceived(queue: string, routingKey: string): void;
16
+
17
+ /** An event was received but no handler matched its routing key. */
18
+ eventWithoutHandler(queue: string, routingKey: string): void;
19
+
20
+ /** An event could not be parsed (invalid JSON). */
21
+ eventNotParsable(queue: string, routingKey: string): void;
22
+
23
+ /** An event was acknowledged (successfully processed). */
24
+ eventAck(queue: string, routingKey: string, durationMs: number): void;
25
+
26
+ /** An event was negatively acknowledged (handler failed). */
27
+ eventNack(queue: string, routingKey: string, durationMs: number): void;
28
+
29
+ /** A message was published successfully. */
30
+ publishSucceed(exchange: string, routingKey: string, durationMs: number): void;
31
+
32
+ /** A message failed to publish. */
33
+ publishFailed(exchange: string, routingKey: string, durationMs: number): void;
34
+ }
35
+
36
+ /**
37
+ * Maps a routing key before it is passed to metrics.
38
+ * Use this to normalize or redact dynamic segments (e.g. UUIDs)
39
+ * to prevent unbounded label cardinality.
40
+ */
41
+ export type RoutingKeyMapper = (key: string) => string;
42
+
43
+ /** Options for configuring metrics behavior. */
44
+ export interface MetricsOptions {
45
+ routingKeyMapper?: RoutingKeyMapper;
46
+ }
47
+
48
+ /**
49
+ * Apply a routing key mapper, defaulting to identity.
50
+ * Empty mapped values are replaced with "unknown".
51
+ */
52
+ export function mapRoutingKey(
53
+ key: string,
54
+ mapper?: RoutingKeyMapper,
55
+ ): string {
56
+ const mapped = mapper ? mapper(key) : key;
57
+ return mapped === "" ? "unknown" : mapped;
58
+ }
package/src/naming.ts ADDED
@@ -0,0 +1,94 @@
1
+ // MIT License
2
+ // Copyright (c) 2026 sparetimecoders
3
+
4
+ /** DefaultEventExchangeName is the default exchange name used for event streaming. */
5
+ export const DefaultEventExchangeName = "events";
6
+
7
+ /** Exchange kind constants matching AMQP exchange types. */
8
+ export const KindTopic = "topic";
9
+ export const KindDirect = "direct";
10
+ export const KindHeaders = "headers";
11
+
12
+ /**
13
+ * TopicExchangeName returns the topic exchange name for the given name.
14
+ * Format: `<name>.topic.exchange`
15
+ */
16
+ export function topicExchangeName(name: string): string {
17
+ return `${name}.${KindTopic}.exchange`;
18
+ }
19
+
20
+ /**
21
+ * ServiceEventQueueName returns the durable event queue name for a service.
22
+ * Format: `<exchangeName>.queue.<service>`
23
+ */
24
+ export function serviceEventQueueName(
25
+ exchangeName: string,
26
+ service: string,
27
+ ): string {
28
+ return `${exchangeName}.queue.${service}`;
29
+ }
30
+
31
+ /**
32
+ * ServiceRequestExchangeName returns the direct exchange name for requests to a service.
33
+ * Format: `<service>.direct.exchange.request`
34
+ */
35
+ export function serviceRequestExchangeName(service: string): string {
36
+ return `${service}.${KindDirect}.exchange.request`;
37
+ }
38
+
39
+ /**
40
+ * ServiceResponseExchangeName returns the headers exchange name for responses from a service.
41
+ * Format: `<service>.headers.exchange.response`
42
+ */
43
+ export function serviceResponseExchangeName(service: string): string {
44
+ return `${service}.${KindHeaders}.exchange.response`;
45
+ }
46
+
47
+ /**
48
+ * ServiceRequestQueueName returns the queue name for requests to a service.
49
+ * Format: `<service>.direct.exchange.request.queue`
50
+ */
51
+ export function serviceRequestQueueName(service: string): string {
52
+ return `${serviceRequestExchangeName(service)}.queue`;
53
+ }
54
+
55
+ /**
56
+ * ServiceResponseQueueName returns the queue name for responses from targetService to serviceName.
57
+ * Format: `<targetService>.headers.exchange.response.queue.<serviceName>`
58
+ */
59
+ export function serviceResponseQueueName(
60
+ targetService: string,
61
+ serviceName: string,
62
+ ): string {
63
+ return `${serviceResponseExchangeName(targetService)}.queue.${serviceName}`;
64
+ }
65
+
66
+ /**
67
+ * NATSStreamName extracts the base stream name from a logical name.
68
+ * If the name follows the AMQP convention `<name>.topic.exchange`, the prefix is extracted.
69
+ * Otherwise the name is returned as-is.
70
+ */
71
+ export function natsStreamName(name: string): string {
72
+ const suffix = ".topic.exchange";
73
+ if (name.endsWith(suffix)) {
74
+ return name.slice(0, -suffix.length);
75
+ }
76
+ return name;
77
+ }
78
+
79
+ /**
80
+ * NATSSubject builds a NATS subject from a stream name and routing key.
81
+ * Format: `<stream>.<routingKey>`
82
+ */
83
+ export function natsSubject(stream: string, routingKey: string): string {
84
+ return `${stream}.${routingKey}`;
85
+ }
86
+
87
+ /**
88
+ * TranslateWildcard converts AMQP-style wildcards to NATS-style wildcards.
89
+ * AMQP "#" (multi-level) -> NATS ">" (multi-level)
90
+ * AMQP "*" (single-level) stays "*" in NATS.
91
+ */
92
+ export function translateWildcard(routingKey: string): string {
93
+ return routingKey.replaceAll("#", ">");
94
+ }
package/src/routing.ts ADDED
@@ -0,0 +1,39 @@
1
+ // MIT License
2
+ // Copyright (c) 2026 sparetimecoders
3
+
4
+ /**
5
+ * Converts an AMQP/NATS binding pattern to a regular expression string.
6
+ *
7
+ * - `.` is escaped to `\.`
8
+ * - `*` matches a single word: `[^.]*`
9
+ * - `#` matches zero or more words: `.*`
10
+ */
11
+ function routingKeyToRegex(pattern: string): string {
12
+ const escaped = pattern
13
+ .replaceAll(".", "\\.")
14
+ .replaceAll("*", "[^.]*")
15
+ .replaceAll("#", ".*");
16
+ return `^${escaped}$`;
17
+ }
18
+
19
+ /**
20
+ * Returns true if routingKey matches the binding pattern.
21
+ * Supports AMQP/NATS wildcard syntax: `*` matches one word, `#` matches zero or more.
22
+ */
23
+ export function matchRoutingKey(pattern: string, routingKey: string): boolean {
24
+ try {
25
+ return new RegExp(routingKeyToRegex(pattern)).test(routingKey);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Returns true if two binding patterns could match the same routing key.
33
+ */
34
+ export function routingKeyOverlaps(p1: string, p2: string): boolean {
35
+ if (p1 === p2) return true;
36
+ if (matchRoutingKey(p1, p2)) return true;
37
+ if (matchRoutingKey(p2, p1)) return true;
38
+ return false;
39
+ }
@@ -0,0 +1,13 @@
1
+ // MIT License
2
+ // Copyright (c) 2026 sparetimecoders
3
+
4
+ // Re-export topology-related types from types.ts.
5
+ // This module serves as the canonical import path for topology types.
6
+ export type {
7
+ Transport,
8
+ EndpointDirection,
9
+ ExchangeKind,
10
+ Pattern,
11
+ Endpoint,
12
+ Topology,
13
+ } from "./types.js";
package/src/types.ts ADDED
@@ -0,0 +1,101 @@
1
+ // MIT License
2
+ // Copyright (c) 2026 sparetimecoders
3
+
4
+ /** Transport identifies the messaging transport implementation. */
5
+ export type Transport = "amqp" | "nats";
6
+
7
+ /** EndpointDirection indicates whether an endpoint publishes or consumes messages. */
8
+ export type EndpointDirection = "publish" | "consume";
9
+
10
+ /** ExchangeKind represents the type of an exchange. */
11
+ export type ExchangeKind = "topic" | "direct" | "headers";
12
+
13
+ /** Pattern identifies the communication pattern an endpoint participates in. */
14
+ export type Pattern =
15
+ | "event-stream"
16
+ | "custom-stream"
17
+ | "service-request"
18
+ | "service-response"
19
+ | "queue-publish";
20
+
21
+ /** Headers represent all meta-data for the message. */
22
+ export type Headers = Record<string, unknown>;
23
+
24
+ /** Metadata holds the metadata of an event. */
25
+ export interface Metadata {
26
+ id: string;
27
+ correlationId: string;
28
+ timestamp: string;
29
+ source: string;
30
+ type: string;
31
+ subject: string;
32
+ dataContentType: string;
33
+ specVersion: string;
34
+ }
35
+
36
+ /** DeliveryInfo holds transport-agnostic delivery metadata. */
37
+ export interface DeliveryInfo {
38
+ destination: string;
39
+ source: string;
40
+ key: string;
41
+ headers: Headers;
42
+ }
43
+
44
+ /** ConsumableEvent represents an event that can be consumed. */
45
+ export interface ConsumableEvent<T> extends Metadata {
46
+ deliveryInfo: DeliveryInfo;
47
+ payload: T;
48
+ }
49
+
50
+ /** Endpoint describes a single exchange/queue/binding that a service declares. */
51
+ export interface Endpoint {
52
+ direction: EndpointDirection;
53
+ pattern: Pattern;
54
+ exchangeName: string;
55
+ exchangeKind: ExchangeKind;
56
+ queueName?: string;
57
+ routingKey?: string;
58
+ messageType?: string;
59
+ ephemeral?: boolean;
60
+ }
61
+
62
+ /** Topology describes the full messaging topology declared by a single service. */
63
+ export interface Topology {
64
+ transport?: Transport;
65
+ serviceName: string;
66
+ endpoints: Endpoint[];
67
+ }
68
+
69
+ /** EventHandler is a function that handles events of a specific type. */
70
+ export type EventHandler<T> = (
71
+ event: ConsumableEvent<T>,
72
+ ) => Promise<void>;
73
+
74
+ /** RequestResponseEventHandler handles events and returns a response. */
75
+ export type RequestResponseEventHandler<T, R> = (
76
+ event: ConsumableEvent<T>,
77
+ ) => Promise<R>;
78
+
79
+ /** NotificationSource identifies the origin of a notification. */
80
+ export type NotificationSource = "CONSUMER";
81
+
82
+ /** Notification represents a successful event processing notification. */
83
+ export interface Notification {
84
+ deliveryInfo: DeliveryInfo;
85
+ durationMs: number;
86
+ source: NotificationSource;
87
+ }
88
+
89
+ /** ErrorNotification represents a failed event processing notification. */
90
+ export interface ErrorNotification extends Notification {
91
+ error: Error;
92
+ }
93
+
94
+ /** NotificationHandler is called after successful event processing. */
95
+ export type NotificationHandler = (n: Notification) => void;
96
+
97
+ /** ErrorNotificationHandler is called after failed event processing. */
98
+ export type ErrorNotificationHandler = (n: ErrorNotification) => void;
99
+
100
+ /** ErrParseJSON sentinel error message. */
101
+ export const ErrParseJSON = "failed to parse";