@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.
- package/dist/core/matador.d.ts +2 -2
- package/dist/core/matador.d.ts.map +1 -1
- package/dist/core/matador.js +1 -0
- package/dist/index.cjs +44 -21
- 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.map +1 -1
- package/dist/topology/builder.js +2 -1
- package/dist/topology/builder.test.js +68 -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/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +30 -22
- package/dist/transport/rabbitmq/rabbitmq-transport.test.js +1 -1
- 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 +3 -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 +90 -0
- package/src/topology/builder.ts +2 -1
- package/src/topology/index.ts +4 -0
- package/src/topology/types.ts +75 -1
- package/src/transport/rabbitmq/rabbitmq-transport.test.ts +1 -1
- package/src/transport/rabbitmq/rabbitmq-transport.ts +33 -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
|
@@ -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,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
|
+
});
|
package/src/topology/builder.ts
CHANGED
|
@@ -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
|
);
|
package/src/topology/index.ts
CHANGED
|
@@ -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';
|
package/src/topology/types.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
633
|
+
// Enable priority if requested
|
|
634
|
+
if (queueDef.priorities) {
|
|
635
|
+
queueOptions.arguments['x-max-priority'] = 10;
|
|
636
|
+
}
|
|
630
637
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
638
|
+
// Set consumer timeout if specified
|
|
639
|
+
if (queueDef.consumerTimeout) {
|
|
640
|
+
queueOptions.arguments['x-consumer-timeout'] = queueDef.consumerTimeout;
|
|
641
|
+
}
|
|
635
642
|
|
|
636
|
-
|
|
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
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -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,
|
package/src/types/subscriber.ts
CHANGED
|
@@ -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
|
|
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:
|
|
50
|
+
context: ResumableCallbackContext,
|
|
32
51
|
) => Promise<void> | void;
|
|
33
52
|
|
|
34
53
|
/**
|