@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,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
|
+
});
|