@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,200 @@
|
|
|
1
|
+
import type { Topology } from '../topology/types.js';
|
|
2
|
+
import type { Envelope } from '../types/index.js';
|
|
3
|
+
import type { TransportCapabilities } from './capabilities.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Transport-specific send options.
|
|
7
|
+
* Each transport can define its own options under its transport name key.
|
|
8
|
+
*/
|
|
9
|
+
export interface TransportSendOptions {
|
|
10
|
+
/** RabbitMQ-specific send options */
|
|
11
|
+
readonly rabbitmq?: RabbitMQSendOptions | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* RabbitMQ-specific options for sending messages.
|
|
16
|
+
*/
|
|
17
|
+
export interface RabbitMQSendOptions {
|
|
18
|
+
/** Message expiration in milliseconds */
|
|
19
|
+
readonly expiration?: number | undefined;
|
|
20
|
+
|
|
21
|
+
/** Message persistence mode */
|
|
22
|
+
readonly persistent?: boolean | undefined;
|
|
23
|
+
|
|
24
|
+
/** Routing key override */
|
|
25
|
+
readonly routingKey?: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for sending a message.
|
|
30
|
+
*/
|
|
31
|
+
export interface SendOptions {
|
|
32
|
+
/** Delay delivery by this many milliseconds */
|
|
33
|
+
readonly delay?: number | undefined;
|
|
34
|
+
|
|
35
|
+
/** Priority level (0-255, higher = more important) */
|
|
36
|
+
readonly priority?: number | undefined;
|
|
37
|
+
|
|
38
|
+
/** Transport-specific options */
|
|
39
|
+
readonly transport?: TransportSendOptions | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Transport-specific subscribe options.
|
|
44
|
+
* Each transport can define its own options under its transport name key.
|
|
45
|
+
*/
|
|
46
|
+
export interface TransportSubscribeOptions {
|
|
47
|
+
/** RabbitMQ-specific subscribe options */
|
|
48
|
+
readonly rabbitmq?: RabbitMQSubscribeOptions | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* RabbitMQ-specific options for subscribing.
|
|
53
|
+
*/
|
|
54
|
+
export interface RabbitMQSubscribeOptions {
|
|
55
|
+
/** Consumer tag */
|
|
56
|
+
readonly consumerTag?: string | undefined;
|
|
57
|
+
|
|
58
|
+
/** Prefetch count (overrides concurrency option) */
|
|
59
|
+
readonly prefetch?: number | undefined;
|
|
60
|
+
|
|
61
|
+
/** Exclusive consumer */
|
|
62
|
+
readonly exclusive?: boolean | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Options for subscribing to a queue.
|
|
67
|
+
*/
|
|
68
|
+
export interface SubscribeOptions {
|
|
69
|
+
/** Concurrency hint (number of concurrent handlers) */
|
|
70
|
+
readonly concurrency?: number | undefined;
|
|
71
|
+
|
|
72
|
+
/** Override default delivery semantics */
|
|
73
|
+
readonly deliveryMode?: 'at-least-once' | 'at-most-once' | undefined;
|
|
74
|
+
|
|
75
|
+
/** Transport-specific options */
|
|
76
|
+
readonly transport?: TransportSubscribeOptions | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Receipt for a received message, used for acknowledgment.
|
|
81
|
+
*/
|
|
82
|
+
export interface MessageReceipt {
|
|
83
|
+
/** Opaque handle for the transport to identify the message */
|
|
84
|
+
readonly handle: unknown;
|
|
85
|
+
|
|
86
|
+
/** True if this is a redelivery (transport-reported if capable) */
|
|
87
|
+
readonly redelivered: boolean;
|
|
88
|
+
|
|
89
|
+
/** 1-based attempt number (transport-reported if capable, else from envelope) */
|
|
90
|
+
readonly attemptNumber: number;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Native delivery count from the transport.
|
|
94
|
+
* Tracks how many times this specific message was delivered without acknowledgment.
|
|
95
|
+
* Used for poison message detection to prevent crash loops.
|
|
96
|
+
* For transports that don't track this, defaults to attemptNumber.
|
|
97
|
+
*/
|
|
98
|
+
readonly deliveryCount: number;
|
|
99
|
+
|
|
100
|
+
/** Original queue/topic the message came from */
|
|
101
|
+
readonly sourceQueue: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The name of the transport that received this message (e.g., 'local', 'rabbitmq').
|
|
105
|
+
* For MultiTransport, this is the actual underlying transport, not the wrapper name.
|
|
106
|
+
*/
|
|
107
|
+
readonly sourceTransport: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Handler function for processing received messages.
|
|
112
|
+
*/
|
|
113
|
+
export type MessageHandler = (
|
|
114
|
+
envelope: Envelope,
|
|
115
|
+
receipt: MessageReceipt,
|
|
116
|
+
) => Promise<void>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Subscription handle for managing active subscriptions.
|
|
120
|
+
*/
|
|
121
|
+
export interface Subscription {
|
|
122
|
+
/** Cancels the subscription */
|
|
123
|
+
unsubscribe(): Promise<void>;
|
|
124
|
+
|
|
125
|
+
/** Whether the subscription is currently active */
|
|
126
|
+
readonly isActive: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Transport interface - the minimal abstraction for message delivery.
|
|
131
|
+
* Transports handle only I/O; all business logic lives in Matador core.
|
|
132
|
+
*/
|
|
133
|
+
export interface Transport {
|
|
134
|
+
/** Transport identifier */
|
|
135
|
+
readonly name: string;
|
|
136
|
+
|
|
137
|
+
/** Capabilities supported by this transport */
|
|
138
|
+
readonly capabilities: TransportCapabilities;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Establishes connection to the message broker.
|
|
142
|
+
* Should handle initial connection with retries.
|
|
143
|
+
*/
|
|
144
|
+
connect(): Promise<void>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Gracefully disconnects from the message broker.
|
|
148
|
+
* Should close all consumers before connection.
|
|
149
|
+
*/
|
|
150
|
+
disconnect(): Promise<void>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returns whether the transport is currently connected.
|
|
154
|
+
*/
|
|
155
|
+
isConnected(): boolean;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Translates and applies the generic topology to the transport.
|
|
159
|
+
* Creates necessary queues, exchanges, topics, etc.
|
|
160
|
+
*/
|
|
161
|
+
applyTopology(topology: Topology): Promise<void>;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sends a message to the specified queue.
|
|
165
|
+
* @returns The name of the transport that was used (useful for MultiTransport)
|
|
166
|
+
*/
|
|
167
|
+
send(
|
|
168
|
+
queue: string,
|
|
169
|
+
envelope: Envelope,
|
|
170
|
+
options?: SendOptions,
|
|
171
|
+
): Promise<Transport['name']>;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Subscribes to messages on the specified queue.
|
|
175
|
+
* The handler receives decoded envelopes and receipts.
|
|
176
|
+
*/
|
|
177
|
+
subscribe(
|
|
178
|
+
queue: string,
|
|
179
|
+
handler: MessageHandler,
|
|
180
|
+
options?: SubscribeOptions,
|
|
181
|
+
): Promise<Subscription>;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Acknowledges/completes a message.
|
|
185
|
+
* Called after processing is done (success, retry scheduled, or dead-lettered).
|
|
186
|
+
*/
|
|
187
|
+
complete(receipt: MessageReceipt): Promise<void>;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Sends a message to the dead-letter queue.
|
|
191
|
+
* For transports with native DL routing, may use native mechanism.
|
|
192
|
+
* For others, sends to DLQ then completes original.
|
|
193
|
+
*/
|
|
194
|
+
sendToDeadLetter?(
|
|
195
|
+
receipt: MessageReceipt,
|
|
196
|
+
dlqName: string,
|
|
197
|
+
envelope: Envelope,
|
|
198
|
+
reason: string,
|
|
199
|
+
): Promise<void>;
|
|
200
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery semantics for message processing.
|
|
3
|
+
* - 'at-least-once': Acknowledge after processing (may redeliver on failure)
|
|
4
|
+
* - 'at-most-once': Acknowledge before processing (no redelivery, may lose messages)
|
|
5
|
+
*/
|
|
6
|
+
export type DeliveryMode = 'at-least-once' | 'at-most-once';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Importance level for subscribers, used for monitoring and alerting prioritization.
|
|
10
|
+
*/
|
|
11
|
+
export type Importance =
|
|
12
|
+
| 'can-ignore'
|
|
13
|
+
| 'should-investigate'
|
|
14
|
+
| 'must-investigate';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Idempotency declaration for subscribers.
|
|
18
|
+
* - 'yes': Safe to retry on failure (subscriber handles duplicates)
|
|
19
|
+
* - 'no': Not safe to retry, may cause duplicate side effects
|
|
20
|
+
* - 'unknown': Idempotency not determined (default)
|
|
21
|
+
* - 'resumable': Uses checkpoint-based idempotency via io() calls
|
|
22
|
+
*/
|
|
23
|
+
export type Idempotency = 'yes' | 'no' | 'unknown' | 'resumable';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Result of a validation operation.
|
|
27
|
+
*/
|
|
28
|
+
export interface ValidationResult {
|
|
29
|
+
readonly valid: boolean;
|
|
30
|
+
readonly errors: readonly ValidationError[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Individual validation error.
|
|
35
|
+
*/
|
|
36
|
+
export interface ValidationError {
|
|
37
|
+
readonly path: string;
|
|
38
|
+
readonly message: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a successful validation result.
|
|
43
|
+
*/
|
|
44
|
+
export function validResult(): ValidationResult {
|
|
45
|
+
return { valid: true, errors: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a failed validation result.
|
|
50
|
+
*/
|
|
51
|
+
export function invalidResult(errors: ValidationError[]): ValidationResult {
|
|
52
|
+
return { valid: false, errors };
|
|
53
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Importance } from './common.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Message envelope containing the event data and routing/observability metadata.
|
|
5
|
+
* This is the transport-agnostic message format used throughout Matador.
|
|
6
|
+
*/
|
|
7
|
+
export interface Envelope<T = unknown> {
|
|
8
|
+
/** Unique message ID (UUID v4) */
|
|
9
|
+
readonly id: string;
|
|
10
|
+
|
|
11
|
+
/** The event data */
|
|
12
|
+
readonly data: T;
|
|
13
|
+
|
|
14
|
+
/** Routing, processing state, and observability metadata */
|
|
15
|
+
readonly docket: Docket;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Metadata associated with an envelope for routing, processing state, and observability.
|
|
20
|
+
*/
|
|
21
|
+
export interface Docket {
|
|
22
|
+
// === Routing ===
|
|
23
|
+
|
|
24
|
+
/** Event key for routing */
|
|
25
|
+
readonly eventKey: string;
|
|
26
|
+
|
|
27
|
+
/** Human-readable description of the event (for observability/logging) */
|
|
28
|
+
readonly eventDescription?: string | undefined;
|
|
29
|
+
|
|
30
|
+
/** Target subscriber name for 1:1 routing */
|
|
31
|
+
readonly targetSubscriber: string;
|
|
32
|
+
|
|
33
|
+
/** Original queue before any dead-letter routing */
|
|
34
|
+
originalQueue?: string | undefined;
|
|
35
|
+
|
|
36
|
+
/** Scheduled processing time for delayed messages (ISO 8601 string) */
|
|
37
|
+
scheduledFor?: string | undefined;
|
|
38
|
+
|
|
39
|
+
// === Processing State ===
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attempt counter managed by Matador (1-based).
|
|
43
|
+
* Incremented on each retry. Used when transport doesn't track attempts.
|
|
44
|
+
*/
|
|
45
|
+
attempts: number;
|
|
46
|
+
|
|
47
|
+
/** When the envelope was first created (ISO 8601 string) */
|
|
48
|
+
readonly createdAt: string;
|
|
49
|
+
|
|
50
|
+
/** Error message from first failure (for debugging) */
|
|
51
|
+
firstError?: string | undefined;
|
|
52
|
+
|
|
53
|
+
/** Error message from most recent failure */
|
|
54
|
+
lastError?: string | undefined;
|
|
55
|
+
|
|
56
|
+
// === Observability ===
|
|
57
|
+
|
|
58
|
+
/** Importance level for monitoring */
|
|
59
|
+
readonly importance: Importance;
|
|
60
|
+
|
|
61
|
+
/** Correlation ID for request tracing */
|
|
62
|
+
readonly correlationId?: string | undefined;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Custom metadata provided by the application.
|
|
66
|
+
* This is the merged result of universal metadata (from loadUniversalMetadata hook)
|
|
67
|
+
* and event-specific metadata (from dispatch options). Event-specific metadata
|
|
68
|
+
* overrides universal metadata when keys conflict.
|
|
69
|
+
*/
|
|
70
|
+
readonly metadata?: Record<string, unknown> | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fields from Docket that can be specified when creating an envelope.
|
|
75
|
+
*/
|
|
76
|
+
type DocketCreateFields = Pick<
|
|
77
|
+
Docket,
|
|
78
|
+
| 'eventKey'
|
|
79
|
+
| 'eventDescription'
|
|
80
|
+
| 'targetSubscriber'
|
|
81
|
+
| 'importance'
|
|
82
|
+
| 'correlationId'
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Options for creating an envelope.
|
|
87
|
+
*/
|
|
88
|
+
export interface CreateEnvelopeOptions<T> extends DocketCreateFields {
|
|
89
|
+
/** Optional custom ID (defaults to UUID v4) */
|
|
90
|
+
readonly id?: string | undefined;
|
|
91
|
+
|
|
92
|
+
/** The event data */
|
|
93
|
+
readonly data: T;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Event-specific metadata to include in the docket.
|
|
97
|
+
* Will be merged with universal metadata, with these values taking precedence.
|
|
98
|
+
*/
|
|
99
|
+
readonly metadata?: Record<string, unknown> | undefined;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Universal metadata loaded from the loadUniversalMetadata hook.
|
|
103
|
+
* This is provided by the fanout engine, not by the caller.
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
readonly universalMetadata?: Record<string, unknown> | undefined;
|
|
107
|
+
|
|
108
|
+
/** Delay processing by this many milliseconds */
|
|
109
|
+
readonly delayMs?: number | undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a new envelope with the provided options.
|
|
114
|
+
*/
|
|
115
|
+
export function createEnvelope<T>(
|
|
116
|
+
options: CreateEnvelopeOptions<T>,
|
|
117
|
+
): Envelope<T> {
|
|
118
|
+
const now = new Date().toISOString();
|
|
119
|
+
|
|
120
|
+
// Merge universal metadata with event-specific metadata
|
|
121
|
+
// Event-specific metadata takes precedence
|
|
122
|
+
const mergedMetadata =
|
|
123
|
+
options.universalMetadata || options.metadata
|
|
124
|
+
? { ...options.universalMetadata, ...options.metadata }
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
id: options.id ?? crypto.randomUUID(),
|
|
129
|
+
data: options.data,
|
|
130
|
+
docket: {
|
|
131
|
+
// Routing
|
|
132
|
+
eventKey: options.eventKey,
|
|
133
|
+
...(options.eventDescription !== undefined && {
|
|
134
|
+
eventDescription: options.eventDescription,
|
|
135
|
+
}),
|
|
136
|
+
targetSubscriber: options.targetSubscriber,
|
|
137
|
+
...(options.delayMs !== undefined &&
|
|
138
|
+
options.delayMs > 0 && {
|
|
139
|
+
scheduledFor: new Date(Date.now() + options.delayMs).toISOString(),
|
|
140
|
+
}),
|
|
141
|
+
// Processing state
|
|
142
|
+
attempts: 1,
|
|
143
|
+
createdAt: now,
|
|
144
|
+
// Observability
|
|
145
|
+
importance: options.importance,
|
|
146
|
+
...(options.correlationId !== undefined && {
|
|
147
|
+
correlationId: options.correlationId,
|
|
148
|
+
}),
|
|
149
|
+
...(mergedMetadata !== undefined && { metadata: mergedMetadata }),
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { createEnvelope } from './envelope.js';
|
|
3
|
+
import { MatadorEvent } from './event.js';
|
|
4
|
+
|
|
5
|
+
class UserCreatedEvent extends MatadorEvent {
|
|
6
|
+
static readonly key = 'user.created';
|
|
7
|
+
static readonly description = 'Fired when a new user is created';
|
|
8
|
+
|
|
9
|
+
constructor(public data: { userId: string; email: string }) {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class OrderPlacedEvent extends MatadorEvent {
|
|
15
|
+
static readonly key = 'order.placed';
|
|
16
|
+
static readonly description = 'Fired when an order is placed';
|
|
17
|
+
static readonly aliases = ['order.created'];
|
|
18
|
+
|
|
19
|
+
constructor(public data: { orderId: string; amount: number }) {
|
|
20
|
+
super();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class MinimalEvent extends MatadorEvent {
|
|
25
|
+
static readonly key = 'minimal.event';
|
|
26
|
+
|
|
27
|
+
constructor(public data: { id: string }) {
|
|
28
|
+
super();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('Event', () => {
|
|
33
|
+
describe('static fields', () => {
|
|
34
|
+
it('should have static key field on class', () => {
|
|
35
|
+
expect(UserCreatedEvent.key).toBe('user.created');
|
|
36
|
+
expect(OrderPlacedEvent.key).toBe('order.placed');
|
|
37
|
+
expect(MinimalEvent.key).toBe('minimal.event');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should have static description field on class when defined', () => {
|
|
41
|
+
expect(UserCreatedEvent.description).toBe(
|
|
42
|
+
'Fired when a new user is created',
|
|
43
|
+
);
|
|
44
|
+
expect(OrderPlacedEvent.description).toBe(
|
|
45
|
+
'Fired when an order is placed',
|
|
46
|
+
);
|
|
47
|
+
expect(MinimalEvent.description).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should have static aliases field on class when defined', () => {
|
|
51
|
+
expect(UserCreatedEvent.aliases).toBeUndefined();
|
|
52
|
+
expect(OrderPlacedEvent.aliases).toEqual(['order.created']);
|
|
53
|
+
expect(MinimalEvent.aliases).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('instance data', () => {
|
|
58
|
+
it('should have instance data field', () => {
|
|
59
|
+
const event = new UserCreatedEvent({
|
|
60
|
+
userId: 'usr_001',
|
|
61
|
+
email: 'test@example.com',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(event.data).toEqual({
|
|
65
|
+
userId: 'usr_001',
|
|
66
|
+
email: 'test@example.com',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Envelope with eventDescription', () => {
|
|
73
|
+
it('should include eventDescription in docket when provided', () => {
|
|
74
|
+
const event = new UserCreatedEvent({
|
|
75
|
+
userId: 'usr_123',
|
|
76
|
+
email: 'test@example.com',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const envelope = createEnvelope({
|
|
80
|
+
eventKey: UserCreatedEvent.key,
|
|
81
|
+
eventDescription: UserCreatedEvent.description,
|
|
82
|
+
targetSubscriber: 'test-subscriber',
|
|
83
|
+
data: event.data,
|
|
84
|
+
importance: 'should-investigate',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(envelope.docket.eventKey).toBe('user.created');
|
|
88
|
+
expect(envelope.docket.eventDescription).toBe(
|
|
89
|
+
'Fired when a new user is created',
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should not include eventDescription in docket when undefined', () => {
|
|
94
|
+
const event = new MinimalEvent({ id: 'min_123' });
|
|
95
|
+
|
|
96
|
+
const envelope = createEnvelope({
|
|
97
|
+
eventKey: MinimalEvent.key,
|
|
98
|
+
eventDescription: MinimalEvent.description,
|
|
99
|
+
targetSubscriber: 'test-subscriber',
|
|
100
|
+
data: event.data,
|
|
101
|
+
importance: 'should-investigate',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(envelope.docket.eventKey).toBe('minimal.event');
|
|
105
|
+
expect(envelope.docket.eventDescription).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should serialize envelope with eventDescription for logging', () => {
|
|
109
|
+
const event = new OrderPlacedEvent({
|
|
110
|
+
orderId: 'ord_456',
|
|
111
|
+
amount: 99.99,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const envelope = createEnvelope({
|
|
115
|
+
eventKey: OrderPlacedEvent.key,
|
|
116
|
+
eventDescription: OrderPlacedEvent.description,
|
|
117
|
+
targetSubscriber: 'order-processor',
|
|
118
|
+
data: event.data,
|
|
119
|
+
importance: 'must-investigate',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const serialized = JSON.stringify(envelope);
|
|
123
|
+
const parsed = JSON.parse(serialized);
|
|
124
|
+
|
|
125
|
+
expect(parsed.docket.eventKey).toBe('order.placed');
|
|
126
|
+
expect(parsed.docket.eventDescription).toBe(
|
|
127
|
+
'Fired when an order is placed',
|
|
128
|
+
);
|
|
129
|
+
expect(parsed.data.orderId).toBe('ord_456');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should include eventDescription in hook logging context', () => {
|
|
133
|
+
const event = new UserCreatedEvent({
|
|
134
|
+
userId: 'usr_error',
|
|
135
|
+
email: 'error@example.com',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const envelope = createEnvelope({
|
|
139
|
+
eventKey: UserCreatedEvent.key,
|
|
140
|
+
eventDescription: UserCreatedEvent.description,
|
|
141
|
+
targetSubscriber: 'user-handler',
|
|
142
|
+
data: event.data,
|
|
143
|
+
importance: 'should-investigate',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Simulating what would be logged in onEnqueueError hook
|
|
147
|
+
const errorLog = {
|
|
148
|
+
message: 'Failed to enqueue event',
|
|
149
|
+
eventKey: envelope.docket.eventKey,
|
|
150
|
+
eventDescription: envelope.docket.eventDescription,
|
|
151
|
+
data: envelope.data,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
expect(errorLog.eventKey).toBe('user.created');
|
|
155
|
+
expect(errorLog.eventDescription).toBe('Fired when a new user is created');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unique event routing key type alias.
|
|
3
|
+
*/
|
|
4
|
+
export type EventKey = string;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* JSON-serializable primitive types.
|
|
8
|
+
*/
|
|
9
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* JSON-serializable value (recursive type for objects and arrays).
|
|
13
|
+
*/
|
|
14
|
+
export type JsonValue =
|
|
15
|
+
| JsonPrimitive
|
|
16
|
+
| JsonValue[]
|
|
17
|
+
| { [key: string]: JsonValue };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* JSON-serializable record type for metadata.
|
|
21
|
+
*/
|
|
22
|
+
export type JsonRecord = Record<string, JsonValue>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Static properties required on Event classes for schema registration.
|
|
26
|
+
*/
|
|
27
|
+
export interface EventStatic<T = unknown> {
|
|
28
|
+
/** Unique routing key for the event */
|
|
29
|
+
readonly key: string;
|
|
30
|
+
|
|
31
|
+
/** Human-readable description of the event */
|
|
32
|
+
readonly description?: string;
|
|
33
|
+
|
|
34
|
+
/** Alternative names/keys for backwards compatibility */
|
|
35
|
+
readonly aliases?: readonly string[];
|
|
36
|
+
|
|
37
|
+
/** Create an instance from data (for deserialization) */
|
|
38
|
+
new (data: T): Event<T>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Base interface for all events.
|
|
43
|
+
* Events represent something that happened in the system.
|
|
44
|
+
*/
|
|
45
|
+
export interface Event<T = unknown> {
|
|
46
|
+
/** The event data/payload */
|
|
47
|
+
readonly data: T;
|
|
48
|
+
|
|
49
|
+
/** Event-specific metadata (merged with EventOptions metadata on dispatch) */
|
|
50
|
+
readonly metadata?: JsonRecord | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Options for dispatching an event.
|
|
55
|
+
*/
|
|
56
|
+
export interface EventOptions {
|
|
57
|
+
/** Delay processing by this many milliseconds */
|
|
58
|
+
readonly delayMs?: number | undefined;
|
|
59
|
+
|
|
60
|
+
/** Correlation ID for request tracing */
|
|
61
|
+
readonly correlationId?: string | undefined;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Event-specific metadata to include in the docket.
|
|
65
|
+
* This metadata will be merged with:
|
|
66
|
+
* 1. Event instance metadata (if defined on the event)
|
|
67
|
+
* 2. Universal metadata from the loadUniversalMetadata hook
|
|
68
|
+
* With EventOptions metadata taking precedence over event metadata,
|
|
69
|
+
* and both taking precedence over universal metadata.
|
|
70
|
+
*/
|
|
71
|
+
readonly metadata?: JsonRecord | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Abstract base class for creating Matador events.
|
|
76
|
+
* Extend this class to define custom events.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* class UserCreatedEvent extends MatadorEvent {
|
|
81
|
+
* static readonly key = 'user.created'
|
|
82
|
+
* static readonly description = 'Fired when a new user is created'
|
|
83
|
+
*
|
|
84
|
+
* constructor(
|
|
85
|
+
* public data: { userId: string; email: string },
|
|
86
|
+
* public metadata?: JsonRecord,
|
|
87
|
+
* ) {
|
|
88
|
+
* super()
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export abstract class MatadorEvent<T = unknown> implements Event<T> {
|
|
94
|
+
static readonly key: string;
|
|
95
|
+
static readonly description?: string;
|
|
96
|
+
static readonly aliases?: readonly string[];
|
|
97
|
+
|
|
98
|
+
/** The event data/payload - must be defined by subclass */
|
|
99
|
+
abstract readonly data: T;
|
|
100
|
+
|
|
101
|
+
/** Event-specific metadata */
|
|
102
|
+
readonly metadata?: JsonRecord | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Type helper to extract the data type from an event class.
|
|
107
|
+
*/
|
|
108
|
+
export type EventData<E extends Event<unknown>> = E extends Event<infer T>
|
|
109
|
+
? T
|
|
110
|
+
: never;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Type helper to get the event class type.
|
|
114
|
+
*/
|
|
115
|
+
export type EventClass<T = unknown> = EventStatic<T> &
|
|
116
|
+
(new (
|
|
117
|
+
data: T,
|
|
118
|
+
) => Event<T>);
|