@zdavison/matador 2.0.2 → 2.0.4
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/dist/core/matador.d.ts +2 -2
- package/dist/core/matador.d.ts.map +1 -1
- package/dist/core/matador.js +6 -0
- package/dist/index.cjs +80 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/pipeline.d.ts +4 -1
- package/dist/pipeline/pipeline.d.ts.map +1 -1
- package/dist/pipeline/pipeline.js +16 -4
- package/dist/pipeline/pipeline.test.js +15 -1
- package/dist/topology/builder.d.ts +14 -0
- package/dist/topology/builder.d.ts.map +1 -1
- package/dist/topology/builder.js +20 -11
- package/dist/topology/builder.test.js +138 -0
- package/dist/topology/index.d.ts +2 -2
- package/dist/topology/index.d.ts.map +1 -1
- package/dist/topology/index.js +1 -1
- package/dist/topology/types.d.ts +53 -1
- package/dist/topology/types.d.ts.map +1 -1
- package/dist/topology/types.js +10 -0
- package/dist/transport/local/local-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +8 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +42 -22
- package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts +2 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts.map +1 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.test.js +98 -0
- package/dist/types/dispatcher.d.ts +14 -0
- package/dist/types/dispatcher.d.ts.map +1 -0
- package/dist/types/dispatcher.js +1 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/subscriber.d.ts +20 -4
- package/dist/types/subscriber.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/matador.ts +14 -1
- package/src/index.ts +9 -0
- package/src/pipeline/pipeline.test.ts +24 -2
- package/src/pipeline/pipeline.ts +28 -6
- package/src/topology/builder.test.ts +176 -0
- package/src/topology/builder.ts +44 -5
- package/src/topology/index.ts +4 -0
- package/src/topology/types.ts +75 -1
- package/src/transport/local/local-transport.ts +4 -1
- package/src/transport/rabbitmq/rabbitmq-transport.test.ts +118 -0
- package/src/transport/rabbitmq/rabbitmq-transport.ts +48 -25
- package/src/types/dispatcher.ts +18 -0
- package/src/types/index.ts +4 -0
- package/src/types/subscriber.ts +22 -3
- package/test/e2e/rabbitmq-transport.e2e.test.ts +287 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from 'bun:test';
|
|
2
|
+
import { RabbitMQTransport, redactAmqpUrl } from './rabbitmq-transport.js';
|
|
3
|
+
describe('redactAmqpUrl', () => {
|
|
4
|
+
it('should redact username and password with 4 asterisks', () => {
|
|
5
|
+
const url = 'amqp://myuser:mypassword@localhost:5672';
|
|
6
|
+
const redacted = redactAmqpUrl(url);
|
|
7
|
+
expect(redacted).toBe('amqp://****:****@localhost:5672');
|
|
8
|
+
});
|
|
9
|
+
it('should redact credentials in amqps URLs', () => {
|
|
10
|
+
const url = 'amqps://admin:secret123@rabbitmq.example.com:5671';
|
|
11
|
+
const redacted = redactAmqpUrl(url);
|
|
12
|
+
expect(redacted).toBe('amqps://****:****@rabbitmq.example.com:5671');
|
|
13
|
+
});
|
|
14
|
+
it('should redact long credentials to exactly 4 asterisks', () => {
|
|
15
|
+
const url = 'amqp://verylongusername:verylongpassword@rabbitmq-cluster.svc.local:5672';
|
|
16
|
+
const redacted = redactAmqpUrl(url);
|
|
17
|
+
expect(redacted).toBe('amqp://****:****@rabbitmq-cluster.svc.local:5672');
|
|
18
|
+
});
|
|
19
|
+
it('should redact short credentials to exactly 4 asterisks', () => {
|
|
20
|
+
const url = 'amqp://a:b@host:5672';
|
|
21
|
+
const redacted = redactAmqpUrl(url);
|
|
22
|
+
expect(redacted).toBe('amqp://****:****@host:5672');
|
|
23
|
+
});
|
|
24
|
+
it('should preserve vhost in URL', () => {
|
|
25
|
+
const url = 'amqp://user:pass@host:5672/myvhost';
|
|
26
|
+
const redacted = redactAmqpUrl(url);
|
|
27
|
+
expect(redacted).toBe('amqp://****:****@host:5672/myvhost');
|
|
28
|
+
});
|
|
29
|
+
it('should not modify URL without credentials', () => {
|
|
30
|
+
const url = 'amqp://localhost:5672';
|
|
31
|
+
const redacted = redactAmqpUrl(url);
|
|
32
|
+
expect(redacted).toBe('amqp://localhost:5672');
|
|
33
|
+
});
|
|
34
|
+
it('should not modify URL with only username (no password)', () => {
|
|
35
|
+
const url = 'amqp://guest@localhost:5672';
|
|
36
|
+
const redacted = redactAmqpUrl(url);
|
|
37
|
+
expect(redacted).toBe('amqp://guest@localhost:5672');
|
|
38
|
+
});
|
|
39
|
+
it('should handle URL-encoded credentials', () => {
|
|
40
|
+
// URL-encoded @ in password: p%40ssword
|
|
41
|
+
const url = 'amqp://user:p%40ssword@host:5672';
|
|
42
|
+
const redacted = redactAmqpUrl(url);
|
|
43
|
+
expect(redacted).toBe('amqp://****:****@host:5672');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('RabbitMQTransport', () => {
|
|
47
|
+
describe('connection logging', () => {
|
|
48
|
+
it('should log redacted connection URL when connecting', async () => {
|
|
49
|
+
const mockLogger = {
|
|
50
|
+
debug: mock(() => { }),
|
|
51
|
+
info: mock(() => { }),
|
|
52
|
+
warn: mock(() => { }),
|
|
53
|
+
error: mock(() => { }),
|
|
54
|
+
};
|
|
55
|
+
const transport = new RabbitMQTransport({
|
|
56
|
+
url: 'amqp://testuser:testpass@localhost:5672',
|
|
57
|
+
logger: mockLogger,
|
|
58
|
+
connection: {
|
|
59
|
+
maxReconnectAttempts: 1, // Only try once to avoid long retries
|
|
60
|
+
initialReconnectDelay: 10,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// Attempt to connect - it will fail since there's no RabbitMQ server
|
|
64
|
+
// but the log should still be emitted before the connection attempt
|
|
65
|
+
try {
|
|
66
|
+
await transport.connect();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Expected to fail - no RabbitMQ server running
|
|
70
|
+
}
|
|
71
|
+
// Verify the log was called with the redacted URL
|
|
72
|
+
expect(mockLogger.info).toHaveBeenCalledWith("[Matador] \u23F3 Connecting to RabbitMQ at 'amqp://****:****@localhost:5672'.");
|
|
73
|
+
});
|
|
74
|
+
it('should log connection URL as-is when no credentials provided', async () => {
|
|
75
|
+
const mockLogger = {
|
|
76
|
+
debug: mock(() => { }),
|
|
77
|
+
info: mock(() => { }),
|
|
78
|
+
warn: mock(() => { }),
|
|
79
|
+
error: mock(() => { }),
|
|
80
|
+
};
|
|
81
|
+
const transport = new RabbitMQTransport({
|
|
82
|
+
url: 'amqp://localhost:5672',
|
|
83
|
+
logger: mockLogger,
|
|
84
|
+
connection: {
|
|
85
|
+
maxReconnectAttempts: 1,
|
|
86
|
+
initialReconnectDelay: 10,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
await transport.connect();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Expected to fail
|
|
94
|
+
}
|
|
95
|
+
expect(mockLogger.info).toHaveBeenCalledWith("[Matador] \u23F3 Connecting to RabbitMQ at 'amqp://localhost:5672'.");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SendResult } from '../core/fanout.js';
|
|
2
|
+
import type { Event, EventClass, EventOptions } from './event.js';
|
|
3
|
+
/**
|
|
4
|
+
* Interface for dispatching events.
|
|
5
|
+
* Implemented by Matador to allow subscribers to send events.
|
|
6
|
+
*/
|
|
7
|
+
export interface Dispatcher {
|
|
8
|
+
/**
|
|
9
|
+
* Sends an event to all registered subscribers.
|
|
10
|
+
*/
|
|
11
|
+
send<T>(eventClass: EventClass<T>, data: T, options?: EventOptions): Promise<SendResult>;
|
|
12
|
+
send<T>(event: Event<T>, options?: EventOptions): Promise<SendResult>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=dispatcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/types/dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,IAAI,CAAC,CAAC,EACJ,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,UAAU,CAAC,CAAC;IACvB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACvE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export type { DeliveryMode, Idempotency, Importance, ValidationError, ValidationResult, } from './common.js';
|
|
2
2
|
export { invalidResult, validResult } from './common.js';
|
|
3
|
+
export type { Dispatcher } from './dispatcher.js';
|
|
3
4
|
export type { CreateEnvelopeOptions, Docket, Envelope } from './envelope.js';
|
|
4
5
|
export { createEnvelope } from './envelope.js';
|
|
5
6
|
export type { Event, EventClass, EventData, EventKey, EventOptions, EventStatic, JsonPrimitive, JsonRecord, JsonValue, } from './event.js';
|
|
6
7
|
export { MatadorEvent } from './event.js';
|
|
7
|
-
export type { AnySubscriber, BaseSubscriberOptions, CreateResumableSubscriberInput, CreateStandardSubscriberInput, CreateSubscriberInput, EnvelopeOf, ResumableCallback, ResumableSubscriber, ResumableSubscriberOptions, StandardCallback, StandardSubscriber, StandardSubscriberOptions, Subscriber, SubscriberCallback, SubscriberDefinition, SubscriberOptions, SubscriberStub, } from './subscriber.js';
|
|
8
|
+
export type { AnySubscriber, BaseSubscriberOptions, CallbackContext, CreateResumableSubscriberInput, CreateStandardSubscriberInput, CreateSubscriberInput, EnvelopeOf, ResumableCallback, ResumableCallbackContext, ResumableSubscriber, ResumableSubscriberOptions, StandardCallback, StandardSubscriber, StandardSubscriberOptions, Subscriber, SubscriberCallback, SubscriberDefinition, SubscriberOptions, SubscriberStub, } from './subscriber.js';
|
|
8
9
|
export { createSubscriber, createSubscriberStub, isResumableSubscriber, isStandardSubscriber, isSubscriber, isSubscriberStub, } from './subscriber.js';
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEzD,YAAY,EAAE,qBAAqB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,YAAY,EACV,KAAK,EACL,UAAU,EACV,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,aAAa,EACb,UAAU,EACV,SAAS,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,YAAY,EACV,aAAa,EACb,qBAAqB,EACrB,8BAA8B,EAC9B,6BAA6B,EAC7B,qBAAqB,EACrB,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EACnB,0BAA0B,EAC1B,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,UAAU,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEzD,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,YAAY,EAAE,qBAAqB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,YAAY,EACV,KAAK,EACL,UAAU,EACV,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,aAAa,EACb,UAAU,EACV,SAAS,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,YAAY,EACV,aAAa,EACb,qBAAqB,EACrB,eAAe,EACf,8BAA8B,EAC9B,6BAA6B,EAC7B,qBAAqB,EACrB,UAAU,EACV,iBAAiB,EACjB,wBAAwB,EACxB,mBAAmB,EACnB,0BAA0B,EAC1B,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,UAAU,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { SubscriberContext } from '../checkpoint/index.js';
|
|
2
2
|
import type { Idempotency, Importance } from './common.js';
|
|
3
|
+
import type { Dispatcher } from './dispatcher.js';
|
|
3
4
|
import type { Envelope } from './envelope.js';
|
|
4
5
|
import type { MatadorEvent } from './event.js';
|
|
6
|
+
/**
|
|
7
|
+
* Context passed to subscriber callbacks.
|
|
8
|
+
* Provides access to the Matador instance for sending additional events.
|
|
9
|
+
*/
|
|
10
|
+
export interface CallbackContext {
|
|
11
|
+
/** Matador dispatcher for sending additional events from within a subscriber */
|
|
12
|
+
readonly matador: Dispatcher;
|
|
13
|
+
}
|
|
5
14
|
/**
|
|
6
15
|
* Helper type to get the envelope type for a subscriber callback.
|
|
7
16
|
* Extracts the data type from a MatadorEvent and wraps it in an Envelope.
|
|
@@ -14,14 +23,21 @@ import type { MatadorEvent } from './event.js';
|
|
|
14
23
|
export type EnvelopeOf<T extends MatadorEvent> = Envelope<T['data']>;
|
|
15
24
|
/**
|
|
16
25
|
* Callback function executed when an event is received (standard subscribers).
|
|
17
|
-
* Receives the full envelope containing id, data, and docket
|
|
26
|
+
* Receives the full envelope containing id, data, and docket, plus a context
|
|
27
|
+
* with access to the matador instance for sending additional events.
|
|
28
|
+
*/
|
|
29
|
+
export type StandardCallback<T = unknown> = (envelope: Envelope<T>, context: CallbackContext) => Promise<void> | void;
|
|
30
|
+
/**
|
|
31
|
+
* Context for resumable subscriber callbacks.
|
|
32
|
+
* Combines checkpoint operations (io, all) with matador access.
|
|
18
33
|
*/
|
|
19
|
-
export type
|
|
34
|
+
export type ResumableCallbackContext = SubscriberContext & CallbackContext;
|
|
20
35
|
/**
|
|
21
36
|
* Callback function for resumable subscribers.
|
|
22
|
-
* Receives the envelope and a
|
|
37
|
+
* Receives the envelope and a context with io() for checkpointed operations
|
|
38
|
+
* and matador for sending additional events.
|
|
23
39
|
*/
|
|
24
|
-
export type ResumableCallback<T = unknown> = (envelope: Envelope<T>, context:
|
|
40
|
+
export type ResumableCallback<T = unknown> = (envelope: Envelope<T>, context: ResumableCallbackContext) => Promise<void> | void;
|
|
25
41
|
/**
|
|
26
42
|
* Callback function executed when an event is received.
|
|
27
43
|
* @deprecated Use StandardCallback or ResumableCallback instead.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../src/types/subscriber.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,YAAY,IAAI,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAErE
|
|
1
|
+
{"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../src/types/subscriber.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/core/matador.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { getQualifiedQueueName } from '../topology/index.js';
|
|
|
18
18
|
import type { Subscription, Transport } from '../transport/index.js';
|
|
19
19
|
import type {
|
|
20
20
|
AnySubscriber,
|
|
21
|
+
Dispatcher,
|
|
21
22
|
Event,
|
|
22
23
|
EventClass,
|
|
23
24
|
EventOptions,
|
|
@@ -71,7 +72,7 @@ export interface MatadorConfig {
|
|
|
71
72
|
* - Fanout: Event sending
|
|
72
73
|
* - Shutdown: Graceful termination
|
|
73
74
|
*/
|
|
74
|
-
export class Matador {
|
|
75
|
+
export class Matador implements Dispatcher {
|
|
75
76
|
private readonly transport: Transport;
|
|
76
77
|
private readonly topology: Topology;
|
|
77
78
|
private readonly schema;
|
|
@@ -114,6 +115,7 @@ export class Matador {
|
|
|
114
115
|
retryPolicy: this.retryPolicy,
|
|
115
116
|
hooks: this.hooks,
|
|
116
117
|
checkpointStore: config.checkpointStore,
|
|
118
|
+
dispatcher: this,
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
// Create fanout engine
|
|
@@ -199,12 +201,23 @@ export class Matador {
|
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
// Connect transport
|
|
204
|
+
this.hooks.logger.info(
|
|
205
|
+
`[Matador] ⏳ Starting backend '${this.transport.name}'...`,
|
|
206
|
+
);
|
|
202
207
|
await this.transport.connect();
|
|
208
|
+
this.hooks.logger.info(
|
|
209
|
+
`[Matador] 🟢 Backend '${this.transport.name}' started.`,
|
|
210
|
+
);
|
|
203
211
|
|
|
204
212
|
// Apply topology
|
|
205
213
|
await this.transport.applyTopology(this.topology);
|
|
206
214
|
|
|
207
215
|
// Subscribe to queues
|
|
216
|
+
if (this.consumeFrom.length > 0) {
|
|
217
|
+
this.hooks.logger.info(
|
|
218
|
+
`[Matador] 🟢 Worker subscribing to '${this.consumeFrom.join(',')}'.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
208
221
|
for (const queueName of this.consumeFrom) {
|
|
209
222
|
const qualifiedName = getQualifiedQueueName(
|
|
210
223
|
this.topology.namespace,
|
package/src/index.ts
CHANGED
|
@@ -19,8 +19,10 @@ export {
|
|
|
19
19
|
export type {
|
|
20
20
|
AnySubscriber,
|
|
21
21
|
BaseSubscriberOptions,
|
|
22
|
+
CallbackContext,
|
|
22
23
|
CreateEnvelopeOptions,
|
|
23
24
|
DeliveryMode,
|
|
25
|
+
Dispatcher,
|
|
24
26
|
Docket,
|
|
25
27
|
Envelope,
|
|
26
28
|
EnvelopeOf,
|
|
@@ -33,6 +35,9 @@ export type {
|
|
|
33
35
|
Idempotency,
|
|
34
36
|
Importance,
|
|
35
37
|
JsonRecord,
|
|
38
|
+
ResumableCallback,
|
|
39
|
+
ResumableCallbackContext,
|
|
40
|
+
StandardCallback,
|
|
36
41
|
Subscriber,
|
|
37
42
|
SubscriberCallback,
|
|
38
43
|
SubscriberDefinition,
|
|
@@ -91,13 +96,17 @@ export type {
|
|
|
91
96
|
DeadLetterQueueConfig,
|
|
92
97
|
QueueDefinition,
|
|
93
98
|
QueueOptions,
|
|
99
|
+
RabbitMQQueueDefinition,
|
|
100
|
+
RabbitMQQueueOptions,
|
|
94
101
|
RetryConfig,
|
|
95
102
|
Topology,
|
|
103
|
+
TransportQueueOptions,
|
|
96
104
|
} from './topology/index.js';
|
|
97
105
|
export {
|
|
98
106
|
getDeadLetterQueueName,
|
|
99
107
|
getQualifiedQueueName,
|
|
100
108
|
getRetryQueueName,
|
|
109
|
+
resolveQueueName,
|
|
101
110
|
TopologyBuilder,
|
|
102
111
|
TopologyValidationError,
|
|
103
112
|
} from './topology/index.js';
|
|
@@ -10,7 +10,11 @@ import type { RetryDecision, RetryPolicy } from '../retry/index.js';
|
|
|
10
10
|
import { StandardRetryPolicy } from '../retry/index.js';
|
|
11
11
|
import type { SchemaRegistry } from '../schema/index.js';
|
|
12
12
|
import type { MessageReceipt, Transport } from '../transport/index.js';
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
Dispatcher,
|
|
15
|
+
Envelope,
|
|
16
|
+
SubscriberDefinition,
|
|
17
|
+
} from '../types/index.js';
|
|
14
18
|
import { createEnvelope } from '../types/index.js';
|
|
15
19
|
import type { PipelineConfig } from './pipeline.js';
|
|
16
20
|
import { ProcessingPipeline } from './pipeline.js';
|
|
@@ -324,7 +328,13 @@ describe('ProcessingPipeline', () => {
|
|
|
324
328
|
const result = await pipeline.process(new Uint8Array(), receipt);
|
|
325
329
|
|
|
326
330
|
expect(result.success).toBe(true);
|
|
327
|
-
expect(callbackMock).
|
|
331
|
+
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
332
|
+
// Callback receives envelope as first argument and context (with matador) as second
|
|
333
|
+
const calls = callbackMock.mock.calls as unknown as [
|
|
334
|
+
[Envelope, { matador: Dispatcher }],
|
|
335
|
+
];
|
|
336
|
+
expect(calls[0][0]).toEqual(envelope);
|
|
337
|
+
expect(calls[0][1]).toHaveProperty('matador');
|
|
328
338
|
});
|
|
329
339
|
|
|
330
340
|
it('should handle callback throwing error', async () => {
|
|
@@ -1189,6 +1199,7 @@ function createMockConfig(
|
|
|
1189
1199
|
codec: Partial<Codec>;
|
|
1190
1200
|
retryPolicy: RetryPolicy | Partial<RetryPolicy>;
|
|
1191
1201
|
hooks: Partial<SafeHooks>;
|
|
1202
|
+
dispatcher: Partial<Dispatcher>;
|
|
1192
1203
|
}> = {},
|
|
1193
1204
|
): PipelineConfig {
|
|
1194
1205
|
const defaultTransport: Transport = {
|
|
@@ -1272,12 +1283,23 @@ function createMockConfig(
|
|
|
1272
1283
|
...overrides.hooks,
|
|
1273
1284
|
} as SafeHooks;
|
|
1274
1285
|
|
|
1286
|
+
const defaultDispatcher: Dispatcher = {
|
|
1287
|
+
send: mock(async () => ({
|
|
1288
|
+
eventKey: 'test.event',
|
|
1289
|
+
subscribersSent: 0,
|
|
1290
|
+
subscribersSkipped: 0,
|
|
1291
|
+
errors: [],
|
|
1292
|
+
})),
|
|
1293
|
+
...overrides.dispatcher,
|
|
1294
|
+
} as Dispatcher;
|
|
1295
|
+
|
|
1275
1296
|
return {
|
|
1276
1297
|
transport: defaultTransport,
|
|
1277
1298
|
schema: { ...defaultSchema, ...overrides.schema } as SchemaRegistry,
|
|
1278
1299
|
codec: defaultCodec,
|
|
1279
1300
|
retryPolicy: defaultRetryPolicy,
|
|
1280
1301
|
hooks: defaultHooks,
|
|
1302
|
+
dispatcher: defaultDispatcher,
|
|
1281
1303
|
};
|
|
1282
1304
|
}
|
|
1283
1305
|
|
package/src/pipeline/pipeline.ts
CHANGED
|
@@ -10,7 +10,13 @@ import type { SafeHooks } from '../hooks/index.js';
|
|
|
10
10
|
import type { RetryDecision, RetryPolicy } from '../retry/index.js';
|
|
11
11
|
import type { SchemaRegistry } from '../schema/index.js';
|
|
12
12
|
import type { MessageReceipt, Transport } from '../transport/index.js';
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
CallbackContext,
|
|
15
|
+
Dispatcher,
|
|
16
|
+
Envelope,
|
|
17
|
+
ResumableCallbackContext,
|
|
18
|
+
SubscriberDefinition,
|
|
19
|
+
} from '../types/index.js';
|
|
14
20
|
import { isResumableSubscriber } from '../types/index.js';
|
|
15
21
|
|
|
16
22
|
/**
|
|
@@ -24,6 +30,8 @@ export interface PipelineConfig {
|
|
|
24
30
|
readonly hooks: SafeHooks;
|
|
25
31
|
/** Optional checkpoint store for resumable subscribers */
|
|
26
32
|
readonly checkpointStore?: CheckpointStore | undefined;
|
|
33
|
+
/** Dispatcher (matador) for sending events from subscriber callbacks */
|
|
34
|
+
readonly dispatcher: Dispatcher;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
@@ -54,6 +62,7 @@ export class ProcessingPipeline {
|
|
|
54
62
|
private readonly retryPolicy: RetryPolicy;
|
|
55
63
|
private readonly hooks: SafeHooks;
|
|
56
64
|
private readonly checkpointStore: CheckpointStore;
|
|
65
|
+
private readonly dispatcher: Dispatcher;
|
|
57
66
|
|
|
58
67
|
constructor(config: PipelineConfig) {
|
|
59
68
|
this.transport = config.transport;
|
|
@@ -62,6 +71,7 @@ export class ProcessingPipeline {
|
|
|
62
71
|
this.retryPolicy = config.retryPolicy;
|
|
63
72
|
this.hooks = config.hooks;
|
|
64
73
|
this.checkpointStore = config.checkpointStore ?? new NoOpCheckpointStore();
|
|
74
|
+
this.dispatcher = config.dispatcher;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
/**
|
|
@@ -180,24 +190,36 @@ export class ProcessingPipeline {
|
|
|
180
190
|
});
|
|
181
191
|
}
|
|
182
192
|
|
|
193
|
+
// Create base callback context with matador dispatcher
|
|
194
|
+
const callbackContext: CallbackContext = { matador: this.dispatcher };
|
|
195
|
+
|
|
183
196
|
await this.hooks.onWorkerWrap(envelope, subscriberDef, async () => {
|
|
184
197
|
await this.hooks.onWorkerBeforeProcess(envelope, subscriberDef);
|
|
185
198
|
|
|
186
199
|
try {
|
|
187
200
|
if (isResumable && context) {
|
|
188
|
-
// Resumable subscriber: pass context
|
|
201
|
+
// Resumable subscriber: pass combined context (checkpoint + matador)
|
|
189
202
|
// Cast needed because TypeScript can't narrow the union type based on isResumable
|
|
190
203
|
const resumableCallback = subscriber.callback as (
|
|
191
204
|
envelope: Envelope,
|
|
192
|
-
context:
|
|
205
|
+
context: ResumableCallbackContext,
|
|
193
206
|
) => Promise<void> | void;
|
|
194
|
-
|
|
207
|
+
// Create combined context by binding class methods and adding matador
|
|
208
|
+
const fullContext: ResumableCallbackContext = {
|
|
209
|
+
io: context.io.bind(context),
|
|
210
|
+
all: context.all.bind(context),
|
|
211
|
+
attempt: context.attempt,
|
|
212
|
+
isRetry: context.isRetry,
|
|
213
|
+
matador: this.dispatcher,
|
|
214
|
+
};
|
|
215
|
+
result = await resumableCallback(envelope, fullContext);
|
|
195
216
|
} else {
|
|
196
|
-
// Standard subscriber:
|
|
217
|
+
// Standard subscriber: pass envelope and callback context
|
|
197
218
|
const standardCallback = subscriber.callback as (
|
|
198
219
|
envelope: Envelope,
|
|
220
|
+
context: CallbackContext,
|
|
199
221
|
) => Promise<void> | void;
|
|
200
|
-
result = await standardCallback(envelope);
|
|
222
|
+
result = await standardCallback(envelope, callbackContext);
|
|
201
223
|
}
|
|
202
224
|
} catch (e) {
|
|
203
225
|
error = e instanceof Error ? e : new Error(String(e));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
import { TopologyBuilder, TopologyValidationError } from './builder.js';
|
|
3
|
+
import { resolveQueueName } from './types.js';
|
|
3
4
|
|
|
4
5
|
describe('TopologyBuilder', () => {
|
|
5
6
|
describe('withNamespace', () => {
|
|
@@ -271,5 +272,180 @@ describe('TopologyBuilder', () => {
|
|
|
271
272
|
|
|
272
273
|
expect(topology.queues[0]?.exact).toBe(true);
|
|
273
274
|
});
|
|
275
|
+
|
|
276
|
+
it('should allow dots in queue name when exact: true', () => {
|
|
277
|
+
const topology = TopologyBuilder.create()
|
|
278
|
+
.withNamespace('test')
|
|
279
|
+
.addQueue('matador.shared.id-platform', { exact: true })
|
|
280
|
+
.build();
|
|
281
|
+
|
|
282
|
+
expect(topology.queues[0]?.name).toBe('matador.shared.id-platform');
|
|
283
|
+
expect(topology.queues[0]?.exact).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should reject dots in queue name when exact: false', () => {
|
|
287
|
+
const builder = TopologyBuilder.create()
|
|
288
|
+
.withNamespace('test')
|
|
289
|
+
.addQueue('invalid.queue.name');
|
|
290
|
+
|
|
291
|
+
expect(() => builder.build()).toThrow('must start with a letter');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should allow transport-specific RabbitMQ options with exact queue', () => {
|
|
295
|
+
const topology = TopologyBuilder.create()
|
|
296
|
+
.withNamespace('test')
|
|
297
|
+
.addQueue('matador.shared.id-platform', {
|
|
298
|
+
exact: true,
|
|
299
|
+
transport: {
|
|
300
|
+
rabbitmq: {
|
|
301
|
+
options: {
|
|
302
|
+
durable: true,
|
|
303
|
+
deadLetterExchange: 'matador.shared.dlx-undeliverable',
|
|
304
|
+
arguments: {
|
|
305
|
+
'x-queue-type': 'quorum',
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
.build();
|
|
312
|
+
|
|
313
|
+
expect(topology.queues[0]?.name).toBe('matador.shared.id-platform');
|
|
314
|
+
expect(topology.queues[0]?.exact).toBe(true);
|
|
315
|
+
expect(topology.queues[0]?.transport?.rabbitmq?.options?.durable).toBe(
|
|
316
|
+
true,
|
|
317
|
+
);
|
|
318
|
+
expect(
|
|
319
|
+
topology.queues[0]?.transport?.rabbitmq?.options?.deadLetterExchange,
|
|
320
|
+
).toBe('matador.shared.dlx-undeliverable');
|
|
321
|
+
expect(
|
|
322
|
+
topology.queues[0]?.transport?.rabbitmq?.options?.arguments?.[
|
|
323
|
+
'x-queue-type'
|
|
324
|
+
],
|
|
325
|
+
).toBe('quorum');
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('addQueue with QueueDefinition object', () => {
|
|
330
|
+
it('should accept a QueueDefinition object', () => {
|
|
331
|
+
const queueDef = {
|
|
332
|
+
name: 'events',
|
|
333
|
+
concurrency: 5,
|
|
334
|
+
consumerTimeout: 30000,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const topology = TopologyBuilder.create()
|
|
338
|
+
.withNamespace('test')
|
|
339
|
+
.addQueue(queueDef)
|
|
340
|
+
.build();
|
|
341
|
+
|
|
342
|
+
expect(topology.queues).toHaveLength(1);
|
|
343
|
+
expect(topology.queues[0]?.name).toBe('events');
|
|
344
|
+
expect(topology.queues[0]?.concurrency).toBe(5);
|
|
345
|
+
expect(topology.queues[0]?.consumerTimeout).toBe(30000);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should allow reusing queue definitions across builders', () => {
|
|
349
|
+
const sharedQueue = {
|
|
350
|
+
name: 'shared-queue',
|
|
351
|
+
concurrency: 10,
|
|
352
|
+
exact: true,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const topology1 = TopologyBuilder.create()
|
|
356
|
+
.withNamespace('app1')
|
|
357
|
+
.addQueue(sharedQueue)
|
|
358
|
+
.build();
|
|
359
|
+
|
|
360
|
+
const topology2 = TopologyBuilder.create()
|
|
361
|
+
.withNamespace('app2')
|
|
362
|
+
.addQueue(sharedQueue)
|
|
363
|
+
.build();
|
|
364
|
+
|
|
365
|
+
expect(topology1.queues[0]).toEqual(sharedQueue);
|
|
366
|
+
expect(topology2.queues[0]).toEqual(sharedQueue);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should work with queue() alias', () => {
|
|
370
|
+
const queueDef = {
|
|
371
|
+
name: 'analytics',
|
|
372
|
+
priorities: true,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const topology = TopologyBuilder.create()
|
|
376
|
+
.withNamespace('test')
|
|
377
|
+
.queue(queueDef)
|
|
378
|
+
.build();
|
|
379
|
+
|
|
380
|
+
expect(topology.queues[0]?.name).toBe('analytics');
|
|
381
|
+
expect(topology.queues[0]?.priorities).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should mix QueueDefinition objects with name+options style', () => {
|
|
385
|
+
const reusableQueue = {
|
|
386
|
+
name: 'shared',
|
|
387
|
+
concurrency: 3,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const topology = TopologyBuilder.create()
|
|
391
|
+
.withNamespace('test')
|
|
392
|
+
.addQueue(reusableQueue)
|
|
393
|
+
.addQueue('local', { concurrency: 1 })
|
|
394
|
+
.build();
|
|
395
|
+
|
|
396
|
+
expect(topology.queues).toHaveLength(2);
|
|
397
|
+
expect(topology.queues[0]?.name).toBe('shared');
|
|
398
|
+
expect(topology.queues[1]?.name).toBe('local');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should validate QueueDefinition objects the same way', () => {
|
|
402
|
+
const invalidQueue = {
|
|
403
|
+
name: '123invalid',
|
|
404
|
+
concurrency: 5,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const builder = TopologyBuilder.create()
|
|
408
|
+
.withNamespace('test')
|
|
409
|
+
.addQueue(invalidQueue);
|
|
410
|
+
|
|
411
|
+
expect(() => builder.build()).toThrow('must start with a letter');
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('resolveQueueName', () => {
|
|
417
|
+
it('should return namespace.name for regular queues', () => {
|
|
418
|
+
const queueDef = { name: 'events' };
|
|
419
|
+
expect(resolveQueueName('myapp', queueDef)).toBe('myapp.events');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should return name as-is when exact: true', () => {
|
|
423
|
+
const queueDef = { name: 'matador.shared.id-platform', exact: true };
|
|
424
|
+
expect(resolveQueueName('myapp', queueDef)).toBe(
|
|
425
|
+
'matador.shared.id-platform',
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should return namespace.name when exact: false', () => {
|
|
430
|
+
const queueDef = { name: 'events', exact: false };
|
|
431
|
+
expect(resolveQueueName('myapp', queueDef)).toBe('myapp.events');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should work with full QueueDefinition including transport options', () => {
|
|
435
|
+
const queueDef = {
|
|
436
|
+
name: 'matador.shared.id-platform',
|
|
437
|
+
exact: true,
|
|
438
|
+
transport: {
|
|
439
|
+
rabbitmq: {
|
|
440
|
+
options: {
|
|
441
|
+
durable: true,
|
|
442
|
+
deadLetterExchange: 'matador.shared.dlx-undeliverable',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
expect(resolveQueueName('myapp', queueDef)).toBe(
|
|
448
|
+
'matador.shared.id-platform',
|
|
449
|
+
);
|
|
274
450
|
});
|
|
275
451
|
});
|