@zdavison/matador 2.0.3 → 2.0.5

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 (76) hide show
  1. package/dist/core/fanout.test.js +0 -1
  2. package/dist/core/matador.d.ts +2 -2
  3. package/dist/core/matador.d.ts.map +1 -1
  4. package/dist/core/matador.js +1 -0
  5. package/dist/errors/index.d.ts +1 -1
  6. package/dist/errors/index.d.ts.map +1 -1
  7. package/dist/errors/index.js +1 -1
  8. package/dist/errors/retry-errors.d.ts +40 -5
  9. package/dist/errors/retry-errors.d.ts.map +1 -1
  10. package/dist/errors/retry-errors.js +56 -9
  11. package/dist/errors/retry-errors.test.d.ts +2 -0
  12. package/dist/errors/retry-errors.test.d.ts.map +1 -0
  13. package/dist/errors/retry-errors.test.js +136 -0
  14. package/dist/index.cjs +73 -28
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +5 -5
  17. package/dist/index.d.ts +5 -5
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/pipeline/pipeline.d.ts +4 -1
  22. package/dist/pipeline/pipeline.d.ts.map +1 -1
  23. package/dist/pipeline/pipeline.js +16 -4
  24. package/dist/pipeline/pipeline.test.js +48 -1
  25. package/dist/retry/standard-policy.test.js +7 -1
  26. package/dist/schema/registry.d.ts.map +1 -1
  27. package/dist/schema/registry.js +4 -1
  28. package/dist/schema/types.test.js +0 -1
  29. package/dist/topology/builder.d.ts.map +1 -1
  30. package/dist/topology/builder.js +2 -1
  31. package/dist/topology/builder.test.js +68 -0
  32. package/dist/topology/index.d.ts +2 -2
  33. package/dist/topology/index.d.ts.map +1 -1
  34. package/dist/topology/index.js +1 -1
  35. package/dist/topology/types.d.ts +53 -1
  36. package/dist/topology/types.d.ts.map +1 -1
  37. package/dist/topology/types.js +10 -0
  38. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
  39. package/dist/transport/rabbitmq/rabbitmq-transport.js +30 -22
  40. package/dist/transport/rabbitmq/rabbitmq-transport.test.js +1 -1
  41. package/dist/types/dispatcher.d.ts +14 -0
  42. package/dist/types/dispatcher.d.ts.map +1 -0
  43. package/dist/types/dispatcher.js +1 -0
  44. package/dist/types/envelope.d.ts +17 -0
  45. package/dist/types/envelope.d.ts.map +1 -1
  46. package/dist/types/envelope.js +19 -0
  47. package/dist/types/index.d.ts +3 -2
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js +1 -1
  50. package/dist/types/subscriber.d.ts +28 -8
  51. package/dist/types/subscriber.d.ts.map +1 -1
  52. package/dist/types/subscriber.js +0 -2
  53. package/package.json +1 -1
  54. package/src/core/fanout.test.ts +0 -1
  55. package/src/core/matador.ts +3 -1
  56. package/src/errors/index.ts +1 -0
  57. package/src/errors/retry-errors.test.ts +175 -0
  58. package/src/errors/retry-errors.ts +63 -9
  59. package/src/index.ts +11 -0
  60. package/src/pipeline/pipeline.test.ts +62 -2
  61. package/src/pipeline/pipeline.ts +28 -6
  62. package/src/retry/standard-policy.test.ts +9 -1
  63. package/src/schema/registry.ts +4 -1
  64. package/src/schema/types.test.ts +0 -1
  65. package/src/topology/builder.test.ts +90 -0
  66. package/src/topology/builder.ts +2 -1
  67. package/src/topology/index.ts +4 -0
  68. package/src/topology/types.ts +75 -1
  69. package/src/transport/rabbitmq/rabbitmq-transport.test.ts +1 -1
  70. package/src/transport/rabbitmq/rabbitmq-transport.ts +33 -25
  71. package/src/types/dispatcher.ts +18 -0
  72. package/src/types/envelope.ts +23 -0
  73. package/src/types/index.ts +5 -1
  74. package/src/types/subscriber.ts +33 -10
  75. package/test/e2e/rabbitmq-transport.e2e.test.ts +287 -0
  76. package/tsconfig.tsbuildinfo +1 -1
