@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.
- package/dist/core/fanout.test.js +0 -1
- 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/errors/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/retry-errors.d.ts +40 -5
- package/dist/errors/retry-errors.d.ts.map +1 -1
- package/dist/errors/retry-errors.js +56 -9
- package/dist/errors/retry-errors.test.d.ts +2 -0
- package/dist/errors/retry-errors.test.d.ts.map +1 -0
- package/dist/errors/retry-errors.test.js +136 -0
- package/dist/index.cjs +73 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- 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 +48 -1
- package/dist/retry/standard-policy.test.js +7 -1
- package/dist/schema/registry.d.ts.map +1 -1
- package/dist/schema/registry.js +4 -1
- package/dist/schema/types.test.js +0 -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/envelope.d.ts +17 -0
- package/dist/types/envelope.d.ts.map +1 -1
- package/dist/types/envelope.js +19 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/subscriber.d.ts +28 -8
- package/dist/types/subscriber.d.ts.map +1 -1
- package/dist/types/subscriber.js +0 -2
- package/package.json +1 -1
- package/src/core/fanout.test.ts +0 -1
- package/src/core/matador.ts +3 -1
- package/src/errors/index.ts +1 -0
- package/src/errors/retry-errors.test.ts +175 -0
- package/src/errors/retry-errors.ts +63 -9
- package/src/index.ts +11 -0
- package/src/pipeline/pipeline.test.ts +62 -2
- package/src/pipeline/pipeline.ts +28 -6
- package/src/retry/standard-policy.test.ts +9 -1
- package/src/schema/registry.ts +4 -1
- package/src/schema/types.test.ts +0 -1
- 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/envelope.ts +23 -0
- package/src/types/index.ts +5 -1
- package/src/types/subscriber.ts +33 -10
- package/test/e2e/rabbitmq-transport.e2e.test.ts +287 -0
- package/tsconfig.tsbuildinfo +1 -1
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));
|
|
@@ -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
|
|
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
|
|
package/src/schema/registry.ts
CHANGED
|
@@ -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) {
|
package/src/schema/types.test.ts
CHANGED
|
@@ -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/envelope.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -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,
|
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,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
|
-
|
|
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
|
|
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:
|
|
32
|
-
) => Promise<
|
|
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
|
|
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
|
|
329
|
+
readonly description?: string | undefined;
|
|
307
330
|
readonly idempotent: Idempotency;
|
|
308
331
|
readonly importance: Importance;
|
|
309
332
|
readonly targetQueue?: string | undefined;
|