@zdavison/matador 2.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/cli.ts +453 -0
- package/dist/checkpoint/context.d.ts +59 -0
- package/dist/checkpoint/context.d.ts.map +1 -0
- package/dist/checkpoint/context.js +140 -0
- package/dist/checkpoint/context.test.d.ts +2 -0
- package/dist/checkpoint/context.test.d.ts.map +1 -0
- package/dist/checkpoint/context.test.js +424 -0
- package/dist/checkpoint/index.d.ts +7 -0
- package/dist/checkpoint/index.d.ts.map +1 -0
- package/dist/checkpoint/index.js +6 -0
- package/dist/checkpoint/stores/memory.d.ts +29 -0
- package/dist/checkpoint/stores/memory.d.ts.map +1 -0
- package/dist/checkpoint/stores/memory.js +39 -0
- package/dist/checkpoint/stores/noop.d.ts +14 -0
- package/dist/checkpoint/stores/noop.d.ts.map +1 -0
- package/dist/checkpoint/stores/noop.js +18 -0
- package/dist/checkpoint/stores/stores.test.d.ts +2 -0
- package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
- package/dist/checkpoint/stores/stores.test.js +146 -0
- package/dist/checkpoint/types.d.ts +119 -0
- package/dist/checkpoint/types.d.ts.map +1 -0
- package/dist/checkpoint/types.js +1 -0
- package/dist/codec/codec.d.ts +29 -0
- package/dist/codec/codec.d.ts.map +1 -0
- package/dist/codec/codec.js +15 -0
- package/dist/codec/header-aware-codec.d.ts +36 -0
- package/dist/codec/header-aware-codec.d.ts.map +1 -0
- package/dist/codec/header-aware-codec.js +1 -0
- package/dist/codec/index.d.ts +6 -0
- package/dist/codec/index.d.ts.map +1 -0
- package/dist/codec/index.js +3 -0
- package/dist/codec/json-codec.d.ts +13 -0
- package/dist/codec/json-codec.d.ts.map +1 -0
- package/dist/codec/json-codec.js +64 -0
- package/dist/codec/rabbitmq-codec.d.ts +28 -0
- package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
- package/dist/codec/rabbitmq-codec.js +242 -0
- package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
- package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
- package/dist/codec/rabbitmq-codec.test.js +433 -0
- package/dist/core/fanout.d.ts +59 -0
- package/dist/core/fanout.d.ts.map +1 -0
- package/dist/core/fanout.js +121 -0
- package/dist/core/fanout.test.d.ts +2 -0
- package/dist/core/fanout.test.d.ts.map +1 -0
- package/dist/core/fanout.test.js +1055 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/matador.d.ts +133 -0
- package/dist/core/matador.d.ts.map +1 -0
- package/dist/core/matador.js +222 -0
- package/dist/core/matador.test.d.ts +2 -0
- package/dist/core/matador.test.d.ts.map +1 -0
- package/dist/core/matador.test.js +446 -0
- package/dist/core/shutdown.d.ts +78 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +111 -0
- package/dist/core/shutdown.test.d.ts +2 -0
- package/dist/core/shutdown.test.d.ts.map +1 -0
- package/dist/core/shutdown.test.js +524 -0
- package/dist/errors/checkpoint-errors.d.ts +30 -0
- package/dist/errors/checkpoint-errors.d.ts.map +1 -0
- package/dist/errors/checkpoint-errors.js +49 -0
- package/dist/errors/has-description.d.ts +18 -0
- package/dist/errors/has-description.d.ts.map +1 -0
- package/dist/errors/has-description.js +9 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +22 -0
- package/dist/errors/matador-errors.d.ts +183 -0
- package/dist/errors/matador-errors.d.ts.map +1 -0
- package/dist/errors/matador-errors.js +376 -0
- package/dist/errors/retry-errors.d.ts +67 -0
- package/dist/errors/retry-errors.d.ts.map +1 -0
- package/dist/errors/retry-errors.js +108 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/safe-hooks.d.ts +34 -0
- package/dist/hooks/safe-hooks.d.ts.map +1 -0
- package/dist/hooks/safe-hooks.js +135 -0
- package/dist/hooks/types.d.ts +189 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +9 -0
- package/dist/index.cjs +3156 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/index.d.ts +3 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/pipeline.d.ts +60 -0
- package/dist/pipeline/pipeline.d.ts.map +1 -0
- package/dist/pipeline/pipeline.js +261 -0
- package/dist/pipeline/pipeline.test.d.ts +2 -0
- package/dist/pipeline/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline/pipeline.test.js +1065 -0
- package/dist/retry/index.d.ts +4 -0
- package/dist/retry/index.d.ts.map +1 -0
- package/dist/retry/index.js +1 -0
- package/dist/retry/policy.d.ts +43 -0
- package/dist/retry/policy.d.ts.map +1 -0
- package/dist/retry/policy.js +1 -0
- package/dist/retry/standard-policy.d.ts +44 -0
- package/dist/retry/standard-policy.d.ts.map +1 -0
- package/dist/retry/standard-policy.js +102 -0
- package/dist/retry/standard-policy.test.d.ts +2 -0
- package/dist/retry/standard-policy.test.d.ts.map +1 -0
- package/dist/retry/standard-policy.test.js +190 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/registry.d.ts +63 -0
- package/dist/schema/registry.d.ts.map +1 -0
- package/dist/schema/registry.js +171 -0
- package/dist/schema/registry.test.d.ts +2 -0
- package/dist/schema/registry.test.d.ts.map +1 -0
- package/dist/schema/registry.test.js +278 -0
- package/dist/schema/types.d.ts +158 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +74 -0
- package/dist/schema/types.test.d.ts +2 -0
- package/dist/schema/types.test.d.ts.map +1 -0
- package/dist/schema/types.test.js +243 -0
- package/dist/topology/builder.d.ts +66 -0
- package/dist/topology/builder.d.ts.map +1 -0
- package/dist/topology/builder.js +156 -0
- package/dist/topology/builder.test.d.ts +2 -0
- package/dist/topology/builder.test.d.ts.map +1 -0
- package/dist/topology/builder.test.js +222 -0
- package/dist/topology/index.d.ts +5 -0
- package/dist/topology/index.d.ts.map +1 -0
- package/dist/topology/index.js +2 -0
- package/dist/topology/types.d.ts +76 -0
- package/dist/topology/types.d.ts.map +1 -0
- package/dist/topology/types.js +18 -0
- package/dist/transport/capabilities.d.ts +66 -0
- package/dist/transport/capabilities.d.ts.map +1 -0
- package/dist/transport/capabilities.js +18 -0
- package/dist/transport/connection-manager.d.ts +95 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/connection-manager.js +144 -0
- package/dist/transport/index.d.ts +11 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/local/local-transport.d.ts +62 -0
- package/dist/transport/local/local-transport.d.ts.map +1 -0
- package/dist/transport/local/local-transport.js +241 -0
- package/dist/transport/local/local-transport.test.d.ts +2 -0
- package/dist/transport/local/local-transport.test.d.ts.map +1 -0
- package/dist/transport/local/local-transport.test.js +192 -0
- package/dist/transport/multi/multi-transport.d.ts +94 -0
- package/dist/transport/multi/multi-transport.d.ts.map +1 -0
- package/dist/transport/multi/multi-transport.js +184 -0
- package/dist/transport/multi/multi-transport.test.d.ts +2 -0
- package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
- package/dist/transport/multi/multi-transport.test.js +236 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
- package/dist/transport/transport.d.ts +152 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/transport/transport.js +1 -0
- package/dist/types/common.d.ts +41 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +12 -0
- package/dist/types/envelope.d.ts +82 -0
- package/dist/types/envelope.d.ts.map +1 -0
- package/dist/types/envelope.js +36 -0
- package/dist/types/event.d.ts +96 -0
- package/dist/types/event.d.ts.map +1 -0
- package/dist/types/event.js +26 -0
- package/dist/types/event.test.d.ts +2 -0
- package/dist/types/event.test.d.ts.map +1 -0
- package/dist/types/event.test.js +130 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/subscriber.d.ts +207 -0
- package/dist/types/subscriber.d.ts.map +1 -0
- package/dist/types/subscriber.js +99 -0
- package/examples/config.ts +126 -0
- package/examples/event.ts +26 -0
- package/examples/order-event.json +19 -0
- package/package.json +66 -0
- package/src/checkpoint/context.test.ts +510 -0
- package/src/checkpoint/context.ts +213 -0
- package/src/checkpoint/index.ts +30 -0
- package/src/checkpoint/stores/memory.ts +47 -0
- package/src/checkpoint/stores/noop.ts +22 -0
- package/src/checkpoint/stores/stores.test.ts +177 -0
- package/src/checkpoint/types.ts +147 -0
- package/src/codec/codec.ts +42 -0
- package/src/codec/header-aware-codec.ts +41 -0
- package/src/codec/index.ts +11 -0
- package/src/codec/json-codec.ts +69 -0
- package/src/codec/rabbitmq-codec.test.ts +516 -0
- package/src/codec/rabbitmq-codec.ts +336 -0
- package/src/core/fanout.test.ts +1351 -0
- package/src/core/fanout.ts +184 -0
- package/src/core/index.ts +12 -0
- package/src/core/matador.test.ts +575 -0
- package/src/core/matador.ts +344 -0
- package/src/core/shutdown.test.ts +853 -0
- package/src/core/shutdown.ts +165 -0
- package/src/errors/checkpoint-errors.ts +62 -0
- package/src/errors/has-description.ts +25 -0
- package/src/errors/index.ts +57 -0
- package/src/errors/matador-errors.ts +477 -0
- package/src/errors/retry-errors.ts +134 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/safe-hooks.ts +223 -0
- package/src/hooks/types.ts +248 -0
- package/src/index.ts +231 -0
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/pipeline.test.ts +1317 -0
- package/src/pipeline/pipeline.ts +371 -0
- package/src/retry/index.ts +4 -0
- package/src/retry/policy.ts +46 -0
- package/src/retry/standard-policy.test.ts +282 -0
- package/src/retry/standard-policy.ts +156 -0
- package/src/schema/index.ts +16 -0
- package/src/schema/registry.test.ts +339 -0
- package/src/schema/registry.ts +226 -0
- package/src/schema/types.test.ts +281 -0
- package/src/schema/types.ts +217 -0
- package/src/topology/builder.test.ts +275 -0
- package/src/topology/builder.ts +199 -0
- package/src/topology/index.ts +15 -0
- package/src/topology/types.ts +109 -0
- package/src/transport/capabilities.ts +88 -0
- package/src/transport/connection-manager.ts +218 -0
- package/src/transport/index.ts +42 -0
- package/src/transport/local/local-transport.test.ts +262 -0
- package/src/transport/local/local-transport.ts +327 -0
- package/src/transport/multi/multi-transport.test.ts +320 -0
- package/src/transport/multi/multi-transport.ts +294 -0
- package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
- package/src/transport/transport.ts +200 -0
- package/src/types/common.ts +53 -0
- package/src/types/envelope.ts +152 -0
- package/src/types/event.test.ts +157 -0
- package/src/types/event.ts +118 -0
- package/src/types/index.ts +52 -0
- package/src/types/subscriber.ts +310 -0
- package/test/e2e/multi-transport.e2e.test.ts +236 -0
- package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
- package/test/e2e/transport-compliance.e2e.test.ts +505 -0
- package/test/integration/matador.integration.test.ts +634 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnySubscriber,
|
|
3
|
+
EventClass,
|
|
4
|
+
MatadorEvent,
|
|
5
|
+
Subscriber,
|
|
6
|
+
SubscriberStub,
|
|
7
|
+
} from '../types/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type-safe schema entry for a single event type (object format).
|
|
11
|
+
* The type parameter enforces that subscribers match the event type.
|
|
12
|
+
*/
|
|
13
|
+
export interface SchemaEntry<T extends MatadorEvent = MatadorEvent> {
|
|
14
|
+
/** The event class */
|
|
15
|
+
readonly eventClass: EventClass<T['data']>;
|
|
16
|
+
|
|
17
|
+
/** Subscribers for this event */
|
|
18
|
+
readonly subscribers: readonly (Subscriber<T> | SubscriberStub)[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type-safe schema entry as a tuple: [EventClass, Subscribers[]]
|
|
23
|
+
* The type parameter enforces that subscribers match the event type.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const entry: SchemaEntryTuple<UserCreatedEvent> = [
|
|
28
|
+
* UserCreatedEvent,
|
|
29
|
+
* [createSubscriber<UserCreatedEvent>('handler', async (env) => {})]
|
|
30
|
+
* ];
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export type SchemaEntryTuple<T extends MatadorEvent = MatadorEvent> = readonly [
|
|
34
|
+
eventClass: EventClass<T['data']>,
|
|
35
|
+
subscribers: readonly (Subscriber<T> | SubscriberStub)[],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a type-safe schema entry tuple.
|
|
40
|
+
* Ensures that subscribers are compatible with the event class at compile time.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const schema = {
|
|
45
|
+
* [UserCreatedEvent.key]: bind(UserCreatedEvent, [
|
|
46
|
+
* createSubscriber<UserCreatedEvent>('send-email', async (env) => {
|
|
47
|
+
* console.log(env.data.email); // Type-safe access
|
|
48
|
+
* }),
|
|
49
|
+
* ]),
|
|
50
|
+
* } satisfies MatadorSchema;
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function bind<T extends MatadorEvent>(
|
|
54
|
+
eventClass: EventClass<T['data']>,
|
|
55
|
+
subscribers: readonly (Subscriber<T> | SubscriberStub)[],
|
|
56
|
+
): SchemaEntryTuple<T> {
|
|
57
|
+
return [eventClass, subscribers] as const;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Minimal type for event class in schema storage.
|
|
62
|
+
* Only requires the static `key` property for routing.
|
|
63
|
+
* Constructor constraint is omitted to allow heterogeneous event types.
|
|
64
|
+
*/
|
|
65
|
+
// biome-ignore lint/suspicious/noExplicitAny: Constructor accepts any data type for variance compatibility
|
|
66
|
+
export type AnyEventClass = {
|
|
67
|
+
readonly key: string;
|
|
68
|
+
new (data: any): MatadorEvent;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Runtime schema entry stored in MatadorSchema (object format).
|
|
73
|
+
* Uses AnySubscriber and AnyEventClass to allow heterogeneous event types.
|
|
74
|
+
* Type safety is enforced at definition time via `schemaEntry()` helper.
|
|
75
|
+
*/
|
|
76
|
+
export interface RuntimeSchemaEntry {
|
|
77
|
+
readonly eventClass: AnyEventClass;
|
|
78
|
+
readonly subscribers: readonly AnySubscriber[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Runtime schema tuple stored in MatadorSchema.
|
|
83
|
+
* Uses AnySubscriber and AnyEventClass to allow heterogeneous event types.
|
|
84
|
+
* Type safety is enforced at definition time via `schemaEntry()` helper.
|
|
85
|
+
*/
|
|
86
|
+
export type RuntimeSchemaEntryTuple = readonly [
|
|
87
|
+
eventClass: AnyEventClass,
|
|
88
|
+
subscribers: readonly AnySubscriber[],
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Matador schema mapping event keys to their definitions.
|
|
93
|
+
* Supports both object format (SchemaEntry) and tuple format (SchemaEntryTuple).
|
|
94
|
+
*
|
|
95
|
+
* For type-safe schema definitions, use the `bind` helper function
|
|
96
|
+
* which ensures subscribers are compatible with their event class.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* // Type-safe with bind helper:
|
|
101
|
+
* const schema = {
|
|
102
|
+
* [UserCreatedEvent.key]: bind(UserCreatedEvent, [emailSubscriber]),
|
|
103
|
+
* } satisfies MatadorSchema;
|
|
104
|
+
*
|
|
105
|
+
* // Or inline (less type-safe):
|
|
106
|
+
* const schema: MatadorSchema = {
|
|
107
|
+
* [UserCreatedEvent.key]: [UserCreatedEvent, [emailSubscriber]],
|
|
108
|
+
* };
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export type MatadorSchema = {
|
|
112
|
+
readonly [eventKey: string]: RuntimeSchemaEntry | RuntimeSchemaEntryTuple;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Type guard to check if a schema entry is in tuple format.
|
|
117
|
+
*/
|
|
118
|
+
export function isSchemaEntryTuple(
|
|
119
|
+
entry: RuntimeSchemaEntry | RuntimeSchemaEntryTuple,
|
|
120
|
+
): entry is RuntimeSchemaEntryTuple {
|
|
121
|
+
return Array.isArray(entry);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Options for schema registration.
|
|
126
|
+
*/
|
|
127
|
+
export interface RegisterOptions {
|
|
128
|
+
/** Override existing registration if present */
|
|
129
|
+
readonly override?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Result of schema validation.
|
|
134
|
+
*/
|
|
135
|
+
export interface SchemaValidationResult {
|
|
136
|
+
readonly valid: boolean;
|
|
137
|
+
readonly issues: readonly SchemaIssue[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Individual schema issue.
|
|
142
|
+
*/
|
|
143
|
+
export interface SchemaIssue {
|
|
144
|
+
readonly severity: 'error' | 'warning';
|
|
145
|
+
readonly eventKey: string;
|
|
146
|
+
readonly message: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Plugin definition for adding subscribers to multiple events.
|
|
151
|
+
*/
|
|
152
|
+
export interface SchemaPlugin {
|
|
153
|
+
/** The subscriber to add to events */
|
|
154
|
+
readonly subscriber: AnySubscriber;
|
|
155
|
+
|
|
156
|
+
/** Event keys to exclude from this plugin */
|
|
157
|
+
readonly exclusions?: readonly string[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Installs plugins (global subscribers) into a schema.
|
|
162
|
+
* Each plugin subscriber is added to all events except those in its exclusions list.
|
|
163
|
+
*
|
|
164
|
+
* @param schema - The base schema to extend
|
|
165
|
+
* @param plugins - Array of plugins to install
|
|
166
|
+
* @returns A new schema with plugin subscribers added
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* const baseSchema: MatadorSchema = {
|
|
171
|
+
* [UserCreatedEvent.key]: [UserCreatedEvent, [sendWelcomeEmail]],
|
|
172
|
+
* [OrderPlacedEvent.key]: [OrderPlacedEvent, [processOrder]],
|
|
173
|
+
* [ChatMessageSent.key]: [ChatMessageSent, [notifyRecipient]],
|
|
174
|
+
* };
|
|
175
|
+
*
|
|
176
|
+
* const schema = installPlugins(baseSchema, [
|
|
177
|
+
* {
|
|
178
|
+
* subscriber: logToBigQuery,
|
|
179
|
+
* exclusions: [ChatMessageSent.key], // Don't log chat messages
|
|
180
|
+
* },
|
|
181
|
+
* {
|
|
182
|
+
* subscriber: trackAnalytics,
|
|
183
|
+
* // No exclusions - added to all events
|
|
184
|
+
* },
|
|
185
|
+
* ]);
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export function installPlugins(
|
|
189
|
+
schema: MatadorSchema,
|
|
190
|
+
plugins: readonly SchemaPlugin[],
|
|
191
|
+
): MatadorSchema {
|
|
192
|
+
const result: Record<string, RuntimeSchemaEntry | RuntimeSchemaEntryTuple> =
|
|
193
|
+
{};
|
|
194
|
+
|
|
195
|
+
for (const [eventKey, entry] of Object.entries(schema)) {
|
|
196
|
+
// Get existing event class and subscribers
|
|
197
|
+
const [eventClass, existingSubscribers] = isSchemaEntryTuple(entry)
|
|
198
|
+
? [entry[0], entry[1]]
|
|
199
|
+
: [entry.eventClass, entry.subscribers];
|
|
200
|
+
|
|
201
|
+
// Collect subscribers to add from plugins
|
|
202
|
+
const pluginSubscribers: AnySubscriber[] = [];
|
|
203
|
+
for (const plugin of plugins) {
|
|
204
|
+
if (!plugin.exclusions?.includes(eventKey)) {
|
|
205
|
+
pluginSubscribers.push(plugin.subscriber);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Create new entry with combined subscribers
|
|
210
|
+
result[eventKey] = [
|
|
211
|
+
eventClass,
|
|
212
|
+
[...existingSubscribers, ...pluginSubscribers],
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { TopologyBuilder, TopologyValidationError } from './builder.js';
|
|
3
|
+
|
|
4
|
+
describe('TopologyBuilder', () => {
|
|
5
|
+
describe('withNamespace', () => {
|
|
6
|
+
it('should set the namespace', () => {
|
|
7
|
+
const topology = TopologyBuilder.create()
|
|
8
|
+
.withNamespace('myapp')
|
|
9
|
+
.addQueue('events')
|
|
10
|
+
.build();
|
|
11
|
+
|
|
12
|
+
expect(topology.namespace).toBe('myapp');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should reject empty namespace', () => {
|
|
16
|
+
const builder = TopologyBuilder.create()
|
|
17
|
+
.withNamespace('')
|
|
18
|
+
.addQueue('events');
|
|
19
|
+
|
|
20
|
+
expect(() => builder.build()).toThrow(TopologyValidationError);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should reject namespace starting with number', () => {
|
|
24
|
+
const builder = TopologyBuilder.create()
|
|
25
|
+
.withNamespace('123app')
|
|
26
|
+
.addQueue('events');
|
|
27
|
+
|
|
28
|
+
expect(() => builder.build()).toThrow(TopologyValidationError);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should allow hyphens and underscores in namespace', () => {
|
|
32
|
+
const topology = TopologyBuilder.create()
|
|
33
|
+
.withNamespace('my-app_v2')
|
|
34
|
+
.addQueue('events')
|
|
35
|
+
.build();
|
|
36
|
+
|
|
37
|
+
expect(topology.namespace).toBe('my-app_v2');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('addQueue', () => {
|
|
42
|
+
it('should add a queue with defaults', () => {
|
|
43
|
+
const topology = TopologyBuilder.create()
|
|
44
|
+
.withNamespace('test')
|
|
45
|
+
.addQueue('events')
|
|
46
|
+
.build();
|
|
47
|
+
|
|
48
|
+
expect(topology.queues).toHaveLength(1);
|
|
49
|
+
expect(topology.queues[0]?.name).toBe('events');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should add queue with options', () => {
|
|
53
|
+
const topology = TopologyBuilder.create()
|
|
54
|
+
.withNamespace('test')
|
|
55
|
+
.addQueue('events', {
|
|
56
|
+
concurrency: 5,
|
|
57
|
+
consumerTimeout: 30000,
|
|
58
|
+
priorities: true,
|
|
59
|
+
})
|
|
60
|
+
.build();
|
|
61
|
+
|
|
62
|
+
expect(topology.queues[0]?.concurrency).toBe(5);
|
|
63
|
+
expect(topology.queues[0]?.consumerTimeout).toBe(30000);
|
|
64
|
+
expect(topology.queues[0]?.priorities).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should add multiple queues', () => {
|
|
68
|
+
const topology = TopologyBuilder.create()
|
|
69
|
+
.withNamespace('test')
|
|
70
|
+
.addQueue('events')
|
|
71
|
+
.addQueue('notifications')
|
|
72
|
+
.addQueue('analytics')
|
|
73
|
+
.build();
|
|
74
|
+
|
|
75
|
+
expect(topology.queues).toHaveLength(3);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should reject duplicate queue names', () => {
|
|
79
|
+
const builder = TopologyBuilder.create()
|
|
80
|
+
.withNamespace('test')
|
|
81
|
+
.addQueue('events')
|
|
82
|
+
.addQueue('events');
|
|
83
|
+
|
|
84
|
+
expect(() => builder.build()).toThrow('Duplicate queue name');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should reject empty queue name', () => {
|
|
88
|
+
const builder = TopologyBuilder.create()
|
|
89
|
+
.withNamespace('test')
|
|
90
|
+
.addQueue('');
|
|
91
|
+
|
|
92
|
+
expect(() => builder.build()).toThrow('Queue name cannot be empty');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should reject queue names starting with number', () => {
|
|
96
|
+
const builder = TopologyBuilder.create()
|
|
97
|
+
.withNamespace('test')
|
|
98
|
+
.addQueue('123queue');
|
|
99
|
+
|
|
100
|
+
expect(() => builder.build()).toThrow('must start with a letter');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should reject invalid concurrency', () => {
|
|
104
|
+
const builder = TopologyBuilder.create()
|
|
105
|
+
.withNamespace('test')
|
|
106
|
+
.addQueue('events', { concurrency: 0 });
|
|
107
|
+
|
|
108
|
+
expect(() => builder.build()).toThrow('concurrency must be at least 1');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should reject negative consumer timeout', () => {
|
|
112
|
+
const builder = TopologyBuilder.create()
|
|
113
|
+
.withNamespace('test')
|
|
114
|
+
.addQueue('events', { consumerTimeout: -1 });
|
|
115
|
+
|
|
116
|
+
expect(() => builder.build()).toThrow(
|
|
117
|
+
'consumer timeout must be non-negative',
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('withDeadLetter', () => {
|
|
123
|
+
it('should configure dead letter settings', () => {
|
|
124
|
+
const topology = TopologyBuilder.create()
|
|
125
|
+
.withNamespace('test')
|
|
126
|
+
.addQueue('events')
|
|
127
|
+
.withDeadLetter({
|
|
128
|
+
unhandled: { enabled: false },
|
|
129
|
+
undeliverable: { enabled: true },
|
|
130
|
+
})
|
|
131
|
+
.build();
|
|
132
|
+
|
|
133
|
+
expect(topology.deadLetter.unhandled.enabled).toBe(false);
|
|
134
|
+
expect(topology.deadLetter.undeliverable.enabled).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should merge with defaults', () => {
|
|
138
|
+
const topology = TopologyBuilder.create()
|
|
139
|
+
.withNamespace('test')
|
|
140
|
+
.addQueue('events')
|
|
141
|
+
.withDeadLetter({ unhandled: { enabled: false } })
|
|
142
|
+
.build();
|
|
143
|
+
|
|
144
|
+
// undeliverable should keep default
|
|
145
|
+
expect(topology.deadLetter.unhandled.enabled).toBe(false);
|
|
146
|
+
expect(topology.deadLetter.undeliverable.enabled).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('withRetry', () => {
|
|
151
|
+
it('should configure retry settings', () => {
|
|
152
|
+
const topology = TopologyBuilder.create()
|
|
153
|
+
.withNamespace('test')
|
|
154
|
+
.addQueue('events')
|
|
155
|
+
.withRetry({
|
|
156
|
+
enabled: true,
|
|
157
|
+
defaultDelayMs: 5000,
|
|
158
|
+
maxDelayMs: 600000,
|
|
159
|
+
})
|
|
160
|
+
.build();
|
|
161
|
+
|
|
162
|
+
expect(topology.retry.enabled).toBe(true);
|
|
163
|
+
expect(topology.retry.defaultDelayMs).toBe(5000);
|
|
164
|
+
expect(topology.retry.maxDelayMs).toBe(600000);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should reject negative default delay', () => {
|
|
168
|
+
const builder = TopologyBuilder.create()
|
|
169
|
+
.withNamespace('test')
|
|
170
|
+
.addQueue('events')
|
|
171
|
+
.withRetry({ defaultDelayMs: -1 });
|
|
172
|
+
|
|
173
|
+
expect(() => builder.build()).toThrow('delay must be non-negative');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should reject max delay less than default', () => {
|
|
177
|
+
const builder = TopologyBuilder.create()
|
|
178
|
+
.withNamespace('test')
|
|
179
|
+
.addQueue('events')
|
|
180
|
+
.withRetry({
|
|
181
|
+
defaultDelayMs: 10000,
|
|
182
|
+
maxDelayMs: 5000,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(() => builder.build()).toThrow(
|
|
186
|
+
'Max retry delay must be greater than or equal to default',
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('withoutRetry', () => {
|
|
192
|
+
it('should disable retry', () => {
|
|
193
|
+
const topology = TopologyBuilder.create()
|
|
194
|
+
.withNamespace('test')
|
|
195
|
+
.addQueue('events')
|
|
196
|
+
.withoutRetry()
|
|
197
|
+
.build();
|
|
198
|
+
|
|
199
|
+
expect(topology.retry.enabled).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('withoutDeadLetter', () => {
|
|
204
|
+
it('should disable dead letter queues', () => {
|
|
205
|
+
const topology = TopologyBuilder.create()
|
|
206
|
+
.withNamespace('test')
|
|
207
|
+
.addQueue('events')
|
|
208
|
+
.withoutDeadLetter()
|
|
209
|
+
.build();
|
|
210
|
+
|
|
211
|
+
expect(topology.deadLetter.unhandled.enabled).toBe(false);
|
|
212
|
+
expect(topology.deadLetter.undeliverable.enabled).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('validate', () => {
|
|
217
|
+
it('should return issues without throwing', () => {
|
|
218
|
+
const builder = TopologyBuilder.create();
|
|
219
|
+
const issues = builder.validate();
|
|
220
|
+
|
|
221
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
222
|
+
expect(issues).toContain('Namespace is required');
|
|
223
|
+
expect(issues).toContain('At least one queue is required');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should return empty array for valid topology', () => {
|
|
227
|
+
const builder = TopologyBuilder.create()
|
|
228
|
+
.withNamespace('test')
|
|
229
|
+
.addQueue('events');
|
|
230
|
+
|
|
231
|
+
const issues = builder.validate();
|
|
232
|
+
expect(issues).toHaveLength(0);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('build', () => {
|
|
237
|
+
it('should throw TopologyValidationError with issues', () => {
|
|
238
|
+
const builder = TopologyBuilder.create();
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
builder.build();
|
|
242
|
+
expect(true).toBe(false); // Should not reach here
|
|
243
|
+
} catch (error) {
|
|
244
|
+
expect(error).toBeInstanceOf(TopologyValidationError);
|
|
245
|
+
expect((error as TopologyValidationError).issues).toContain(
|
|
246
|
+
'Namespace is required',
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should return immutable topology', () => {
|
|
252
|
+
const topology = TopologyBuilder.create()
|
|
253
|
+
.withNamespace('test')
|
|
254
|
+
.addQueue('events')
|
|
255
|
+
.build();
|
|
256
|
+
|
|
257
|
+
// Verify structure
|
|
258
|
+
expect(topology.namespace).toBe('test');
|
|
259
|
+
expect(topology.queues).toHaveLength(1);
|
|
260
|
+
expect(topology.deadLetter.unhandled.enabled).toBe(true);
|
|
261
|
+
expect(topology.retry.enabled).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('exact queue option', () => {
|
|
266
|
+
it('should mark queue as exact (external)', () => {
|
|
267
|
+
const topology = TopologyBuilder.create()
|
|
268
|
+
.withNamespace('test')
|
|
269
|
+
.addQueue('external-queue', { exact: true })
|
|
270
|
+
.build();
|
|
271
|
+
|
|
272
|
+
expect(topology.queues[0]?.exact).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { HasDescription } from '../errors/index.js';
|
|
2
|
+
import type {
|
|
3
|
+
DeadLetterConfig,
|
|
4
|
+
QueueDefinition,
|
|
5
|
+
RetryConfig,
|
|
6
|
+
Topology,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for adding a queue.
|
|
11
|
+
* Excludes 'name' as that is provided as the first argument to addQueue.
|
|
12
|
+
*/
|
|
13
|
+
export type QueueOptions = Omit<QueueDefinition, 'name'>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when topology validation fails.
|
|
17
|
+
*/
|
|
18
|
+
export class TopologyValidationError extends Error implements HasDescription {
|
|
19
|
+
readonly description =
|
|
20
|
+
'The topology configuration is invalid. Check the issues array for ' +
|
|
21
|
+
'specific validation failures such as missing namespace, invalid queue ' +
|
|
22
|
+
'names, or conflicting settings. This error occurs during Matador ' +
|
|
23
|
+
'initialization and must be fixed in the configuration.';
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
message: string,
|
|
27
|
+
public readonly issues: readonly string[],
|
|
28
|
+
) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'TopologyValidationError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fluent builder for creating Topology configurations.
|
|
36
|
+
*/
|
|
37
|
+
export class TopologyBuilder {
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new TopologyBuilder instance.
|
|
40
|
+
*/
|
|
41
|
+
static create(): TopologyBuilder {
|
|
42
|
+
return new TopologyBuilder();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private namespace = '';
|
|
46
|
+
private queues: QueueDefinition[] = [];
|
|
47
|
+
private deadLetter: DeadLetterConfig = {
|
|
48
|
+
unhandled: { enabled: true },
|
|
49
|
+
undeliverable: { enabled: true },
|
|
50
|
+
};
|
|
51
|
+
private retry: RetryConfig = {
|
|
52
|
+
enabled: true,
|
|
53
|
+
defaultDelayMs: 1000,
|
|
54
|
+
maxDelayMs: 300000, // 5 minutes
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sets the namespace prefix for all queues.
|
|
59
|
+
*/
|
|
60
|
+
withNamespace(namespace: string): this {
|
|
61
|
+
this.namespace = namespace;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Adds a queue to the topology.
|
|
67
|
+
*/
|
|
68
|
+
addQueue(name: string, options: QueueOptions = {}): this {
|
|
69
|
+
this.queues.push({ name, ...options });
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Alias for addQueue().
|
|
75
|
+
*/
|
|
76
|
+
queue(name: string, options: QueueOptions = {}): this {
|
|
77
|
+
return this.addQueue(name, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Configures dead-letter queue settings.
|
|
82
|
+
*/
|
|
83
|
+
withDeadLetter(config: Partial<DeadLetterConfig>): this {
|
|
84
|
+
this.deadLetter = {
|
|
85
|
+
unhandled: config.unhandled ?? this.deadLetter.unhandled,
|
|
86
|
+
undeliverable: config.undeliverable ?? this.deadLetter.undeliverable,
|
|
87
|
+
};
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Configures retry settings.
|
|
93
|
+
*/
|
|
94
|
+
withRetry(config: Partial<RetryConfig>): this {
|
|
95
|
+
this.retry = {
|
|
96
|
+
enabled: config.enabled ?? this.retry.enabled,
|
|
97
|
+
defaultDelayMs: config.defaultDelayMs ?? this.retry.defaultDelayMs,
|
|
98
|
+
maxDelayMs: config.maxDelayMs ?? this.retry.maxDelayMs,
|
|
99
|
+
};
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Disables retry functionality.
|
|
105
|
+
*/
|
|
106
|
+
withoutRetry(): this {
|
|
107
|
+
this.retry = { ...this.retry, enabled: false };
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Disables dead-letter queues.
|
|
113
|
+
*/
|
|
114
|
+
withoutDeadLetter(): this {
|
|
115
|
+
this.deadLetter = {
|
|
116
|
+
unhandled: { enabled: false },
|
|
117
|
+
undeliverable: { enabled: false },
|
|
118
|
+
};
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validates the topology configuration.
|
|
124
|
+
*/
|
|
125
|
+
validate(): readonly string[] {
|
|
126
|
+
const issues: string[] = [];
|
|
127
|
+
|
|
128
|
+
if (!this.namespace || this.namespace.trim() === '') {
|
|
129
|
+
issues.push('Namespace is required');
|
|
130
|
+
} else if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(this.namespace)) {
|
|
131
|
+
issues.push(
|
|
132
|
+
'Namespace must start with a letter and contain only alphanumeric characters, underscores, and hyphens',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.queues.length === 0) {
|
|
137
|
+
issues.push('At least one queue is required');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const queueNames = new Set<string>();
|
|
141
|
+
for (const queue of this.queues) {
|
|
142
|
+
if (!queue.name || queue.name.trim() === '') {
|
|
143
|
+
issues.push('Queue name cannot be empty');
|
|
144
|
+
} else if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(queue.name)) {
|
|
145
|
+
issues.push(
|
|
146
|
+
`Queue name "${queue.name}" must start with a letter and contain only alphanumeric characters, underscores, and hyphens`,
|
|
147
|
+
);
|
|
148
|
+
} else if (queueNames.has(queue.name)) {
|
|
149
|
+
issues.push(`Duplicate queue name: "${queue.name}"`);
|
|
150
|
+
} else {
|
|
151
|
+
queueNames.add(queue.name);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (queue.concurrency !== undefined && queue.concurrency < 1) {
|
|
155
|
+
issues.push(`Queue "${queue.name}" concurrency must be at least 1`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (queue.consumerTimeout !== undefined && queue.consumerTimeout < 0) {
|
|
159
|
+
issues.push(
|
|
160
|
+
`Queue "${queue.name}" consumer timeout must be non-negative`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.retry.enabled) {
|
|
166
|
+
if (this.retry.defaultDelayMs < 0) {
|
|
167
|
+
issues.push('Default retry delay must be non-negative');
|
|
168
|
+
}
|
|
169
|
+
if (this.retry.maxDelayMs < this.retry.defaultDelayMs) {
|
|
170
|
+
issues.push(
|
|
171
|
+
'Max retry delay must be greater than or equal to default delay',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return issues;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Builds the topology configuration.
|
|
181
|
+
* @throws TopologyValidationError if validation fails
|
|
182
|
+
*/
|
|
183
|
+
build(): Topology {
|
|
184
|
+
const issues = this.validate();
|
|
185
|
+
if (issues.length > 0) {
|
|
186
|
+
throw new TopologyValidationError(
|
|
187
|
+
`Invalid topology: ${issues.join('; ')}`,
|
|
188
|
+
issues,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
namespace: this.namespace,
|
|
194
|
+
queues: [...this.queues],
|
|
195
|
+
deadLetter: this.deadLetter,
|
|
196
|
+
retry: this.retry,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
DeadLetterConfig,
|
|
3
|
+
DeadLetterQueueConfig,
|
|
4
|
+
QueueDefinition,
|
|
5
|
+
RetryConfig,
|
|
6
|
+
Topology,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
export {
|
|
9
|
+
getDeadLetterQueueName,
|
|
10
|
+
getQualifiedQueueName,
|
|
11
|
+
getRetryQueueName,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
|
|
14
|
+
export type { QueueOptions } from './builder.js';
|
|
15
|
+
export { TopologyBuilder, TopologyValidationError } from './builder.js';
|