@zdavison/matador 2.0.3 → 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 (49) 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 +1 -0
  4. package/dist/index.cjs +44 -21
  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.map +1 -1
  16. package/dist/topology/builder.js +2 -1
  17. package/dist/topology/builder.test.js +68 -0
  18. package/dist/topology/index.d.ts +2 -2
  19. package/dist/topology/index.d.ts.map +1 -1
  20. package/dist/topology/index.js +1 -1
  21. package/dist/topology/types.d.ts +53 -1
  22. package/dist/topology/types.d.ts.map +1 -1
  23. package/dist/topology/types.js +10 -0
  24. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
  25. package/dist/transport/rabbitmq/rabbitmq-transport.js +30 -22
  26. package/dist/transport/rabbitmq/rabbitmq-transport.test.js +1 -1
  27. package/dist/types/dispatcher.d.ts +14 -0
  28. package/dist/types/dispatcher.d.ts.map +1 -0
  29. package/dist/types/dispatcher.js +1 -0
  30. package/dist/types/index.d.ts +2 -1
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/dist/types/subscriber.d.ts +20 -4
  33. package/dist/types/subscriber.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/core/matador.ts +3 -1
  36. package/src/index.ts +9 -0
  37. package/src/pipeline/pipeline.test.ts +24 -2
  38. package/src/pipeline/pipeline.ts +28 -6
  39. package/src/topology/builder.test.ts +90 -0
  40. package/src/topology/builder.ts +2 -1
  41. package/src/topology/index.ts +4 -0
  42. package/src/topology/types.ts +75 -1
  43. package/src/transport/rabbitmq/rabbitmq-transport.test.ts +1 -1
  44. package/src/transport/rabbitmq/rabbitmq-transport.ts +33 -25
  45. package/src/types/dispatcher.ts +18 -0
  46. package/src/types/index.ts +4 -0
  47. package/src/types/subscriber.ts +22 -3
  48. package/test/e2e/rabbitmq-transport.e2e.test.ts +287 -0
  49. package/tsconfig.tsbuildinfo +1 -1
@@ -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,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
+ }
@@ -7,6 +7,8 @@ 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
13
  export { createEnvelope } from './envelope.js';
12
14
 
@@ -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,19 +26,28 @@ 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.
20
31
  */
21
32
  export type StandardCallback<T = unknown> = (
22
33
  envelope: Envelope<T>,
34
+ context: CallbackContext,
23
35
  ) => Promise<void> | void;
24
36
 
37
+ /**
38
+ * Context for resumable subscriber callbacks.
39
+ * Combines checkpoint operations (io, all) with matador access.
40
+ */
41
+ export type ResumableCallbackContext = SubscriberContext & CallbackContext;
42
+
25
43
  /**
26
44
  * Callback function for resumable subscribers.
27
- * Receives the envelope and a SubscriberContext with io() for checkpointed operations.
45
+ * Receives the envelope and a context with io() for checkpointed operations
46
+ * and matador for sending additional events.
28
47
  */
29
48
  export type ResumableCallback<T = unknown> = (
30
49
  envelope: Envelope<T>,
31
- context: SubscriberContext,
50
+ context: ResumableCallbackContext,
32
51
  ) => Promise<void> | void;
33
52
 
34
53
  /**