@zdavison/matador 3.0.3 → 3.0.6
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/cli.ts +66 -52
- package/dist/checkpoint/context.test.js +4 -0
- package/dist/codec/rabbitmq-codec.test.js +36 -41
- package/dist/core/fanout.test.js +12 -2
- package/dist/index.cjs +89 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/pipeline.test.js +16 -4
- package/dist/schema/types.test.js +4 -0
- package/dist/topology/builder.d.ts.map +1 -1
- package/dist/topology/builder.js +66 -41
- package/dist/transport/local/local-transport.test.js +6 -2
- package/dist/transport/multi/multi-transport.d.ts.map +1 -1
- package/dist/transport/multi/multi-transport.js +2 -0
- package/dist/transport/multi/multi-transport.test.js +6 -2
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +1 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +23 -30
- package/package.json +1 -1
package/cli.ts
CHANGED
|
@@ -252,26 +252,75 @@ async function runSendTestEvent(
|
|
|
252
252
|
await dispatchEvent(config, config.testEvent, options);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
config
|
|
257
|
-
eventSpec: EventExport,
|
|
258
|
-
options: { dryRun: boolean; timeout: number; verbose: boolean },
|
|
259
|
-
): Promise<void> {
|
|
260
|
-
// Validate event exists in config
|
|
261
|
-
const schemaEntry = config.schema[eventSpec.eventKey];
|
|
255
|
+
function resolveSchemaEntry(config: ConfigExport, eventKey: string) {
|
|
256
|
+
const schemaEntry = config.schema[eventKey];
|
|
262
257
|
if (!schemaEntry) {
|
|
263
|
-
logError(`Event "${
|
|
258
|
+
logError(`Event "${eventKey}" not found in config`);
|
|
264
259
|
logInfo(`Available events: ${Object.keys(config.schema).join(', ')}`);
|
|
265
260
|
process.exit(1);
|
|
266
261
|
}
|
|
267
|
-
|
|
268
|
-
// Extract EventClass and subscribers from schema entry
|
|
269
262
|
const EventClass = isSchemaEntryTuple(schemaEntry)
|
|
270
263
|
? schemaEntry[0]
|
|
271
264
|
: schemaEntry.eventClass;
|
|
272
265
|
const subscribers = isSchemaEntryTuple(schemaEntry)
|
|
273
266
|
? schemaEntry[1]
|
|
274
267
|
: schemaEntry.subscribers;
|
|
268
|
+
return { EventClass, subscribers };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function logEventDetails(eventSpec: EventExport): void {
|
|
272
|
+
logInfo(`Data: ${JSON.stringify(eventSpec.data, null, 2)}`);
|
|
273
|
+
if (eventSpec.before) {
|
|
274
|
+
logInfo(`Before: ${JSON.stringify(eventSpec.before, null, 2)}`);
|
|
275
|
+
}
|
|
276
|
+
if (eventSpec.options) {
|
|
277
|
+
logInfo(`Options: ${JSON.stringify(eventSpec.options, null, 2)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function logSendResult(result: {
|
|
282
|
+
eventKey: string;
|
|
283
|
+
subscribersSent: number;
|
|
284
|
+
subscribersSkipped: number;
|
|
285
|
+
errors: ReadonlyArray<{ subscriberName: string; error: Error }>;
|
|
286
|
+
}): void {
|
|
287
|
+
logSection('Send Result');
|
|
288
|
+
logInfo(`Event key: ${result.eventKey}`);
|
|
289
|
+
logInfo(`Subscribers sent: ${result.subscribersSent}`);
|
|
290
|
+
logInfo(`Subscribers skipped: ${result.subscribersSkipped}`);
|
|
291
|
+
|
|
292
|
+
if (result.errors.length > 0) {
|
|
293
|
+
logWarning(`Dispatch errors: ${result.errors.length}`);
|
|
294
|
+
for (const err of result.errors) {
|
|
295
|
+
logError(` [${err.subscriberName}] ${err.error.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function buildCliHooks(config: ConfigExport, verbose: boolean): MatadorHooks {
|
|
301
|
+
return {
|
|
302
|
+
logger: verbose ? consoleLogger : undefined,
|
|
303
|
+
onWorkerSuccess: (ctx) => {
|
|
304
|
+
logSuccess(`[${ctx.subscriber.name}] processed in ${ctx.durationMs}ms`);
|
|
305
|
+
},
|
|
306
|
+
onWorkerError: (ctx) => {
|
|
307
|
+
logError(
|
|
308
|
+
`[${ctx.subscriber.name}] failed after ${ctx.durationMs}ms: ${ctx.error.message}`,
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
...config.hooks,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function dispatchEvent(
|
|
316
|
+
config: ConfigExport,
|
|
317
|
+
eventSpec: EventExport,
|
|
318
|
+
options: { dryRun: boolean; timeout: number; verbose: boolean },
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
const { EventClass, subscribers } = resolveSchemaEntry(
|
|
321
|
+
config,
|
|
322
|
+
eventSpec.eventKey,
|
|
323
|
+
);
|
|
275
324
|
|
|
276
325
|
if (!subscribers || subscribers.length === 0) {
|
|
277
326
|
logWarning(`No subscribers registered for event "${eventSpec.eventKey}"`);
|
|
@@ -288,7 +337,6 @@ async function dispatchEvent(
|
|
|
288
337
|
|
|
289
338
|
logSection('Dispatching Event');
|
|
290
339
|
|
|
291
|
-
// Create topology
|
|
292
340
|
const topology =
|
|
293
341
|
config.topology ??
|
|
294
342
|
TopologyBuilder.create()
|
|
@@ -297,24 +345,9 @@ async function dispatchEvent(
|
|
|
297
345
|
.withoutDeadLetter()
|
|
298
346
|
.build();
|
|
299
347
|
|
|
300
|
-
// Create transport
|
|
301
348
|
const transport = new LocalTransport();
|
|
349
|
+
const hooks = buildCliHooks(config, options.verbose);
|
|
302
350
|
|
|
303
|
-
// Create hooks for logging
|
|
304
|
-
const hooks: MatadorHooks = {
|
|
305
|
-
logger: options.verbose ? consoleLogger : undefined,
|
|
306
|
-
onWorkerSuccess: (ctx) => {
|
|
307
|
-
logSuccess(`[${ctx.subscriber.name}] processed in ${ctx.durationMs}ms`);
|
|
308
|
-
},
|
|
309
|
-
onWorkerError: (ctx) => {
|
|
310
|
-
logError(
|
|
311
|
-
`[${ctx.subscriber.name}] failed after ${ctx.durationMs}ms: ${ctx.error.message}`,
|
|
312
|
-
);
|
|
313
|
-
},
|
|
314
|
-
...config.hooks,
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
// Create Matador instance with schema and hooks
|
|
318
351
|
const matador = new Matador(
|
|
319
352
|
{
|
|
320
353
|
transport,
|
|
@@ -329,35 +362,16 @@ async function dispatchEvent(
|
|
|
329
362
|
await matador.start();
|
|
330
363
|
logSuccess('[Matador] Started.');
|
|
331
364
|
|
|
332
|
-
// Create and dispatch the event
|
|
333
365
|
const event = new EventClass(eventSpec.data, eventSpec.before);
|
|
334
366
|
logInfo(`Dispatching: ${eventSpec.eventKey}`);
|
|
335
367
|
|
|
336
368
|
if (options.verbose) {
|
|
337
|
-
|
|
338
|
-
if (eventSpec.before) {
|
|
339
|
-
logInfo(`Before: ${JSON.stringify(eventSpec.before, null, 2)}`);
|
|
340
|
-
}
|
|
341
|
-
if (eventSpec.options) {
|
|
342
|
-
logInfo(`Options: ${JSON.stringify(eventSpec.options, null, 2)}`);
|
|
343
|
-
}
|
|
369
|
+
logEventDetails(eventSpec);
|
|
344
370
|
}
|
|
345
371
|
|
|
346
372
|
const result = await matador.send(event, eventSpec.options);
|
|
373
|
+
logSendResult(result);
|
|
347
374
|
|
|
348
|
-
logSection('Send Result');
|
|
349
|
-
logInfo(`Event key: ${result.eventKey}`);
|
|
350
|
-
logInfo(`Subscribers sent: ${result.subscribersSent}`);
|
|
351
|
-
logInfo(`Subscribers skipped: ${result.subscribersSkipped}`);
|
|
352
|
-
|
|
353
|
-
if (result.errors.length > 0) {
|
|
354
|
-
logWarning(`Dispatch errors: ${result.errors.length}`);
|
|
355
|
-
for (const err of result.errors) {
|
|
356
|
-
logError(` [${err.subscriberName}] ${err.error.message}`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Wait for processing
|
|
361
375
|
logSection('Processing');
|
|
362
376
|
const idle = await matador.waitForIdle(options.timeout);
|
|
363
377
|
|
|
@@ -413,8 +427,8 @@ async function main(): Promise<void> {
|
|
|
413
427
|
process.exit(1);
|
|
414
428
|
}
|
|
415
429
|
|
|
416
|
-
const [configPath, eventPath] = positionals;
|
|
417
|
-
await runSend(configPath
|
|
430
|
+
const [configPath, eventPath] = positionals as [string, string];
|
|
431
|
+
await runSend(configPath, eventPath, {
|
|
418
432
|
dryRun: values['dry-run'] ?? false,
|
|
419
433
|
timeout: Number.parseInt(values.timeout ?? '5000', 10),
|
|
420
434
|
verbose: values.verbose ?? false,
|
|
@@ -436,8 +450,8 @@ async function main(): Promise<void> {
|
|
|
436
450
|
process.exit(1);
|
|
437
451
|
}
|
|
438
452
|
|
|
439
|
-
const [configPath] = positionals;
|
|
440
|
-
await runSendTestEvent(configPath
|
|
453
|
+
const [configPath] = positionals as [string];
|
|
454
|
+
await runSendTestEvent(configPath, {
|
|
441
455
|
dryRun: values['dry-run'] ?? false,
|
|
442
456
|
timeout: Number.parseInt(values.timeout ?? '5000', 10),
|
|
443
457
|
verbose: values.verbose ?? false,
|
|
@@ -119,9 +119,13 @@ describe('ResumableContext', () => {
|
|
|
119
119
|
});
|
|
120
120
|
await context.io('step-1', () => 'result-1');
|
|
121
121
|
let checkpoint = await store.get(envelope.id);
|
|
122
|
+
if (!checkpoint)
|
|
123
|
+
throw new Error('expected checkpoint after step-1');
|
|
122
124
|
expect(Object.keys(checkpoint.completedSteps)).toHaveLength(1);
|
|
123
125
|
await context.io('step-2', () => 'result-2');
|
|
124
126
|
checkpoint = await store.get(envelope.id);
|
|
127
|
+
if (!checkpoint)
|
|
128
|
+
throw new Error('expected checkpoint after step-2');
|
|
125
129
|
expect(Object.keys(checkpoint.completedSteps)).toHaveLength(2);
|
|
126
130
|
});
|
|
127
131
|
it('should support various JSON-serializable return types', async () => {
|
|
@@ -323,6 +323,37 @@ const V1_MESSAGE_FIXTURES = [
|
|
|
323
323
|
// },
|
|
324
324
|
// },
|
|
325
325
|
];
|
|
326
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
327
|
+
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
|
|
328
|
+
function assertV1Id(envelope, expectedId) {
|
|
329
|
+
if (expectedId !== undefined) {
|
|
330
|
+
expect(envelope.id).toBe(expectedId);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
expect(envelope.id).toMatch(UUID_PATTERN);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function assertV1Metadata(metadata, expected) {
|
|
337
|
+
if (expected !== undefined) {
|
|
338
|
+
expect(metadata).toEqual(expected);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
expect(metadata === undefined || Object.keys(metadata).length === 0).toBe(true);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function assertV1Translation(envelope, expected) {
|
|
345
|
+
expect(envelope.docket.eventKey).toBe(expected.eventKey);
|
|
346
|
+
expect(envelope.docket.targetSubscriber).toBe(expected.targetSubscriber);
|
|
347
|
+
expect(envelope.data).toEqual(expected.data);
|
|
348
|
+
assertV1Id(envelope, expected.id);
|
|
349
|
+
assertV1Metadata(envelope.docket.metadata, expected.metadata);
|
|
350
|
+
if (expected.correlationId !== undefined) {
|
|
351
|
+
expect(envelope.docket.correlationId).toBe(expected.correlationId);
|
|
352
|
+
}
|
|
353
|
+
expect(envelope.docket.attempts).toBe(expected.attempts ?? 1);
|
|
354
|
+
expect(envelope.docket.importance).toBe(expected.importance ?? 'should-investigate');
|
|
355
|
+
expect(envelope.docket.createdAt).toMatch(ISO_DATE_PATTERN);
|
|
356
|
+
}
|
|
326
357
|
describe('RabbitMQCodec', () => {
|
|
327
358
|
const codec = new RabbitMQCodec();
|
|
328
359
|
describe('v1 to v2 translation', () => {
|
|
@@ -331,45 +362,7 @@ describe('RabbitMQCodec', () => {
|
|
|
331
362
|
const body = new TextEncoder().encode(JSON.stringify(fixture.body));
|
|
332
363
|
const headers = fixture.headers ?? {};
|
|
333
364
|
const envelope = codec.decode(body, headers);
|
|
334
|
-
|
|
335
|
-
expect(envelope.docket.eventKey).toBe(fixture.expected.eventKey);
|
|
336
|
-
expect(envelope.docket.targetSubscriber).toBe(fixture.expected.targetSubscriber);
|
|
337
|
-
expect(envelope.data).toEqual(fixture.expected.data);
|
|
338
|
-
// Check optional ID (may be generated if not specified)
|
|
339
|
-
if (fixture.expected.id !== undefined) {
|
|
340
|
-
expect(envelope.id).toBe(fixture.expected.id);
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
expect(envelope.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
344
|
-
}
|
|
345
|
-
// Check metadata
|
|
346
|
-
if (fixture.expected.metadata !== undefined) {
|
|
347
|
-
expect(envelope.docket.metadata).toEqual(fixture.expected.metadata);
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
expect(envelope.docket.metadata === undefined ||
|
|
351
|
-
Object.keys(envelope.docket.metadata).length === 0).toBe(true);
|
|
352
|
-
}
|
|
353
|
-
// Check correlation ID
|
|
354
|
-
if (fixture.expected.correlationId !== undefined) {
|
|
355
|
-
expect(envelope.docket.correlationId).toBe(fixture.expected.correlationId);
|
|
356
|
-
}
|
|
357
|
-
// Check attempts (defaults to 1)
|
|
358
|
-
if (fixture.expected.attempts !== undefined) {
|
|
359
|
-
expect(envelope.docket.attempts).toBe(fixture.expected.attempts);
|
|
360
|
-
}
|
|
361
|
-
else {
|
|
362
|
-
expect(envelope.docket.attempts).toBe(1);
|
|
363
|
-
}
|
|
364
|
-
// Check importance (defaults to 'should-investigate')
|
|
365
|
-
if (fixture.expected.importance !== undefined) {
|
|
366
|
-
expect(envelope.docket.importance).toBe(fixture.expected.importance);
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
expect(envelope.docket.importance).toBe('should-investigate');
|
|
370
|
-
}
|
|
371
|
-
// Check createdAt is a valid ISO string
|
|
372
|
-
expect(envelope.docket.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/);
|
|
365
|
+
assertV1Translation(envelope, fixture.expected);
|
|
373
366
|
});
|
|
374
367
|
}
|
|
375
368
|
});
|
|
@@ -385,8 +378,10 @@ describe('RabbitMQCodec', () => {
|
|
|
385
378
|
const before = Date.now();
|
|
386
379
|
const envelope = codec.decode(encoded, {});
|
|
387
380
|
const after = Date.now();
|
|
388
|
-
|
|
389
|
-
|
|
381
|
+
const { scheduledFor } = envelope.docket;
|
|
382
|
+
if (!scheduledFor)
|
|
383
|
+
throw new Error('expected scheduledFor to be set');
|
|
384
|
+
const scheduledTime = new Date(scheduledFor).getTime();
|
|
390
385
|
expect(scheduledTime).toBeGreaterThanOrEqual(before + 5000);
|
|
391
386
|
expect(scheduledTime).toBeLessThanOrEqual(after + 5000);
|
|
392
387
|
});
|
package/dist/core/fanout.test.js
CHANGED
|
@@ -151,6 +151,8 @@ describe('FanoutEngine', () => {
|
|
|
151
151
|
await fanout.send(UserCreatedEvent, event);
|
|
152
152
|
const sendCall = transport.send.mock
|
|
153
153
|
.calls[0];
|
|
154
|
+
if (!sendCall)
|
|
155
|
+
throw new Error('expected transport.send to be called');
|
|
154
156
|
const envelope = sendCall[1];
|
|
155
157
|
expect(envelope.docket.eventKey).toBe('user.created');
|
|
156
158
|
expect(envelope.docket.eventDescription).toBe('Fired when a new user is created');
|
|
@@ -176,6 +178,8 @@ describe('FanoutEngine', () => {
|
|
|
176
178
|
await fanout.send(OrderPlacedEvent, event);
|
|
177
179
|
const sendCall = transport.send.mock
|
|
178
180
|
.calls[0];
|
|
181
|
+
if (!sendCall)
|
|
182
|
+
throw new Error('expected transport.send to be called');
|
|
179
183
|
const envelope = sendCall[1];
|
|
180
184
|
expect(envelope.docket.eventKey).toBe('order.placed');
|
|
181
185
|
expect(envelope.docket.eventDescription).toBe('Fired when an order is placed');
|
|
@@ -819,7 +823,10 @@ describe('FanoutEngine', () => {
|
|
|
819
823
|
transport: 'mock',
|
|
820
824
|
});
|
|
821
825
|
const calls = onEnqueueSuccess.mock.calls;
|
|
822
|
-
const
|
|
826
|
+
const firstCall = calls[0];
|
|
827
|
+
if (!firstCall)
|
|
828
|
+
throw new Error('expected onEnqueueSuccess to be called');
|
|
829
|
+
const callArgs = firstCall[0];
|
|
823
830
|
expect(callArgs.envelope.docket.eventKey).toBe('user.created');
|
|
824
831
|
expect(callArgs.envelope.docket.targetSubscriber).toBe('handle-user');
|
|
825
832
|
});
|
|
@@ -860,7 +867,10 @@ describe('FanoutEngine', () => {
|
|
|
860
867
|
transport: 'mock',
|
|
861
868
|
});
|
|
862
869
|
const calls = onEnqueueError.mock.calls;
|
|
863
|
-
const
|
|
870
|
+
const firstCall = calls[0];
|
|
871
|
+
if (!firstCall)
|
|
872
|
+
throw new Error('expected onEnqueueError to be called');
|
|
873
|
+
const callArgs = firstCall[0];
|
|
864
874
|
expect(callArgs.envelope.docket.eventKey).toBe('user.created');
|
|
865
875
|
expect(callArgs.error).toBeInstanceOf(TransportSendError);
|
|
866
876
|
});
|
package/dist/index.cjs
CHANGED
|
@@ -471,50 +471,11 @@ var TopologyBuilder = class _TopologyBuilder {
|
|
|
471
471
|
* Validates the topology configuration.
|
|
472
472
|
*/
|
|
473
473
|
validate() {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
"Namespace must start with a letter and contain only alphanumeric characters, underscores, and hyphens"
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
if (this.queues.length === 0) {
|
|
483
|
-
issues.push("At least one queue is required");
|
|
484
|
-
}
|
|
485
|
-
const queueNames = /* @__PURE__ */ new Set();
|
|
486
|
-
for (const queue of this.queues) {
|
|
487
|
-
if (!queue.name || queue.name.trim() === "") {
|
|
488
|
-
issues.push("Queue name cannot be empty");
|
|
489
|
-
} else if (!queue.exact && !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(queue.name)) {
|
|
490
|
-
issues.push(
|
|
491
|
-
`Queue name "${queue.name}" must start with a letter and contain only alphanumeric characters, underscores, and hyphens`
|
|
492
|
-
);
|
|
493
|
-
} else if (queueNames.has(queue.name)) {
|
|
494
|
-
issues.push(`Duplicate queue name: "${queue.name}"`);
|
|
495
|
-
} else {
|
|
496
|
-
queueNames.add(queue.name);
|
|
497
|
-
}
|
|
498
|
-
if (queue.concurrency !== void 0 && queue.concurrency < 1) {
|
|
499
|
-
issues.push(`Queue "${queue.name}" concurrency must be at least 1`);
|
|
500
|
-
}
|
|
501
|
-
if (queue.consumerTimeout !== void 0 && queue.consumerTimeout < 0) {
|
|
502
|
-
issues.push(
|
|
503
|
-
`Queue "${queue.name}" consumer timeout must be non-negative`
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (this.retry.enabled) {
|
|
508
|
-
if (this.retry.defaultDelayMs < 0) {
|
|
509
|
-
issues.push("Default retry delay must be non-negative");
|
|
510
|
-
}
|
|
511
|
-
if (this.retry.maxDelayMs < this.retry.defaultDelayMs) {
|
|
512
|
-
issues.push(
|
|
513
|
-
"Max retry delay must be greater than or equal to default delay"
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return issues;
|
|
474
|
+
return [
|
|
475
|
+
...validateNamespace(this.namespace),
|
|
476
|
+
...validateQueues(this.queues),
|
|
477
|
+
...validateRetry(this.retry)
|
|
478
|
+
];
|
|
518
479
|
}
|
|
519
480
|
/**
|
|
520
481
|
* Builds the topology configuration.
|
|
@@ -536,6 +497,68 @@ var TopologyBuilder = class _TopologyBuilder {
|
|
|
536
497
|
};
|
|
537
498
|
}
|
|
538
499
|
};
|
|
500
|
+
var IDENTIFIER_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
501
|
+
function validateNamespace(namespace) {
|
|
502
|
+
if (!namespace || namespace.trim() === "") {
|
|
503
|
+
return ["Namespace is required"];
|
|
504
|
+
}
|
|
505
|
+
if (!IDENTIFIER_PATTERN.test(namespace)) {
|
|
506
|
+
return [
|
|
507
|
+
"Namespace must start with a letter and contain only alphanumeric characters, underscores, and hyphens"
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
function validateQueueName(queue, seen) {
|
|
513
|
+
if (!queue.name || queue.name.trim() === "") {
|
|
514
|
+
return ["Queue name cannot be empty"];
|
|
515
|
+
}
|
|
516
|
+
if (!queue.exact && !IDENTIFIER_PATTERN.test(queue.name)) {
|
|
517
|
+
return [
|
|
518
|
+
`Queue name "${queue.name}" must start with a letter and contain only alphanumeric characters, underscores, and hyphens`
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
if (seen.has(queue.name)) {
|
|
522
|
+
return [`Duplicate queue name: "${queue.name}"`];
|
|
523
|
+
}
|
|
524
|
+
seen.add(queue.name);
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
function validateQueueLimits(queue) {
|
|
528
|
+
const issues = [];
|
|
529
|
+
if (queue.concurrency !== void 0 && queue.concurrency < 1) {
|
|
530
|
+
issues.push(`Queue "${queue.name}" concurrency must be at least 1`);
|
|
531
|
+
}
|
|
532
|
+
if (queue.consumerTimeout !== void 0 && queue.consumerTimeout < 0) {
|
|
533
|
+
issues.push(`Queue "${queue.name}" consumer timeout must be non-negative`);
|
|
534
|
+
}
|
|
535
|
+
return issues;
|
|
536
|
+
}
|
|
537
|
+
function validateQueues(queues) {
|
|
538
|
+
const issues = [];
|
|
539
|
+
if (queues.length === 0) {
|
|
540
|
+
issues.push("At least one queue is required");
|
|
541
|
+
}
|
|
542
|
+
const seen = /* @__PURE__ */ new Set();
|
|
543
|
+
for (const queue of queues) {
|
|
544
|
+
issues.push(...validateQueueName(queue, seen));
|
|
545
|
+
issues.push(...validateQueueLimits(queue));
|
|
546
|
+
}
|
|
547
|
+
return issues;
|
|
548
|
+
}
|
|
549
|
+
function validateRetry(retry) {
|
|
550
|
+
if (!retry.enabled) return [];
|
|
551
|
+
const issues = [];
|
|
552
|
+
if (retry.defaultDelayMs < 0) {
|
|
553
|
+
issues.push("Default retry delay must be non-negative");
|
|
554
|
+
}
|
|
555
|
+
if (retry.maxDelayMs < retry.defaultDelayMs) {
|
|
556
|
+
issues.push(
|
|
557
|
+
"Max retry delay must be greater than or equal to default delay"
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return issues;
|
|
561
|
+
}
|
|
539
562
|
|
|
540
563
|
// src/types/common.ts
|
|
541
564
|
function validResult() {
|
|
@@ -2645,6 +2668,7 @@ var MultiTransport = class {
|
|
|
2645
2668
|
const errors = [];
|
|
2646
2669
|
for (let i = 0; i < transportOrder.length; i++) {
|
|
2647
2670
|
const transport = transportOrder[i];
|
|
2671
|
+
if (!transport) continue;
|
|
2648
2672
|
try {
|
|
2649
2673
|
return await transport.send(queue, envelope, options);
|
|
2650
2674
|
} catch (error) {
|
|
@@ -3096,31 +3120,30 @@ var RabbitMQTransport = class {
|
|
|
3096
3120
|
});
|
|
3097
3121
|
});
|
|
3098
3122
|
}
|
|
3123
|
+
buildWorkQueueOptions(topology, queueDef) {
|
|
3124
|
+
const queueOptions = {
|
|
3125
|
+
durable: true,
|
|
3126
|
+
arguments: {}
|
|
3127
|
+
};
|
|
3128
|
+
if (this.config.quorumQueues && !queueDef.exact) {
|
|
3129
|
+
queueOptions.arguments["x-queue-type"] = "quorum";
|
|
3130
|
+
}
|
|
3131
|
+
if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
|
|
3132
|
+
queueOptions.arguments["x-dead-letter-exchange"] = this.getDLXExchangeName(topology.namespace);
|
|
3133
|
+
}
|
|
3134
|
+
if (queueDef.priorities) {
|
|
3135
|
+
queueOptions.arguments["x-max-priority"] = 10;
|
|
3136
|
+
}
|
|
3137
|
+
if (queueDef.consumerTimeout) {
|
|
3138
|
+
queueOptions.arguments["x-consumer-timeout"] = queueDef.consumerTimeout;
|
|
3139
|
+
}
|
|
3140
|
+
return queueOptions;
|
|
3141
|
+
}
|
|
3099
3142
|
async assertWorkQueue(channel, topology, queueDef) {
|
|
3100
3143
|
const queueName = queueDef.exact ? queueDef.name : `${topology.namespace}.${queueDef.name}`;
|
|
3101
3144
|
const rabbitmqOptions = queueDef.transport?.rabbitmq?.options;
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
} else {
|
|
3105
|
-
const queueOptions = {
|
|
3106
|
-
durable: true,
|
|
3107
|
-
arguments: {}
|
|
3108
|
-
};
|
|
3109
|
-
if (this.config.quorumQueues && !queueDef.exact) {
|
|
3110
|
-
queueOptions.arguments["x-queue-type"] = "quorum";
|
|
3111
|
-
}
|
|
3112
|
-
const dlxExchange = this.getDLXExchangeName(topology.namespace);
|
|
3113
|
-
if (topology.deadLetter.unhandled.enabled || topology.deadLetter.undeliverable.enabled) {
|
|
3114
|
-
queueOptions.arguments["x-dead-letter-exchange"] = dlxExchange;
|
|
3115
|
-
}
|
|
3116
|
-
if (queueDef.priorities) {
|
|
3117
|
-
queueOptions.arguments["x-max-priority"] = 10;
|
|
3118
|
-
}
|
|
3119
|
-
if (queueDef.consumerTimeout) {
|
|
3120
|
-
queueOptions.arguments["x-consumer-timeout"] = queueDef.consumerTimeout;
|
|
3121
|
-
}
|
|
3122
|
-
await channel.assertQueue(queueName, queueOptions);
|
|
3123
|
-
}
|
|
3145
|
+
const queueOptions = rabbitmqOptions ?? this.buildWorkQueueOptions(topology, queueDef);
|
|
3146
|
+
await channel.assertQueue(queueName, queueOptions);
|
|
3124
3147
|
const mainExchange = this.getMainExchangeName(topology.namespace);
|
|
3125
3148
|
await channel.bindQueue(queueName, mainExchange, queueName);
|
|
3126
3149
|
if (this.delayedExchangeAvailable) {
|