@@ -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));
@@ -14,7 +14,15 @@ describe('StandardRetryPolicy', () => {
14
14
  describe('shouldRetry', () => {
15
15
  it('should dead-letter on EventAssertionError', () => {
16
16
  const policy = new StandardRetryPolicy();
17
- const context = createContext(new EventAssertionError('Invalid event'));
17
+ const envelope = createEnvelope({
18
+ eventKey: 'test.event',
19
+ targetSubscriber: 'test-subscriber',
20
+ data: { test: 'data' },
21
+ importance: 'should-investigate',
22
+ });
23
+ const context = createContext(
24
+ new EventAssertionError(envelope, 'Invalid event'),
25
+ );
18
26
 
19
27
  const decision = policy.shouldRetry(context);
20
28
 
@@ -120,9 +120,12 @@ export class SchemaRegistry {
120
120
 
121
121
  const def: SubscriberDefinition = {
122
122
  name: subscriber.name,
123
- description: subscriber.description,
124
123
  idempotent: subscriber.idempotent ?? 'unknown',
125
124
  importance: subscriber.importance ?? 'should-investigate',
125
+ ...('description' in subscriber &&
126
+ subscriber.description !== undefined && {
127
+ description: subscriber.description,
128
+ }),
126
129
  };
127
130
 
128
131
  if (subscriber.targetQueue !== undefined) {
@@ -225,7 +225,6 @@ describe('installPlugins', () => {
225
225
  });
226
226
  const stubSub = createSubscriberStub({
227
227
  name: 'remote-analytics',
228
- description: 'Remote analytics stub',
229
228
  targetQueue: 'analytics-worker',
230
229
  });
231
230
 
@@ -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,6 +272,58 @@ 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
+ });
274
327
  });
275
328
 
276
329
  describe('addQueue with QueueDefinition object', () => {
@@ -359,3 +412,40 @@ describe('TopologyBuilder', () => {
359
412
  });
360
413
  });
361
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
+ );
450
+ });
451
+ });
@@ -179,7 +179,8 @@ export class TopologyBuilder {
179
179
  for (const queue of this.queues) {
180
180
  if (!queue.name || queue.name.trim() === '') {
181
181
  issues.push('Queue name cannot be empty');
182
- } else if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(queue.name)) {
182
+ } else if (!queue.exact && !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(queue.name)) {
183
+ // Skip pattern validation for exact queues (allows names like 'matador.shared.id-platform')
183
184
  issues.push(
184
185
  `Queue name "${queue.name}" must start with a letter and contain only alphanumeric characters, underscores, and hyphens`,
185
186
  );
@@ -2,13 +2,17 @@ export type {
2
2
  DeadLetterConfig,
3
3
  DeadLetterQueueConfig,
4
4
  QueueDefinition,
5
+ RabbitMQQueueDefinition,
6
+ RabbitMQQueueOptions,
5
7
  RetryConfig,
6
8
  Topology,
9
+ TransportQueueOptions,
7
10
  } from './types.js';
8
11
  export {
9
12
  getDeadLetterQueueName,
10
13
  getQualifiedQueueName,
11
14
  getRetryQueueName,
15
+ resolveQueueName,
12
16
  } from './types.js';
13
17
 
14
18
  export type { QueueOptions } from './builder.js';
@@ -20,7 +20,7 @@ export interface Topology {
20
20
  * Individual queue definition.
21
21
  */
22
22
  export interface QueueDefinition {
23
- /** Queue name (will be prefixed with namespace) */
23
+ /** Queue name (will be prefixed with namespace unless exact: true) */
24
24
  readonly name: string;
25
25
 
26
26
  /** Concurrency for this queue */
@@ -39,6 +39,66 @@ export interface QueueDefinition {
39
39
  * queues that are not managed by Matador.
40
40
  */
41
41
  readonly exact?: boolean | undefined;
42
+
43
+ /** Transport-specific queue options */
44
+ readonly transport?: TransportQueueOptions | undefined;
45
+ }
46
+
47
+ /**
48
+ * Transport-specific queue options.
49
+ * Each transport can define its own options under its transport name key.
50
+ */
51
+ export interface TransportQueueOptions {
52
+ /** RabbitMQ-specific queue options */
53
+ readonly rabbitmq?: RabbitMQQueueDefinition | undefined;
54
+ }
55
+
56
+ /**
57
+ * RabbitMQ-specific queue definition options.
58
+ */
59
+ export interface RabbitMQQueueDefinition {
60
+ /**
61
+ * Exact RabbitMQ queue assertion options.
62
+ * When provided, these options completely replace all auto-computed defaults
63
+ * (durable, x-queue-type, x-dead-letter-exchange, etc.).
64
+ */
65
+ readonly options?: RabbitMQQueueOptions | undefined;
66
+ }
67
+
68
+ /**
69
+ * RabbitMQ queue assertion options.
70
+ * Maps to amqplib's Options.AssertQueue.
71
+ */
72
+ export interface RabbitMQQueueOptions {
73
+ /** Queue survives broker restart */
74
+ readonly durable?: boolean | undefined;
75
+
76
+ /** Queue is deleted when last consumer unsubscribes */
77
+ readonly autoDelete?: boolean | undefined;
78
+
79
+ /** Queue can only be used by the declaring connection */
80
+ readonly exclusive?: boolean | undefined;
81
+
82
+ /** Exchange to which dead-lettered messages are sent */
83
+ readonly deadLetterExchange?: string | undefined;
84
+
85
+ /** Routing key for dead-lettered messages */
86
+ readonly deadLetterRoutingKey?: string | undefined;
87
+
88
+ /** Message TTL in milliseconds */
89
+ readonly messageTtl?: number | undefined;
90
+
91
+ /** Queue expires after this many milliseconds of non-use */
92
+ readonly expires?: number | undefined;
93
+
94
+ /** Maximum number of messages in the queue */
95
+ readonly maxLength?: number | undefined;
96
+
97
+ /** Maximum priority level (0-255) */
98
+ readonly maxPriority?: number | undefined;
99
+
100
+ /** Additional x-* arguments for RabbitMQ */
101
+ readonly arguments?: Record<string, unknown> | undefined;
42
102
  }
43
103
 
44
104
  /**
@@ -107,3 +167,17 @@ export function getRetryQueueName(
107
167
  ): string {
108
168
  return `${namespace}.${queueName}.retry`;
109
169
  }
170
+
171
+ /**
172
+ * Resolves the actual queue name for a given queue definition.
173
+ * When exact: true, returns name as-is. Otherwise, returns namespace.name.
174
+ */
175
+ export function resolveQueueName(
176
+ namespace: string,
177
+ queueDef: QueueDefinition,
178
+ ): string {
179
+ if (queueDef.exact) {
180
+ return queueDef.name;
181
+ }
182
+ return `${namespace}.${queueDef.name}`;
183
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, mock } from 'bun:test';
2
2
  import type { Logger } from '../../hooks/index.js';
3
- import { redactAmqpUrl, RabbitMQTransport } from './rabbitmq-transport.js';
3
+ import { RabbitMQTransport, redactAmqpUrl } from './rabbitmq-transport.js';
4
4
 
5
5
  describe('redactAmqpUrl', () => {
6
6
  it('should redact username and password with 4 asterisks', () => {
@@ -604,36 +604,44 @@ export class RabbitMQTransport implements Transport {
604
604
  ? queueDef.name
605
605
  : `${topology.namespace}.${queueDef.name}`;
606
606
 
607
- const queueOptions: Options.AssertQueue = {
608
- durable: true,
609
- arguments: {} as Record<string, unknown>,
610
- };
607
+ const rabbitmqOptions = queueDef.transport?.rabbitmq?.options;
608
+
609
+ // If user provided exact RabbitMQ options, use them directly (replaces all defaults)
610
+ if (rabbitmqOptions) {
611
+ await channel.assertQueue(queueName, rabbitmqOptions);
612
+ } else {
613
+ // Use computed defaults
614
+ const queueOptions: Options.AssertQueue = {
615
+ durable: true,
616
+ arguments: {} as Record<string, unknown>,
617
+ };
611
618
 
612
- // Use quorum queues for durability
613
- if (this.config.quorumQueues && !queueDef.exact) {
614
- queueOptions.arguments['x-queue-type'] = 'quorum';
615
- }
619
+ // Use quorum queues for durability
620
+ if (this.config.quorumQueues && !queueDef.exact) {
621
+ queueOptions.arguments['x-queue-type'] = 'quorum';
622
+ }
616
623
 
617
- // Set up dead-letter exchange routing
618
- const dlxExchange = this.getDLXExchangeName(topology.namespace);
619
- if (
620
- topology.deadLetter.unhandled.enabled ||
621
- topology.deadLetter.undeliverable.enabled
622
- ) {
623
- queueOptions.arguments['x-dead-letter-exchange'] = dlxExchange;
624
- }
624
+ // Set up dead-letter exchange routing
625
+ const dlxExchange = this.getDLXExchangeName(topology.namespace);
626
+ if (
627
+ topology.deadLetter.unhandled.enabled ||
628
+ topology.deadLetter.undeliverable.enabled
629
+ ) {
630
+ queueOptions.arguments['x-dead-letter-exchange'] = dlxExchange;
631
+ }
625
632
 
626
- // Enable priority if requested
627
- if (queueDef.priorities) {
628
- queueOptions.arguments['x-max-priority'] = 10;
629
- }
633
+ // Enable priority if requested
634
+ if (queueDef.priorities) {
635
+ queueOptions.arguments['x-max-priority'] = 10;
636
+ }
630
637
 
631
- // Set consumer timeout if specified
632
- if (queueDef.consumerTimeout) {
633
- queueOptions.arguments['x-consumer-timeout'] = queueDef.consumerTimeout;
634
- }
638
+ // Set consumer timeout if specified
639
+ if (queueDef.consumerTimeout) {
640
+ queueOptions.arguments['x-consumer-timeout'] = queueDef.consumerTimeout;
641
+ }
635
642
 
636
- await channel.assertQueue(queueName, queueOptions);
643
+ await channel.assertQueue(queueName, queueOptions);
644
+ }
637
645
 
638
646
  // Bind queue to main exchange
639
647
  const mainExchange = this.getMainExchangeName(topology.namespace);
@@ -0,0 +1,18 @@
1
+ import type { SendResult } from '../core/fanout.js';
2
+ import type { Event, EventClass, EventOptions } from './event.js';
3
+
4
+ /**
5
+ * Interface for dispatching events.
6
+ * Implemented by Matador to allow subscribers to send events.
7
+ */
8
+ export interface Dispatcher {
9
+ /**
10
+ * Sends an event to all registered subscribers.
11
+ */
12
+ send<T>(
13
+ eventClass: EventClass<T>,
14
+ data: T,
15
+ options?: EventOptions,
16
+ ): Promise<SendResult>;
17
+ send<T>(event: Event<T>, options?: EventOptions): Promise<SendResult>;
18
+ }
@@ -150,3 +150,26 @@ export function createEnvelope<T>(
150
150
  },
151
151
  };
152
152
  }
153
+
154
+ /**
155
+ * Helper to create a test envelope for a given event instance.
156
+ * Useful for unit testing subscriber callbacks directly.
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * const event = new UserCreatedEvent({ userId: '123', email: 'test@example.com' });
161
+ * const envelope = createDummyEnvelope(event);
162
+ * await mySubscriber.callback(envelope, event);
163
+ * ```
164
+ */
165
+ export function createDummyEnvelope<T>(event: {
166
+ data: T;
167
+ constructor: { key: string };
168
+ }): Envelope<T> {
169
+ return createEnvelope({
170
+ data: event.data,
171
+ eventKey: event.constructor.key,
172
+ targetSubscriber: 'dummy-subscriber',
173
+ importance: 'can-ignore',
174
+ });
175
+ }
@@ -7,8 +7,10 @@ export type {
7
7
  } from './common.js';
8
8
  export { invalidResult, validResult } from './common.js';
9
9
 
10
+ export type { Dispatcher } from './dispatcher.js';
11
+
10
12
  export type { CreateEnvelopeOptions, Docket, Envelope } from './envelope.js';
11
- export { createEnvelope } from './envelope.js';
13
+ export { createDummyEnvelope, createEnvelope } from './envelope.js';
12
14
 
13
15
  export type {
14
16
  Event,
@@ -26,11 +28,13 @@ export { MatadorEvent } from './event.js';
26
28
  export type {
27
29
  AnySubscriber,
28
30
  BaseSubscriberOptions,
31
+ CallbackContext,
29
32
  CreateResumableSubscriberInput,
30
33
  CreateStandardSubscriberInput,
31
34
  CreateSubscriberInput,
32
35
  EnvelopeOf,
33
36
  ResumableCallback,
37
+ ResumableCallbackContext,
34
38
  ResumableSubscriber,
35
39
  ResumableSubscriberOptions,
36
40
  StandardCallback,
@@ -1,8 +1,18 @@
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';
5
6
 
7
+ /**
8
+ * Context passed to subscriber callbacks.
9
+ * Provides access to the Matador instance for sending additional events.
10
+ */
11
+ export interface CallbackContext {
12
+ /** Matador dispatcher for sending additional events from within a subscriber */
13
+ readonly matador: Dispatcher;
14
+ }
15
+
6
16
  /**
7
17
  * Helper type to get the envelope type for a subscriber callback.
8
18
  * Extracts the data type from a MatadorEvent and wraps it in an Envelope.
@@ -16,20 +26,35 @@ export type EnvelopeOf<T extends MatadorEvent> = Envelope<T['data']>;
16
26
 
17
27
  /**
18
28
  * Callback function executed when an event is received (standard subscribers).
19
- * Receives the full envelope containing id, data, and docket.
29
+ * Receives the full envelope containing id, data, and docket, plus a context
30
+ * with access to the matador instance for sending additional events.
31
+ *
32
+ * The callback may optionally return a value, which will be included in the
33
+ * onWorkerSuccess hook context for logging or observability purposes.
20
34
  */
21
35
  export type StandardCallback<T = unknown> = (
22
36
  envelope: Envelope<T>,
23
- ) => Promise<void> | void;
37
+ context: CallbackContext,
38
+ ) => Promise<unknown> | unknown | void;
39
+
40
+ /**
41
+ * Context for resumable subscriber callbacks.
42
+ * Combines checkpoint operations (io, all) with matador access.
43
+ */
44
+ export type ResumableCallbackContext = SubscriberContext & CallbackContext;
24
45
 
25
46
  /**
26
47
  * Callback function for resumable subscribers.
27
- * Receives the envelope and a SubscriberContext with io() for checkpointed operations.
48
+ * Receives the envelope and a context with io() for checkpointed operations
49
+ * and matador for sending additional events.
50
+ *
51
+ * The callback may optionally return a value, which will be included in the
52
+ * onWorkerSuccess hook context for logging or observability purposes.
28
53
  */
29
54
  export type ResumableCallback<T = unknown> = (
30
55
  envelope: Envelope<T>,
31
- context: SubscriberContext,
32
- ) => Promise<void> | void;
56
+ context: ResumableCallbackContext,
57
+ ) => Promise<unknown> | unknown | void;
33
58
 
34
59
  /**
35
60
  * Callback function executed when an event is received.
@@ -114,7 +139,8 @@ export type Subscriber<T extends MatadorEvent> =
114
139
  * is in a remote service. Declares the subscriber contract without providing
115
140
  * the callback.
116
141
  */
117
- export interface SubscriberStub extends StandardSubscriberOptions {
142
+ export interface SubscriberStub
143
+ extends Omit<StandardSubscriberOptions, 'description'> {
118
144
  /** Human-readable name for the subscriber */
119
145
  readonly name: string;
120
146
 
@@ -263,7 +289,6 @@ export function createSubscriber<T extends MatadorEvent>(
263
289
  */
264
290
  export interface CreateSubscriberStubInput {
265
291
  readonly name: string;
266
- readonly description: string;
267
292
  readonly idempotent?: 'yes' | 'no' | 'unknown' | undefined;
268
293
  readonly importance?: Importance | undefined;
269
294
  readonly targetQueue?: string | undefined;
@@ -277,7 +302,6 @@ export interface CreateSubscriberStubInput {
277
302
  * ```typescript
278
303
  * const stub = createSubscriberStub({
279
304
  * name: 'remote-analytics',
280
- * description: 'Sends events to remote analytics service',
281
305
  * targetQueue: 'analytics-worker',
282
306
  * });
283
307
  * ```
@@ -287,7 +311,6 @@ export function createSubscriberStub(
287
311
  ): SubscriberStub {
288
312
  return {
289
313
  name: input.name,
290
- description: input.description,
291
314
  isStub: true,
292
315
  idempotent: input.idempotent ?? 'unknown',
293
316
  importance: input.importance ?? 'should-investigate',
@@ -303,7 +326,7 @@ export function createSubscriberStub(
303
326
  */
304
327
  export interface SubscriberDefinition {
305
328
  readonly name: string;
306
- readonly description: string;
329
+ readonly description?: string | undefined;
307
330
  readonly idempotent: Idempotency;
308
331
  readonly importance: Importance;
309
332
  readonly targetQueue?: string | undefined;