@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.
- package/README.md +185 -0
- package/dist/cloudevents.d.ts +73 -0
- package/dist/cloudevents.d.ts.map +1 -0
- package/dist/cloudevents.js +148 -0
- package/dist/cloudevents.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +40 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +11 -0
- package/dist/metrics.js.map +1 -0
- package/dist/naming.d.ts +54 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +78 -0
- package/dist/naming.js.map +1 -0
- package/dist/routing.d.ts +10 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +41 -0
- package/dist/routing.js.map +1 -0
- package/dist/topology.d.ts +2 -0
- package/dist/topology.d.ts.map +1 -0
- package/dist/topology.js +4 -0
- package/dist/topology.js.map +1 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +12 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +138 -0
- package/dist/validate.js.map +1 -0
- package/dist/visualize.d.ts +10 -0
- package/dist/visualize.d.ts.map +1 -0
- package/dist/visualize.js +134 -0
- package/dist/visualize.js.map +1 -0
- package/package.json +34 -0
- package/src/cloudevents.ts +166 -0
- package/src/index.ts +86 -0
- package/src/metrics.ts +58 -0
- package/src/naming.ts +94 -0
- package/src/routing.ts +39 -0
- package/src/topology.ts +13 -0
- package/src/types.ts +101 -0
- package/src/validate.ts +183 -0
- 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
|
+
}
|
package/src/topology.ts
ADDED
|
@@ -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";
|