@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.
Files changed (256) hide show
  1. package/cli.ts +453 -0
  2. package/dist/checkpoint/context.d.ts +59 -0
  3. package/dist/checkpoint/context.d.ts.map +1 -0
  4. package/dist/checkpoint/context.js +140 -0
  5. package/dist/checkpoint/context.test.d.ts +2 -0
  6. package/dist/checkpoint/context.test.d.ts.map +1 -0
  7. package/dist/checkpoint/context.test.js +424 -0
  8. package/dist/checkpoint/index.d.ts +7 -0
  9. package/dist/checkpoint/index.d.ts.map +1 -0
  10. package/dist/checkpoint/index.js +6 -0
  11. package/dist/checkpoint/stores/memory.d.ts +29 -0
  12. package/dist/checkpoint/stores/memory.d.ts.map +1 -0
  13. package/dist/checkpoint/stores/memory.js +39 -0
  14. package/dist/checkpoint/stores/noop.d.ts +14 -0
  15. package/dist/checkpoint/stores/noop.d.ts.map +1 -0
  16. package/dist/checkpoint/stores/noop.js +18 -0
  17. package/dist/checkpoint/stores/stores.test.d.ts +2 -0
  18. package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
  19. package/dist/checkpoint/stores/stores.test.js +146 -0
  20. package/dist/checkpoint/types.d.ts +119 -0
  21. package/dist/checkpoint/types.d.ts.map +1 -0
  22. package/dist/checkpoint/types.js +1 -0
  23. package/dist/codec/codec.d.ts +29 -0
  24. package/dist/codec/codec.d.ts.map +1 -0
  25. package/dist/codec/codec.js +15 -0
  26. package/dist/codec/header-aware-codec.d.ts +36 -0
  27. package/dist/codec/header-aware-codec.d.ts.map +1 -0
  28. package/dist/codec/header-aware-codec.js +1 -0
  29. package/dist/codec/index.d.ts +6 -0
  30. package/dist/codec/index.d.ts.map +1 -0
  31. package/dist/codec/index.js +3 -0
  32. package/dist/codec/json-codec.d.ts +13 -0
  33. package/dist/codec/json-codec.d.ts.map +1 -0
  34. package/dist/codec/json-codec.js +64 -0
  35. package/dist/codec/rabbitmq-codec.d.ts +28 -0
  36. package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
  37. package/dist/codec/rabbitmq-codec.js +242 -0
  38. package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
  39. package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
  40. package/dist/codec/rabbitmq-codec.test.js +433 -0
  41. package/dist/core/fanout.d.ts +59 -0
  42. package/dist/core/fanout.d.ts.map +1 -0
  43. package/dist/core/fanout.js +121 -0
  44. package/dist/core/fanout.test.d.ts +2 -0
  45. package/dist/core/fanout.test.d.ts.map +1 -0
  46. package/dist/core/fanout.test.js +1055 -0
  47. package/dist/core/index.d.ts +7 -0
  48. package/dist/core/index.d.ts.map +1 -0
  49. package/dist/core/index.js +3 -0
  50. package/dist/core/matador.d.ts +133 -0
  51. package/dist/core/matador.d.ts.map +1 -0
  52. package/dist/core/matador.js +222 -0
  53. package/dist/core/matador.test.d.ts +2 -0
  54. package/dist/core/matador.test.d.ts.map +1 -0
  55. package/dist/core/matador.test.js +446 -0
  56. package/dist/core/shutdown.d.ts +78 -0
  57. package/dist/core/shutdown.d.ts.map +1 -0
  58. package/dist/core/shutdown.js +111 -0
  59. package/dist/core/shutdown.test.d.ts +2 -0
  60. package/dist/core/shutdown.test.d.ts.map +1 -0
  61. package/dist/core/shutdown.test.js +524 -0
  62. package/dist/errors/checkpoint-errors.d.ts +30 -0
  63. package/dist/errors/checkpoint-errors.d.ts.map +1 -0
  64. package/dist/errors/checkpoint-errors.js +49 -0
  65. package/dist/errors/has-description.d.ts +18 -0
  66. package/dist/errors/has-description.d.ts.map +1 -0
  67. package/dist/errors/has-description.js +9 -0
  68. package/dist/errors/index.d.ts +6 -0
  69. package/dist/errors/index.d.ts.map +1 -0
  70. package/dist/errors/index.js +22 -0
  71. package/dist/errors/matador-errors.d.ts +183 -0
  72. package/dist/errors/matador-errors.d.ts.map +1 -0
  73. package/dist/errors/matador-errors.js +376 -0
  74. package/dist/errors/retry-errors.d.ts +67 -0
  75. package/dist/errors/retry-errors.d.ts.map +1 -0
  76. package/dist/errors/retry-errors.js +108 -0
  77. package/dist/hooks/index.d.ts +4 -0
  78. package/dist/hooks/index.d.ts.map +1 -0
  79. package/dist/hooks/index.js +2 -0
  80. package/dist/hooks/safe-hooks.d.ts +34 -0
  81. package/dist/hooks/safe-hooks.d.ts.map +1 -0
  82. package/dist/hooks/safe-hooks.js +135 -0
  83. package/dist/hooks/types.d.ts +189 -0
  84. package/dist/hooks/types.d.ts.map +1 -0
  85. package/dist/hooks/types.js +9 -0
  86. package/dist/index.cjs +3156 -0
  87. package/dist/index.cjs.map +1 -0
  88. package/dist/index.d.cts +23 -0
  89. package/dist/index.d.ts +23 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +31 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/pipeline/index.d.ts +3 -0
  94. package/dist/pipeline/index.d.ts.map +1 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline.d.ts +60 -0
  97. package/dist/pipeline/pipeline.d.ts.map +1 -0
  98. package/dist/pipeline/pipeline.js +261 -0
  99. package/dist/pipeline/pipeline.test.d.ts +2 -0
  100. package/dist/pipeline/pipeline.test.d.ts.map +1 -0
  101. package/dist/pipeline/pipeline.test.js +1065 -0
  102. package/dist/retry/index.d.ts +4 -0
  103. package/dist/retry/index.d.ts.map +1 -0
  104. package/dist/retry/index.js +1 -0
  105. package/dist/retry/policy.d.ts +43 -0
  106. package/dist/retry/policy.d.ts.map +1 -0
  107. package/dist/retry/policy.js +1 -0
  108. package/dist/retry/standard-policy.d.ts +44 -0
  109. package/dist/retry/standard-policy.d.ts.map +1 -0
  110. package/dist/retry/standard-policy.js +102 -0
  111. package/dist/retry/standard-policy.test.d.ts +2 -0
  112. package/dist/retry/standard-policy.test.d.ts.map +1 -0
  113. package/dist/retry/standard-policy.test.js +190 -0
  114. package/dist/schema/index.d.ts +4 -0
  115. package/dist/schema/index.d.ts.map +1 -0
  116. package/dist/schema/index.js +2 -0
  117. package/dist/schema/registry.d.ts +63 -0
  118. package/dist/schema/registry.d.ts.map +1 -0
  119. package/dist/schema/registry.js +171 -0
  120. package/dist/schema/registry.test.d.ts +2 -0
  121. package/dist/schema/registry.test.d.ts.map +1 -0
  122. package/dist/schema/registry.test.js +278 -0
  123. package/dist/schema/types.d.ts +158 -0
  124. package/dist/schema/types.d.ts.map +1 -0
  125. package/dist/schema/types.js +74 -0
  126. package/dist/schema/types.test.d.ts +2 -0
  127. package/dist/schema/types.test.d.ts.map +1 -0
  128. package/dist/schema/types.test.js +243 -0
  129. package/dist/topology/builder.d.ts +66 -0
  130. package/dist/topology/builder.d.ts.map +1 -0
  131. package/dist/topology/builder.js +156 -0
  132. package/dist/topology/builder.test.d.ts +2 -0
  133. package/dist/topology/builder.test.d.ts.map +1 -0
  134. package/dist/topology/builder.test.js +222 -0
  135. package/dist/topology/index.d.ts +5 -0
  136. package/dist/topology/index.d.ts.map +1 -0
  137. package/dist/topology/index.js +2 -0
  138. package/dist/topology/types.d.ts +76 -0
  139. package/dist/topology/types.d.ts.map +1 -0
  140. package/dist/topology/types.js +18 -0
  141. package/dist/transport/capabilities.d.ts +66 -0
  142. package/dist/transport/capabilities.d.ts.map +1 -0
  143. package/dist/transport/capabilities.js +18 -0
  144. package/dist/transport/connection-manager.d.ts +95 -0
  145. package/dist/transport/connection-manager.d.ts.map +1 -0
  146. package/dist/transport/connection-manager.js +144 -0
  147. package/dist/transport/index.d.ts +11 -0
  148. package/dist/transport/index.d.ts.map +1 -0
  149. package/dist/transport/index.js +5 -0
  150. package/dist/transport/local/local-transport.d.ts +62 -0
  151. package/dist/transport/local/local-transport.d.ts.map +1 -0
  152. package/dist/transport/local/local-transport.js +241 -0
  153. package/dist/transport/local/local-transport.test.d.ts +2 -0
  154. package/dist/transport/local/local-transport.test.d.ts.map +1 -0
  155. package/dist/transport/local/local-transport.test.js +192 -0
  156. package/dist/transport/multi/multi-transport.d.ts +94 -0
  157. package/dist/transport/multi/multi-transport.d.ts.map +1 -0
  158. package/dist/transport/multi/multi-transport.js +184 -0
  159. package/dist/transport/multi/multi-transport.test.d.ts +2 -0
  160. package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
  161. package/dist/transport/multi/multi-transport.test.js +236 -0
  162. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
  163. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
  164. package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
  165. package/dist/transport/transport.d.ts +152 -0
  166. package/dist/transport/transport.d.ts.map +1 -0
  167. package/dist/transport/transport.js +1 -0
  168. package/dist/types/common.d.ts +41 -0
  169. package/dist/types/common.d.ts.map +1 -0
  170. package/dist/types/common.js +12 -0
  171. package/dist/types/envelope.d.ts +82 -0
  172. package/dist/types/envelope.d.ts.map +1 -0
  173. package/dist/types/envelope.js +36 -0
  174. package/dist/types/event.d.ts +96 -0
  175. package/dist/types/event.d.ts.map +1 -0
  176. package/dist/types/event.js +26 -0
  177. package/dist/types/event.test.d.ts +2 -0
  178. package/dist/types/event.test.d.ts.map +1 -0
  179. package/dist/types/event.test.js +130 -0
  180. package/dist/types/index.d.ts +9 -0
  181. package/dist/types/index.d.ts.map +1 -0
  182. package/dist/types/index.js +4 -0
  183. package/dist/types/subscriber.d.ts +207 -0
  184. package/dist/types/subscriber.d.ts.map +1 -0
  185. package/dist/types/subscriber.js +99 -0
  186. package/examples/config.ts +126 -0
  187. package/examples/event.ts +26 -0
  188. package/examples/order-event.json +19 -0
  189. package/package.json +66 -0
  190. package/src/checkpoint/context.test.ts +510 -0
  191. package/src/checkpoint/context.ts +213 -0
  192. package/src/checkpoint/index.ts +30 -0
  193. package/src/checkpoint/stores/memory.ts +47 -0
  194. package/src/checkpoint/stores/noop.ts +22 -0
  195. package/src/checkpoint/stores/stores.test.ts +177 -0
  196. package/src/checkpoint/types.ts +147 -0
  197. package/src/codec/codec.ts +42 -0
  198. package/src/codec/header-aware-codec.ts +41 -0
  199. package/src/codec/index.ts +11 -0
  200. package/src/codec/json-codec.ts +69 -0
  201. package/src/codec/rabbitmq-codec.test.ts +516 -0
  202. package/src/codec/rabbitmq-codec.ts +336 -0
  203. package/src/core/fanout.test.ts +1351 -0
  204. package/src/core/fanout.ts +184 -0
  205. package/src/core/index.ts +12 -0
  206. package/src/core/matador.test.ts +575 -0
  207. package/src/core/matador.ts +344 -0
  208. package/src/core/shutdown.test.ts +853 -0
  209. package/src/core/shutdown.ts +165 -0
  210. package/src/errors/checkpoint-errors.ts +62 -0
  211. package/src/errors/has-description.ts +25 -0
  212. package/src/errors/index.ts +57 -0
  213. package/src/errors/matador-errors.ts +477 -0
  214. package/src/errors/retry-errors.ts +134 -0
  215. package/src/hooks/index.ts +15 -0
  216. package/src/hooks/safe-hooks.ts +223 -0
  217. package/src/hooks/types.ts +248 -0
  218. package/src/index.ts +231 -0
  219. package/src/pipeline/index.ts +2 -0
  220. package/src/pipeline/pipeline.test.ts +1317 -0
  221. package/src/pipeline/pipeline.ts +371 -0
  222. package/src/retry/index.ts +4 -0
  223. package/src/retry/policy.ts +46 -0
  224. package/src/retry/standard-policy.test.ts +282 -0
  225. package/src/retry/standard-policy.ts +156 -0
  226. package/src/schema/index.ts +16 -0
  227. package/src/schema/registry.test.ts +339 -0
  228. package/src/schema/registry.ts +226 -0
  229. package/src/schema/types.test.ts +281 -0
  230. package/src/schema/types.ts +217 -0
  231. package/src/topology/builder.test.ts +275 -0
  232. package/src/topology/builder.ts +199 -0
  233. package/src/topology/index.ts +15 -0
  234. package/src/topology/types.ts +109 -0
  235. package/src/transport/capabilities.ts +88 -0
  236. package/src/transport/connection-manager.ts +218 -0
  237. package/src/transport/index.ts +42 -0
  238. package/src/transport/local/local-transport.test.ts +262 -0
  239. package/src/transport/local/local-transport.ts +327 -0
  240. package/src/transport/multi/multi-transport.test.ts +320 -0
  241. package/src/transport/multi/multi-transport.ts +294 -0
  242. package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
  243. package/src/transport/transport.ts +200 -0
  244. package/src/types/common.ts +53 -0
  245. package/src/types/envelope.ts +152 -0
  246. package/src/types/event.test.ts +157 -0
  247. package/src/types/event.ts +118 -0
  248. package/src/types/index.ts +52 -0
  249. package/src/types/subscriber.ts +310 -0
  250. package/test/e2e/multi-transport.e2e.test.ts +236 -0
  251. package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
  252. package/test/e2e/transport-compliance.e2e.test.ts +505 -0
  253. package/test/integration/matador.integration.test.ts +634 -0
  254. package/tsconfig.json +29 -0
  255. package/tsconfig.tsbuildinfo +1 -0
  256. package/tsup.config.ts +13 -0
