@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.
Files changed (55) hide show
  1. package/dist/core/matador.d.ts +2 -2
  2. package/dist/core/matador.d.ts.map +1 -1
  3. package/dist/core/matador.js +6 -0
  4. package/dist/index.cjs +80 -32
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +3 -3
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/pipeline/pipeline.d.ts +4 -1
  12. package/dist/pipeline/pipeline.d.ts.map +1 -1
  13. package/dist/pipeline/pipeline.js +16 -4
  14. package/dist/pipeline/pipeline.test.js +15 -1
  15. package/dist/topology/builder.d.ts +14 -0
  16. package/dist/topology/builder.d.ts.map +1 -1
  17. package/dist/topology/builder.js +20 -11
  18. package/dist/topology/builder.test.js +138 -0
  19. package/dist/topology/index.d.ts +2 -2
  20. package/dist/topology/index.d.ts.map +1 -1
  21. package/dist/topology/index.js +1 -1
  22. package/dist/topology/types.d.ts +53 -1
  23. package/dist/topology/types.d.ts.map +1 -1
  24. package/dist/topology/types.js +10 -0
  25. package/dist/transport/local/local-transport.d.ts.map +1 -1
  26. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +8 -0
  27. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
  28. package/dist/transport/rabbitmq/rabbitmq-transport.js +42 -22
  29. package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts +2 -0
  30. package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts.map +1 -0
  31. package/dist/transport/rabbitmq/rabbitmq-transport.test.js +98 -0
  32. package/dist/types/dispatcher.d.ts +14 -0
  33. package/dist/types/dispatcher.d.ts.map +1 -0
  34. package/dist/types/dispatcher.js +1 -0
  35. package/dist/types/index.d.ts +2 -1
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/types/subscriber.d.ts +20 -4
  38. package/dist/types/subscriber.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/core/matador.ts +14 -1
  41. package/src/index.ts +9 -0
  42. package/src/pipeline/pipeline.test.ts +24 -2
  43. package/src/pipeline/pipeline.ts +28 -6
  44. package/src/topology/builder.test.ts +176 -0
  45. package/src/topology/builder.ts +44 -5
  46. package/src/topology/index.ts +4 -0
  47. package/src/topology/types.ts +75 -1
  48. package/src/transport/local/local-transport.ts +4 -1
  49. package/src/transport/rabbitmq/rabbitmq-transport.test.ts +118 -0
  50. package/src/transport/rabbitmq/rabbitmq-transport.ts +48 -25
  51. package/src/types/dispatcher.ts +18 -0
  52. package/src/types/index.ts +4 -0
  53. package/src/types/subscriber.ts +22 -3
  54. package/test/e2e/rabbitmq-transport.e2e.test.ts +287 -0
  55. 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 {};
@@ -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 StandardCallback<T = unknown> = (envelope: Envelope<T>) => Promise<void> | void;
34
+ export type ResumableCallbackContext = SubscriberContext & CallbackContext;
20
35
  /**
21
36
  * Callback function for resumable subscribers.
22
- * Receives the envelope and a SubscriberContext with io() for checkpointed operations.
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: SubscriberContext) => Promise<void> | void;
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":"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"}
1
+ {"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../src/types/subscriber.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zdavison/matador",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "A batteries-included event system.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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 { Envelope, SubscriberDefinition } from '../types/index.js';
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).toHaveBeenCalledWith(envelope);
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
 
@@ -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 { Envelope, SubscriberDefinition } from '../types/index.js';
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 as second argument
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: ResumableContext,
205
+ context: ResumableCallbackContext,
193
206
  ) => Promise<void> | void;
194
- result = await resumableCallback(envelope, context);
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: just pass envelope
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
  });