@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,52 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
DeliveryMode,
|
|
3
|
+
Idempotency,
|
|
4
|
+
Importance,
|
|
5
|
+
ValidationError,
|
|
6
|
+
ValidationResult,
|
|
7
|
+
} from './common.js';
|
|
8
|
+
export { invalidResult, validResult } from './common.js';
|
|
9
|
+
|
|
10
|
+
export type { CreateEnvelopeOptions, Docket, Envelope } from './envelope.js';
|
|
11
|
+
export { createEnvelope } from './envelope.js';
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
Event,
|
|
15
|
+
EventClass,
|
|
16
|
+
EventData,
|
|
17
|
+
EventKey,
|
|
18
|
+
EventOptions,
|
|
19
|
+
EventStatic,
|
|
20
|
+
JsonPrimitive,
|
|
21
|
+
JsonRecord,
|
|
22
|
+
JsonValue,
|
|
23
|
+
} from './event.js';
|
|
24
|
+
export { MatadorEvent } from './event.js';
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
AnySubscriber,
|
|
28
|
+
BaseSubscriberOptions,
|
|
29
|
+
CreateResumableSubscriberInput,
|
|
30
|
+
CreateStandardSubscriberInput,
|
|
31
|
+
CreateSubscriberInput,
|
|
32
|
+
EnvelopeOf,
|
|
33
|
+
ResumableCallback,
|
|
34
|
+
ResumableSubscriber,
|
|
35
|
+
ResumableSubscriberOptions,
|
|
36
|
+
StandardCallback,
|
|
37
|
+
StandardSubscriber,
|
|
38
|
+
StandardSubscriberOptions,
|
|
39
|
+
Subscriber,
|
|
40
|
+
SubscriberCallback,
|
|
41
|
+
SubscriberDefinition,
|
|
42
|
+
SubscriberOptions,
|
|
43
|
+
SubscriberStub,
|
|
44
|
+
} from './subscriber.js';
|
|
45
|
+
export {
|
|
46
|
+
createSubscriber,
|
|
47
|
+
createSubscriberStub,
|
|
48
|
+
isResumableSubscriber,
|
|
49
|
+
isStandardSubscriber,
|
|
50
|
+
isSubscriber,
|
|
51
|
+
isSubscriberStub,
|
|
52
|
+
} from './subscriber.js';
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { SubscriberContext } from '../checkpoint/index.js';
|
|
2
|
+
import type { Idempotency, Importance } from './common.js';
|
|
3
|
+
import type { Envelope } from './envelope.js';
|
|
4
|
+
import type { MatadorEvent } from './event.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper type to get the envelope type for a subscriber callback.
|
|
8
|
+
* Extracts the data type from a MatadorEvent and wraps it in an Envelope.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* async callback(envelope: EnvelopeOf<MyEvent>) {
|
|
12
|
+
* console.log(envelope.data.someField); // Type-safe access
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export type EnvelopeOf<T extends MatadorEvent> = Envelope<T['data']>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Callback function executed when an event is received (standard subscribers).
|
|
19
|
+
* Receives the full envelope containing id, data, and docket.
|
|
20
|
+
*/
|
|
21
|
+
export type StandardCallback<T = unknown> = (
|
|
22
|
+
envelope: Envelope<T>,
|
|
23
|
+
) => Promise<void> | void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback function for resumable subscribers.
|
|
27
|
+
* Receives the envelope and a SubscriberContext with io() for checkpointed operations.
|
|
28
|
+
*/
|
|
29
|
+
export type ResumableCallback<T = unknown> = (
|
|
30
|
+
envelope: Envelope<T>,
|
|
31
|
+
context: SubscriberContext,
|
|
32
|
+
) => Promise<void> | void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Callback function executed when an event is received.
|
|
36
|
+
* @deprecated Use StandardCallback or ResumableCallback instead.
|
|
37
|
+
*/
|
|
38
|
+
export type SubscriberCallback<T = unknown> = StandardCallback<T>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Base configuration options shared by all subscriber types.
|
|
42
|
+
*/
|
|
43
|
+
export interface BaseSubscriberOptions {
|
|
44
|
+
/** Human-readable description of what this subscriber does */
|
|
45
|
+
readonly description: string;
|
|
46
|
+
|
|
47
|
+
/** Route this subscriber's events to a specific queue */
|
|
48
|
+
readonly targetQueue?: string | undefined;
|
|
49
|
+
|
|
50
|
+
/** Importance level for monitoring and alerting */
|
|
51
|
+
readonly importance?: Importance | undefined;
|
|
52
|
+
|
|
53
|
+
/** Feature flag function to conditionally enable/disable the subscriber */
|
|
54
|
+
readonly enabled?: (() => boolean | Promise<boolean>) | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Options for standard (non-resumable) subscribers.
|
|
59
|
+
*/
|
|
60
|
+
export interface StandardSubscriberOptions extends BaseSubscriberOptions {
|
|
61
|
+
/** Idempotency declaration for retry handling (non-resumable) */
|
|
62
|
+
readonly idempotent?: 'yes' | 'no' | 'unknown' | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Options for resumable subscribers that use io() for checkpointed operations.
|
|
67
|
+
*/
|
|
68
|
+
export interface ResumableSubscriberOptions extends BaseSubscriberOptions {
|
|
69
|
+
/** Must be 'resumable' to enable checkpoint-based idempotency */
|
|
70
|
+
readonly idempotent: 'resumable';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configuration options for a subscriber.
|
|
75
|
+
* Discriminated union based on idempotent value.
|
|
76
|
+
*/
|
|
77
|
+
export type SubscriberOptions =
|
|
78
|
+
| StandardSubscriberOptions
|
|
79
|
+
| ResumableSubscriberOptions;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Standard subscriber definition with standard callback.
|
|
83
|
+
*/
|
|
84
|
+
export interface StandardSubscriber<T extends MatadorEvent>
|
|
85
|
+
extends StandardSubscriberOptions {
|
|
86
|
+
/** Human-readable name for the subscriber */
|
|
87
|
+
readonly name: string;
|
|
88
|
+
|
|
89
|
+
/** Callback function to execute when event is received */
|
|
90
|
+
readonly callback: StandardCallback<T['data']>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resumable subscriber definition with resumable callback.
|
|
95
|
+
*/
|
|
96
|
+
export interface ResumableSubscriber<T extends MatadorEvent>
|
|
97
|
+
extends ResumableSubscriberOptions {
|
|
98
|
+
/** Human-readable name for the subscriber */
|
|
99
|
+
readonly name: string;
|
|
100
|
+
|
|
101
|
+
/** Callback function with SubscriberContext for checkpointed operations */
|
|
102
|
+
readonly callback: ResumableCallback<T['data']>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Full subscriber definition with callback (either standard or resumable).
|
|
107
|
+
*/
|
|
108
|
+
export type Subscriber<T extends MatadorEvent> =
|
|
109
|
+
| StandardSubscriber<T>
|
|
110
|
+
| ResumableSubscriber<T>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Subscriber stub for multi-codebase scenarios where subscriber implementation
|
|
114
|
+
* is in a remote service. Declares the subscriber contract without providing
|
|
115
|
+
* the callback.
|
|
116
|
+
*/
|
|
117
|
+
export interface SubscriberStub extends StandardSubscriberOptions {
|
|
118
|
+
/** Human-readable name for the subscriber */
|
|
119
|
+
readonly name: string;
|
|
120
|
+
|
|
121
|
+
/** Indicates this is a stub without implementation */
|
|
122
|
+
readonly isStub: true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Union type for any subscriber definition (full or stub).
|
|
127
|
+
* This is the type-erased version for use in collections and schema.
|
|
128
|
+
* Uses `any` because Subscriber<T> is contravariant in T (callback parameter),
|
|
129
|
+
* making it impossible to assign Subscriber<SpecificEvent> to Subscriber<MatadorEvent<unknown>>.
|
|
130
|
+
*/
|
|
131
|
+
// biome-ignore lint/suspicious/noExplicitAny: Required for variance compatibility in heterogeneous collections
|
|
132
|
+
export type AnySubscriber = Subscriber<MatadorEvent<any>> | SubscriberStub;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Type guard to check if a subscriber is a stub.
|
|
136
|
+
*/
|
|
137
|
+
export function isSubscriberStub(
|
|
138
|
+
subscriber: AnySubscriber,
|
|
139
|
+
): subscriber is SubscriberStub {
|
|
140
|
+
return 'isStub' in subscriber && subscriber.isStub === true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Type guard to check if a subscriber has a callback implementation.
|
|
145
|
+
*/
|
|
146
|
+
export function isSubscriber(
|
|
147
|
+
subscriber: AnySubscriber,
|
|
148
|
+
// biome-ignore lint/suspicious/noExplicitAny: Required for variance compatibility
|
|
149
|
+
): subscriber is Subscriber<MatadorEvent<any>> {
|
|
150
|
+
return 'callback' in subscriber && typeof subscriber.callback === 'function';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Type guard to check if a subscriber is resumable.
|
|
155
|
+
*/
|
|
156
|
+
export function isResumableSubscriber(
|
|
157
|
+
subscriber: AnySubscriber,
|
|
158
|
+
// biome-ignore lint/suspicious/noExplicitAny: Required for variance compatibility
|
|
159
|
+
): subscriber is ResumableSubscriber<MatadorEvent<any>> {
|
|
160
|
+
return isSubscriber(subscriber) && subscriber.idempotent === 'resumable';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Type guard to check if a subscriber is a standard (non-resumable) subscriber.
|
|
165
|
+
*/
|
|
166
|
+
export function isStandardSubscriber(
|
|
167
|
+
subscriber: AnySubscriber,
|
|
168
|
+
// biome-ignore lint/suspicious/noExplicitAny: Required for variance compatibility
|
|
169
|
+
): subscriber is StandardSubscriber<MatadorEvent<any>> {
|
|
170
|
+
return isSubscriber(subscriber) && subscriber.idempotent !== 'resumable';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Input options for createSubscriber with standard callback.
|
|
175
|
+
*/
|
|
176
|
+
export interface CreateStandardSubscriberInput<T extends MatadorEvent> {
|
|
177
|
+
readonly name: string;
|
|
178
|
+
readonly description: string;
|
|
179
|
+
readonly callback: StandardCallback<T['data']>;
|
|
180
|
+
readonly idempotent?: 'yes' | 'no' | 'unknown' | undefined;
|
|
181
|
+
readonly importance?: Importance | undefined;
|
|
182
|
+
readonly targetQueue?: string | undefined;
|
|
183
|
+
readonly enabled?: (() => boolean | Promise<boolean>) | undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Input options for createSubscriber with resumable callback.
|
|
188
|
+
*/
|
|
189
|
+
export interface CreateResumableSubscriberInput<T extends MatadorEvent> {
|
|
190
|
+
readonly name: string;
|
|
191
|
+
readonly description: string;
|
|
192
|
+
readonly callback: ResumableCallback<T['data']>;
|
|
193
|
+
readonly idempotent: 'resumable';
|
|
194
|
+
readonly importance?: Importance | undefined;
|
|
195
|
+
readonly targetQueue?: string | undefined;
|
|
196
|
+
readonly enabled?: (() => boolean | Promise<boolean>) | undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Input options for createSubscriber (discriminated union).
|
|
201
|
+
*/
|
|
202
|
+
export type CreateSubscriberInput<T extends MatadorEvent> =
|
|
203
|
+
| CreateStandardSubscriberInput<T>
|
|
204
|
+
| CreateResumableSubscriberInput<T>;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Creates a subscriber definition.
|
|
208
|
+
*
|
|
209
|
+
* @example Standard subscriber
|
|
210
|
+
* ```typescript
|
|
211
|
+
* const subscriber = createSubscriber<MyEvent>({
|
|
212
|
+
* name: 'my-subscriber',
|
|
213
|
+
* description: 'Handles MyEvent by logging the data',
|
|
214
|
+
* callback: async (envelope) => {
|
|
215
|
+
* console.log(envelope.data);
|
|
216
|
+
* },
|
|
217
|
+
* });
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @example Resumable subscriber with io()
|
|
221
|
+
* ```typescript
|
|
222
|
+
* const subscriber = createSubscriber<MyEvent>({
|
|
223
|
+
* name: 'my-resumable-subscriber',
|
|
224
|
+
* description: 'Processes MyEvent with checkpoint-based idempotency',
|
|
225
|
+
* idempotent: 'resumable',
|
|
226
|
+
* callback: async (envelope, { io }) => {
|
|
227
|
+
* await io('step-1', () => doSomething());
|
|
228
|
+
* },
|
|
229
|
+
* });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
export function createSubscriber<T extends MatadorEvent>(
|
|
233
|
+
input: CreateSubscriberInput<T>,
|
|
234
|
+
): Subscriber<T> {
|
|
235
|
+
const base = {
|
|
236
|
+
name: input.name,
|
|
237
|
+
description: input.description,
|
|
238
|
+
callback: input.callback,
|
|
239
|
+
importance: input.importance ?? 'should-investigate',
|
|
240
|
+
...(input.targetQueue !== undefined && {
|
|
241
|
+
targetQueue: input.targetQueue,
|
|
242
|
+
}),
|
|
243
|
+
...(input.enabled !== undefined && { enabled: input.enabled }),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (input.idempotent === 'resumable') {
|
|
247
|
+
return {
|
|
248
|
+
...base,
|
|
249
|
+
idempotent: 'resumable',
|
|
250
|
+
callback: input.callback,
|
|
251
|
+
} as ResumableSubscriber<T>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
...base,
|
|
256
|
+
idempotent: input.idempotent ?? 'unknown',
|
|
257
|
+
callback: input.callback,
|
|
258
|
+
} as StandardSubscriber<T>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Input options for createSubscriberStub.
|
|
263
|
+
*/
|
|
264
|
+
export interface CreateSubscriberStubInput {
|
|
265
|
+
readonly name: string;
|
|
266
|
+
readonly description: string;
|
|
267
|
+
readonly idempotent?: 'yes' | 'no' | 'unknown' | undefined;
|
|
268
|
+
readonly importance?: Importance | undefined;
|
|
269
|
+
readonly targetQueue?: string | undefined;
|
|
270
|
+
readonly enabled?: (() => boolean | Promise<boolean>) | undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Creates a subscriber stub for remote implementations.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* const stub = createSubscriberStub({
|
|
279
|
+
* name: 'remote-analytics',
|
|
280
|
+
* description: 'Sends events to remote analytics service',
|
|
281
|
+
* targetQueue: 'analytics-worker',
|
|
282
|
+
* });
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
export function createSubscriberStub(
|
|
286
|
+
input: CreateSubscriberStubInput,
|
|
287
|
+
): SubscriberStub {
|
|
288
|
+
return {
|
|
289
|
+
name: input.name,
|
|
290
|
+
description: input.description,
|
|
291
|
+
isStub: true,
|
|
292
|
+
idempotent: input.idempotent ?? 'unknown',
|
|
293
|
+
importance: input.importance ?? 'should-investigate',
|
|
294
|
+
...(input.targetQueue !== undefined && {
|
|
295
|
+
targetQueue: input.targetQueue,
|
|
296
|
+
}),
|
|
297
|
+
...(input.enabled !== undefined && { enabled: input.enabled }),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Definition interface used by the pipeline (excludes event class reference).
|
|
303
|
+
*/
|
|
304
|
+
export interface SubscriberDefinition {
|
|
305
|
+
readonly name: string;
|
|
306
|
+
readonly description: string;
|
|
307
|
+
readonly idempotent: Idempotency;
|
|
308
|
+
readonly importance: Importance;
|
|
309
|
+
readonly targetQueue?: string | undefined;
|
|
310
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeAll,
|
|
5
|
+
beforeEach,
|
|
6
|
+
describe,
|
|
7
|
+
expect,
|
|
8
|
+
it,
|
|
9
|
+
} from 'bun:test';
|
|
10
|
+
import {
|
|
11
|
+
RabbitMQContainer,
|
|
12
|
+
type StartedRabbitMQContainer,
|
|
13
|
+
} from '@testcontainers/rabbitmq';
|
|
14
|
+
import type { TransportFallbackContext } from '../../src/hooks/index.js';
|
|
15
|
+
import type { Subscription } from '../../src/transport/index.js';
|
|
16
|
+
import { LocalTransport } from '../../src/transport/local/local-transport.js';
|
|
17
|
+
import { MultiTransport } from '../../src/transport/multi/multi-transport.js';
|
|
18
|
+
import { RabbitMQTransport } from '../../src/transport/rabbitmq/rabbitmq-transport.js';
|
|
19
|
+
import {
|
|
20
|
+
createTestEnvelope,
|
|
21
|
+
createTestTopology,
|
|
22
|
+
} from './transport-compliance.e2e.test.js';
|
|
23
|
+
|
|
24
|
+
// Skip tests if docker is not available
|
|
25
|
+
const SKIP_E2E = process.env.SKIP_E2E_TESTS === 'true';
|
|
26
|
+
|
|
27
|
+
describe.skipIf(SKIP_E2E)('MultiTransport E2E', () => {
|
|
28
|
+
let container: StartedRabbitMQContainer;
|
|
29
|
+
let connectionUrl: string;
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
container = await new RabbitMQContainer('rabbitmq:3.13-management')
|
|
33
|
+
.withExposedPorts(5672, 15672)
|
|
34
|
+
.start();
|
|
35
|
+
|
|
36
|
+
connectionUrl = container.getAmqpUrl();
|
|
37
|
+
console.log(`RabbitMQ container started at ${connectionUrl}`);
|
|
38
|
+
}, 120_000);
|
|
39
|
+
|
|
40
|
+
afterAll(async () => {
|
|
41
|
+
if (container) {
|
|
42
|
+
await container.stop();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('fallback to local transport when RabbitMQ fails', () => {
|
|
47
|
+
let rabbitTransport: RabbitMQTransport;
|
|
48
|
+
let localTransport: LocalTransport;
|
|
49
|
+
let multiTransport: MultiTransport;
|
|
50
|
+
let subscriptions: Subscription[];
|
|
51
|
+
let fallbackEvents: TransportFallbackContext[];
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
subscriptions = [];
|
|
55
|
+
fallbackEvents = [];
|
|
56
|
+
|
|
57
|
+
rabbitTransport = new RabbitMQTransport({
|
|
58
|
+
url: connectionUrl,
|
|
59
|
+
quorumQueues: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
localTransport = new LocalTransport();
|
|
63
|
+
|
|
64
|
+
multiTransport = new MultiTransport(
|
|
65
|
+
{ transports: [rabbitTransport, localTransport] },
|
|
66
|
+
{ onEnqueueFallback: (ctx) => fallbackEvents.push(ctx) },
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await multiTransport.connect();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
for (const sub of subscriptions) {
|
|
74
|
+
if (sub.isActive) {
|
|
75
|
+
await sub.unsubscribe();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (multiTransport.isConnected()) {
|
|
79
|
+
await multiTransport.disconnect();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should send to RabbitMQ when healthy', async () => {
|
|
84
|
+
const topology = createTestTopology(`healthy-${Date.now()}`);
|
|
85
|
+
await multiTransport.applyTopology(topology);
|
|
86
|
+
const queueName = `${topology.namespace}.events`;
|
|
87
|
+
|
|
88
|
+
const receivedMessages: string[] = [];
|
|
89
|
+
|
|
90
|
+
const subscription = await multiTransport.subscribe(
|
|
91
|
+
queueName,
|
|
92
|
+
async (env, receipt) => {
|
|
93
|
+
receivedMessages.push(env.id);
|
|
94
|
+
await multiTransport.complete(receipt);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
subscriptions.push(subscription);
|
|
98
|
+
|
|
99
|
+
const envelope = createTestEnvelope();
|
|
100
|
+
await multiTransport.send(queueName, envelope);
|
|
101
|
+
|
|
102
|
+
await waitFor(() => receivedMessages.length >= 1, 5000);
|
|
103
|
+
|
|
104
|
+
expect(receivedMessages).toContain(envelope.id);
|
|
105
|
+
expect(fallbackEvents).toHaveLength(0); // No fallback occurred
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should fallback to memory transport when RabbitMQ send fails', async () => {
|
|
109
|
+
const topology = createTestTopology(`fallback-${Date.now()}`);
|
|
110
|
+
await multiTransport.applyTopology(topology);
|
|
111
|
+
const queueName = `${topology.namespace}.events`;
|
|
112
|
+
|
|
113
|
+
const receivedMessages: string[] = [];
|
|
114
|
+
|
|
115
|
+
// Subscribe BEFORE disconnecting RabbitMQ
|
|
116
|
+
const subscription = await multiTransport.subscribe(
|
|
117
|
+
queueName,
|
|
118
|
+
async (env, receipt) => {
|
|
119
|
+
receivedMessages.push(env.id);
|
|
120
|
+
await multiTransport.complete(receipt);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
subscriptions.push(subscription);
|
|
124
|
+
|
|
125
|
+
// Disconnect RabbitMQ to simulate failure
|
|
126
|
+
await rabbitTransport.disconnect();
|
|
127
|
+
|
|
128
|
+
// Send should fallback to memory transport
|
|
129
|
+
const envelope = createTestEnvelope();
|
|
130
|
+
await multiTransport.send(queueName, envelope);
|
|
131
|
+
|
|
132
|
+
// Message should be processed by subscriber on memory transport
|
|
133
|
+
await waitFor(() => receivedMessages.length >= 1, 5000);
|
|
134
|
+
|
|
135
|
+
expect(receivedMessages).toContain(envelope.id);
|
|
136
|
+
|
|
137
|
+
// Verify fallback was triggered
|
|
138
|
+
expect(fallbackEvents).toHaveLength(1);
|
|
139
|
+
expect(fallbackEvents[0]!.failedTransport).toBe('rabbitmq');
|
|
140
|
+
expect(fallbackEvents[0]!.nextTransport).toBe('local');
|
|
141
|
+
expect(fallbackEvents[0]!.queue).toBe(queueName);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should process multiple messages via fallback', async () => {
|
|
145
|
+
const topology = createTestTopology(`multi-${Date.now()}`);
|
|
146
|
+
await multiTransport.applyTopology(topology);
|
|
147
|
+
const queueName = `${topology.namespace}.events`;
|
|
148
|
+
|
|
149
|
+
const receivedMessages: string[] = [];
|
|
150
|
+
|
|
151
|
+
const subscription = await multiTransport.subscribe(
|
|
152
|
+
queueName,
|
|
153
|
+
async (env, receipt) => {
|
|
154
|
+
receivedMessages.push(env.id);
|
|
155
|
+
await multiTransport.complete(receipt);
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
subscriptions.push(subscription);
|
|
159
|
+
|
|
160
|
+
// Disconnect RabbitMQ
|
|
161
|
+
await rabbitTransport.disconnect();
|
|
162
|
+
|
|
163
|
+
// Send multiple messages
|
|
164
|
+
const envelopes = [
|
|
165
|
+
createTestEnvelope({ id: 'msg-1' }),
|
|
166
|
+
createTestEnvelope({ id: 'msg-2' }),
|
|
167
|
+
createTestEnvelope({ id: 'msg-3' }),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
for (const env of envelopes) {
|
|
171
|
+
await multiTransport.send(queueName, env);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await waitFor(() => receivedMessages.length >= 3, 5000);
|
|
175
|
+
|
|
176
|
+
expect(receivedMessages).toContain('msg-1');
|
|
177
|
+
expect(receivedMessages).toContain('msg-2');
|
|
178
|
+
expect(receivedMessages).toContain('msg-3');
|
|
179
|
+
expect(fallbackEvents).toHaveLength(3);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should handle mixed success/fallback sends', async () => {
|
|
183
|
+
const topology = createTestTopology(`mixed-${Date.now()}`);
|
|
184
|
+
await multiTransport.applyTopology(topology);
|
|
185
|
+
const queueName = `${topology.namespace}.events`;
|
|
186
|
+
|
|
187
|
+
const receivedMessages: string[] = [];
|
|
188
|
+
|
|
189
|
+
const subscription = await multiTransport.subscribe(
|
|
190
|
+
queueName,
|
|
191
|
+
async (env, receipt) => {
|
|
192
|
+
receivedMessages.push(env.id);
|
|
193
|
+
await multiTransport.complete(receipt);
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
subscriptions.push(subscription);
|
|
197
|
+
|
|
198
|
+
// Send first message to RabbitMQ (should succeed)
|
|
199
|
+
const env1 = createTestEnvelope({ id: 'rabbit-msg' });
|
|
200
|
+
await multiTransport.send(queueName, env1);
|
|
201
|
+
|
|
202
|
+
await waitFor(() => receivedMessages.includes('rabbit-msg'), 5000);
|
|
203
|
+
|
|
204
|
+
// Disconnect RabbitMQ
|
|
205
|
+
await rabbitTransport.disconnect();
|
|
206
|
+
|
|
207
|
+
// Send second message (should fallback to memory)
|
|
208
|
+
const env2 = createTestEnvelope({ id: 'memory-msg' });
|
|
209
|
+
await multiTransport.send(queueName, env2);
|
|
210
|
+
|
|
211
|
+
await waitFor(() => receivedMessages.includes('memory-msg'), 5000);
|
|
212
|
+
|
|
213
|
+
expect(receivedMessages).toContain('rabbit-msg');
|
|
214
|
+
expect(receivedMessages).toContain('memory-msg');
|
|
215
|
+
expect(fallbackEvents).toHaveLength(1);
|
|
216
|
+
expect(fallbackEvents[0]!.envelope.id).toBe('memory-msg');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Waits for a condition to be true, with timeout.
|
|
223
|
+
*/
|
|
224
|
+
async function waitFor(
|
|
225
|
+
condition: () => boolean,
|
|
226
|
+
timeoutMs: number,
|
|
227
|
+
intervalMs = 50,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const startTime = Date.now();
|
|
230
|
+
while (!condition()) {
|
|
231
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
232
|
+
throw new Error(`Timeout waiting for condition after ${timeoutMs}ms`);
|
|
233
|
+
}
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
235
|
+
}
|
|
236
|
+
}
|