@@ -0,0 +1,156 @@
1
+ import {
2
+ IdempotentMessageCannotRetryError,
3
+ MessageMaybePoisonedError,
4
+ isAssertionError,
5
+ isDoRetry,
6
+ isDontRetry,
7
+ } from '../errors/index.js';
8
+ import type { RetryContext, RetryDecision, RetryPolicy } from './policy.js';
9
+
10
+ /**
11
+ * Configuration for the standard retry policy.
12
+ */
13
+ export interface StandardRetryPolicyConfig {
14
+ /** Maximum number of attempts before dead-lettering */
15
+ readonly maxAttempts: number;
16
+
17
+ /** Base delay between retries in milliseconds */
18
+ readonly baseDelay: number;
19
+
20
+ /** Maximum delay between retries in milliseconds */
21
+ readonly maxDelay: number;
22
+
23
+ /** Multiplier for exponential backoff */
24
+ readonly backoffMultiplier: number;
25
+
26
+ /**
27
+ * Maximum native delivery count before considering message poisoned.
28
+ * This prevents crash loops from messages that crash the worker.
29
+ * Poison messages are sent directly to the dead-letter queue.
30
+ * Default: 5
31
+ */
32
+ readonly maxDeliveries: number;
33
+ }
34
+
35
+ /**
36
+ * Default configuration values.
37
+ */
38
+ export const defaultRetryConfig: StandardRetryPolicyConfig = {
39
+ maxAttempts: 3,
40
+ baseDelay: 1000,
41
+ maxDelay: 300000, // 5 minutes
42
+ backoffMultiplier: 2,
43
+ maxDeliveries: 5,
44
+ };
45
+
46
+ /**
47
+ * Standard retry policy implementing Matador v1 behavior.
48
+ *
49
+ * Decision logic (in priority order):
50
+ * 1. Poison message → dead-letter (prevent crash loops)
51
+ * 2. EventAssertionError → dead-letter (never retry)
52
+ * 3. DontRetry → dead-letter (explicit no-retry)
53
+ * 4. DoRetry → retry if under max attempts
54
+ * 5. Max attempts exceeded → dead-letter
55
+ * 6. Non-idempotent subscriber on redelivery → dead-letter
56
+ * 7. Default → retry with exponential backoff
57
+ */
58
+ export class StandardRetryPolicy implements RetryPolicy {
59
+ private readonly config: StandardRetryPolicyConfig;
60
+
61
+ constructor(config: Partial<StandardRetryPolicyConfig> = {}) {
62
+ this.config = { ...defaultRetryConfig, ...config };
63
+ }
64
+
65
+ shouldRetry(context: RetryContext): RetryDecision {
66
+ const { envelope, error, subscriber, receipt } = context;
67
+ const errorMessage = error.message;
68
+
69
+ // 1. Poison message detection - prevent crash loops
70
+ if (receipt.deliveryCount >= this.config.maxDeliveries) {
71
+ const poisonError = new MessageMaybePoisonedError(
72
+ envelope.id,
73
+ receipt.deliveryCount,
74
+ this.config.maxDeliveries,
75
+ );
76
+ return {
77
+ action: 'dead-letter',
78
+ queue: 'undeliverable',
79
+ reason: poisonError.message,
80
+ };
81
+ }
82
+
83
+ // 2. Assertion errors never retry
84
+ if (isAssertionError(error)) {
85
+ return {
86
+ action: 'dead-letter',
87
+ queue: 'undeliverable',
88
+ reason: `assertion error: ${errorMessage}`,
89
+ };
90
+ }
91
+
92
+ // 3. Explicit no-retry
93
+ if (isDontRetry(error)) {
94
+ return {
95
+ action: 'dead-letter',
96
+ queue: 'undeliverable',
97
+ reason: errorMessage,
98
+ };
99
+ }
100
+
101
+ // 4. Explicit retry request
102
+ if (isDoRetry(error)) {
103
+ if (receipt.attemptNumber >= this.config.maxAttempts) {
104
+ return {
105
+ action: 'dead-letter',
106
+ queue: 'undeliverable',
107
+ reason: `max attempts exceeded (${this.config.maxAttempts}) with forced retry`,
108
+ };
109
+ }
110
+ return {
111
+ action: 'retry',
112
+ delay: this.getDelay(context),
113
+ };
114
+ }
115
+
116
+ // 5. Max attempts exceeded
117
+ if (receipt.attemptNumber >= this.config.maxAttempts) {
118
+ return {
119
+ action: 'dead-letter',
120
+ queue: 'undeliverable',
121
+ reason: `max attempts exceeded (${this.config.maxAttempts})`,
122
+ };
123
+ }
124
+
125
+ // 6. Non-idempotent subscriber on redelivery
126
+ // 'no' and 'unknown' are treated the same (safer default)
127
+ if (
128
+ receipt.redelivered &&
129
+ (subscriber.idempotent === 'no' || subscriber.idempotent === 'unknown')
130
+ ) {
131
+ const idempotentError = new IdempotentMessageCannotRetryError(
132
+ envelope.id,
133
+ subscriber.name,
134
+ );
135
+ return {
136
+ action: 'dead-letter',
137
+ queue: 'undeliverable',
138
+ reason: idempotentError.message,
139
+ };
140
+ }
141
+
142
+ // 7. Default: retry with backoff
143
+ return {
144
+ action: 'retry',
145
+ delay: this.getDelay(context),
146
+ };
147
+ }
148
+
149
+ getDelay(context: RetryContext): number {
150
+ const attempt = context.receipt.attemptNumber;
151
+ const delay =
152
+ this.config.baseDelay *
153
+ Math.pow(this.config.backoffMultiplier, attempt - 1);
154
+ return Math.min(delay, this.config.maxDelay);
155
+ }
156
+ }
@@ -0,0 +1,16 @@
1
+ export type {
2
+ AnyEventClass,
3
+ MatadorSchema,
4
+ RegisterOptions,
5
+ RuntimeSchemaEntry,
6
+ RuntimeSchemaEntryTuple,
7
+ SchemaEntry,
8
+ SchemaEntryTuple,
9
+ SchemaIssue,
10
+ SchemaPlugin,
11
+ SchemaValidationResult,
12
+ } from './types.js';
13
+
14
+ export { bind, installPlugins, isSchemaEntryTuple } from './types.js';
15
+
16
+ export { SchemaError, SchemaRegistry } from './registry.js';
@@ -0,0 +1,339 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test';
2
+ import { MatadorEvent, createSubscriber } from '../types/index.js';
3
+ import { SchemaError, SchemaRegistry } from './registry.js';
4
+
5
+ class TestEvent extends MatadorEvent {
6
+ static readonly key = 'test.event';
7
+ static readonly description = 'A test event';
8
+
9
+ constructor(public data: { id: string }) {
10
+ super();
11
+ }
12
+ }
13
+
14
+ class AliasedEvent extends MatadorEvent {
15
+ static readonly key = 'user.created.v2';
16
+ static readonly description = 'User created event with aliases';
17
+ static readonly aliases = ['user.created', 'user.created.v1'];
18
+
19
+ constructor(public data: { name: string }) {
20
+ super();
21
+ }
22
+ }
23
+
24
+ describe('SchemaRegistry', () => {
25
+ let registry: SchemaRegistry;
26
+
27
+ beforeEach(() => {
28
+ registry = new SchemaRegistry();
29
+ });
30
+
31
+ describe('register', () => {
32
+ it('should register an event with subscribers', () => {
33
+ const subscriber = createSubscriber({
34
+ name: 'test-subscriber',
35
+ description: 'Test subscriber',
36
+ callback: async () => {},
37
+ });
38
+
39
+ registry.register(TestEvent, [subscriber]);
40
+
41
+ expect(registry.hasEvent('test.event')).toBe(true);
42
+ expect(registry.getSubscribers('test.event')).toHaveLength(1);
43
+ });
44
+
45
+ it('should throw when registering duplicate event without override', () => {
46
+ const subscriber = createSubscriber({
47
+ name: 'test-subscriber',
48
+ description: 'Test subscriber',
49
+ callback: async () => {},
50
+ });
51
+
52
+ registry.register(TestEvent, [subscriber]);
53
+
54
+ expect(() => registry.register(TestEvent, [subscriber])).toThrow(
55
+ SchemaError,
56
+ );
57
+ });
58
+
59
+ it('should allow override when option is set', () => {
60
+ const subscriber1 = createSubscriber({
61
+ name: 'sub-1',
62
+ description: 'Subscriber 1',
63
+ callback: async () => {},
64
+ });
65
+ const subscriber2 = createSubscriber({
66
+ name: 'sub-2',
67
+ description: 'Subscriber 2',
68
+ callback: async () => {},
69
+ });
70
+
71
+ registry.register(TestEvent, [subscriber1]);
72
+ registry.register(TestEvent, [subscriber2], { override: true });
73
+
74
+ const subscribers = registry.getSubscribers('test.event');
75
+ expect(subscribers).toHaveLength(1);
76
+ expect(subscribers[0]?.name).toBe('sub-2');
77
+ });
78
+
79
+ it('should register aliases', () => {
80
+ const subscriber = createSubscriber({
81
+ name: 'test-subscriber',
82
+ description: 'Test subscriber',
83
+ callback: async () => {},
84
+ });
85
+
86
+ registry.register(AliasedEvent, [subscriber]);
87
+
88
+ expect(registry.hasEvent('user.created.v2')).toBe(true);
89
+ expect(registry.hasEvent('user.created')).toBe(true);
90
+ expect(registry.hasEvent('user.created.v1')).toBe(true);
91
+ });
92
+
93
+ it('should throw on duplicate aliases without override', () => {
94
+ class EventWithSameAlias extends MatadorEvent {
95
+ static readonly key = 'another.event';
96
+ static readonly description = 'Another event';
97
+ static readonly aliases = ['user.created']; // conflicts with AliasedEvent
98
+
99
+ constructor(public data: { x: number }) {
100
+ super();
101
+ }
102
+ }
103
+
104
+ const sub1 = createSubscriber({
105
+ name: 'sub-1',
106
+ description: 'Subscriber 1',
107
+ callback: async () => {},
108
+ });
109
+ const sub2 = createSubscriber({
110
+ name: 'sub-2',
111
+ description: 'Subscriber 2',
112
+ callback: async () => {},
113
+ });
114
+
115
+ registry.register(AliasedEvent, [sub1]);
116
+
117
+ expect(() => registry.register(EventWithSameAlias, [sub2])).toThrow(
118
+ 'Alias "user.created" is already registered',
119
+ );
120
+ });
121
+ });
122
+
123
+ describe('getEventClass', () => {
124
+ it('should return event class by key', () => {
125
+ const subscriber = createSubscriber({
126
+ name: 'test-subscriber',
127
+ description: 'Test subscriber',
128
+ callback: async () => {},
129
+ });
130
+ registry.register(TestEvent, [subscriber]);
131
+
132
+ const eventClass = registry.getEventClass('test.event');
133
+ expect(eventClass).toBeDefined();
134
+ expect(eventClass?.key).toBe('test.event');
135
+ });
136
+
137
+ it('should return event class by alias', () => {
138
+ const subscriber = createSubscriber({
139
+ name: 'test-subscriber',
140
+ description: 'Test subscriber',
141
+ callback: async () => {},
142
+ });
143
+ registry.register(AliasedEvent, [subscriber]);
144
+
145
+ const eventClass = registry.getEventClass('user.created');
146
+ expect(eventClass).toBeDefined();
147
+ expect(eventClass?.key).toBe('user.created.v2');
148
+ });
149
+
150
+ it('should return undefined for unknown key', () => {
151
+ expect(registry.getEventClass('unknown.event')).toBeUndefined();
152
+ });
153
+ });
154
+
155
+ describe('getSubscribers', () => {
156
+ it('should return all subscribers for an event', () => {
157
+ const sub1 = createSubscriber({
158
+ name: 'sub-1',
159
+ description: 'Subscriber 1',
160
+ callback: async () => {},
161
+ });
162
+ const sub2 = createSubscriber({
163
+ name: 'sub-2',
164
+ description: 'Subscriber 2',
165
+ callback: async () => {},
166
+ });
167
+
168
+ registry.register(TestEvent, [sub1, sub2]);
169
+
170
+ const subscribers = registry.getSubscribers('test.event');
171
+ expect(subscribers).toHaveLength(2);
172
+ expect(subscribers[0]?.name).toBe('sub-1');
173
+ expect(subscribers[1]?.name).toBe('sub-2');
174
+ });
175
+
176
+ it('should return empty array for unknown event', () => {
177
+ expect(registry.getSubscribers('unknown.event')).toEqual([]);
178
+ });
179
+ });
180
+
181
+ describe('getSubscriber', () => {
182
+ it('should return specific subscriber by name', () => {
183
+ const sub1 = createSubscriber({
184
+ name: 'sub-1',
185
+ description: 'Subscriber 1',
186
+ callback: async () => {},
187
+ });
188
+ const sub2 = createSubscriber({
189
+ name: 'sub-2',
190
+ description: 'Subscriber 2',
191
+ callback: async () => {},
192
+ });
193
+
194
+ registry.register(TestEvent, [sub1, sub2]);
195
+
196
+ const subscriber = registry.getSubscriber('test.event', 'sub-2');
197
+ expect(subscriber?.name).toBe('sub-2');
198
+ });
199
+
200
+ it('should return undefined for unknown subscriber', () => {
201
+ const sub = createSubscriber({
202
+ name: 'sub-1',
203
+ description: 'Subscriber 1',
204
+ callback: async () => {},
205
+ });
206
+ registry.register(TestEvent, [sub]);
207
+
208
+ expect(registry.getSubscriber('test.event', 'unknown')).toBeUndefined();
209
+ });
210
+ });
211
+
212
+ describe('getSubscriberDefinition', () => {
213
+ it('should return subscriber definition for pipeline', () => {
214
+ const sub = createSubscriber({
215
+ name: 'sub-1',
216
+ description: 'Subscriber 1',
217
+ callback: async () => {},
218
+ idempotent: 'yes',
219
+ importance: 'must-investigate',
220
+ targetQueue: 'custom-queue',
221
+ });
222
+
223
+ registry.register(TestEvent, [sub]);
224
+
225
+ const definition = registry.getSubscriberDefinition(
226
+ 'test.event',
227
+ 'sub-1',
228
+ );
229
+ expect(definition).toEqual({
230
+ name: 'sub-1',
231
+ description: 'Subscriber 1',
232
+ idempotent: 'yes',
233
+ importance: 'must-investigate',
234
+ targetQueue: 'custom-queue',
235
+ });
236
+ });
237
+
238
+ it('should return undefined for unknown subscriber', () => {
239
+ expect(
240
+ registry.getSubscriberDefinition('unknown', 'unknown'),
241
+ ).toBeUndefined();
242
+ });
243
+ });
244
+
245
+ describe('getExecutableSubscriber', () => {
246
+ it('should return subscriber with callback', () => {
247
+ const callback = async () => {};
248
+ const sub = createSubscriber({
249
+ name: 'sub-1',
250
+ description: 'Subscriber 1',
251
+ callback,
252
+ });
253
+
254
+ registry.register(TestEvent, [sub]);
255
+
256
+ const executable = registry.getExecutableSubscriber(
257
+ 'test.event',
258
+ 'sub-1',
259
+ );
260
+ expect(executable).toBeDefined();
261
+ expect(executable?.callback).toBe(callback);
262
+ });
263
+ });
264
+
265
+ describe('getEventKeys', () => {
266
+ it('should return all registered event keys', () => {
267
+ const sub1 = createSubscriber({
268
+ name: 'sub-1',
269
+ description: 'Subscriber 1',
270
+ callback: async () => {},
271
+ });
272
+ const sub2 = createSubscriber({
273
+ name: 'sub-2',
274
+ description: 'Subscriber 2',
275
+ callback: async () => {},
276
+ });
277
+
278
+ registry.register(TestEvent, [sub1]);
279
+ registry.register(AliasedEvent, [sub2]);
280
+
281
+ const keys = registry.getEventKeys();
282
+ expect(keys).toContain('test.event');
283
+ expect(keys).toContain('user.created.v2');
284
+ expect(keys).not.toContain('user.created'); // aliases not included
285
+ });
286
+ });
287
+
288
+ describe('validate', () => {
289
+ it('should pass validation for valid schema', () => {
290
+ const sub = createSubscriber({
291
+ name: 'sub-1',
292
+ description: 'Subscriber 1',
293
+ callback: async () => {},
294
+ });
295
+ registry.register(TestEvent, [sub]);
296
+
297
+ const result = registry.validate();
298
+ expect(result.valid).toBe(true);
299
+ expect(result.issues).toHaveLength(0);
300
+ });
301
+
302
+ it('should detect duplicate subscriber names', () => {
303
+ const sub1 = createSubscriber({
304
+ name: 'duplicate-name',
305
+ description: 'Duplicate subscriber',
306
+ callback: async () => {},
307
+ });
308
+ const sub2 = createSubscriber({
309
+ name: 'duplicate-name',
310
+ description: 'Duplicate subscriber',
311
+ callback: async () => {},
312
+ });
313
+
314
+ registry.register(TestEvent, [sub1, sub2]);
315
+
316
+ const result = registry.validate();
317
+ expect(result.valid).toBe(false);
318
+ expect(result.issues).toHaveLength(1);
319
+ expect(result.issues[0]?.severity).toBe('error');
320
+ expect(result.issues[0]?.message).toContain('Duplicate subscriber name');
321
+ });
322
+ });
323
+
324
+ describe('clear', () => {
325
+ it('should remove all entries', () => {
326
+ const sub = createSubscriber({
327
+ name: 'sub-1',
328
+ description: 'Subscriber 1',
329
+ callback: async () => {},
330
+ });
331
+ registry.register(TestEvent, [sub]);
332
+
333
+ registry.clear();
334
+
335
+ expect(registry.hasEvent('test.event')).toBe(false);
336
+ expect(registry.getEventKeys()).toHaveLength(0);
337
+ });
338
+ });
339
+ });