@rocketmq/core 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocketmq/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -9,10 +9,11 @@
9
9
  }
10
10
  },
11
11
  "dependencies": {
12
- "@rocketmq/amqp": "0.1.1",
13
- "@rocketmq/serializer": "0.1.1",
14
- "@rocketmq/schema": "0.1.1",
15
- "@rocketmq/protobuf": "0.1.1"
12
+ "@rocketmq/amqp": "0.1.2",
13
+ "@rocketmq/protobuf": "0.1.2",
14
+ "@rocketmq/schema": "0.1.2",
15
+ "@rocketmq/zod": "0.1.1",
16
+ "@rocketmq/serializer": "0.1.2"
16
17
  },
17
18
  "scripts": {
18
19
  "build": "tsup",
@@ -10,6 +10,7 @@
10
10
  import { Field, Schema, SchemaRegistry } from '@rocketmq/schema';
11
11
  import { JsonSerializer } from '@rocketmq/serializer';
12
12
  import { beforeEach, describe, expect, it, vi } from 'vitest';
13
+ import { z } from 'zod';
13
14
  import { connect, RocketMQ } from './client.js';
14
15
  import { ConnectionError, ConsumeError, PublishError, QueueError } from './errors.js';
15
16
 
@@ -21,6 +22,7 @@ vi.mock('@rocketmq/amqp', async (importOriginal) => {
21
22
  AmqpConnection: {
22
23
  connect: vi.fn().mockResolvedValue({
23
24
  createChannel: vi.fn().mockResolvedValue({
25
+ raw: { on: vi.fn(), removeListener: vi.fn() },
24
26
  assertQueue: vi.fn().mockResolvedValue({ queue: 'q', messageCount: 0, consumerCount: 0 }),
25
27
  close: vi.fn().mockResolvedValue(undefined),
26
28
  }),
@@ -31,7 +33,10 @@ vi.mock('@rocketmq/amqp', async (importOriginal) => {
31
33
  });
32
34
  /** Named fake channel mock. */
33
35
  class FakeAmqpChannel {
34
- raw = this;
36
+ raw = {
37
+ on: vi.fn(),
38
+ removeListener: vi.fn(),
39
+ } as unknown as import('@rocketmq/amqp').AmqpChannel['raw'];
35
40
  assertQueue = vi.fn().mockResolvedValue({ queue: 'q', messageCount: 0, consumerCount: 0 });
36
41
  assertExchange = vi.fn().mockResolvedValue({ exchange: 'ex' });
37
42
  bindQueue = vi.fn().mockResolvedValue({});
@@ -66,6 +71,11 @@ describe('RocketMQ', () => {
66
71
  expect(mq.channel).toBe(ch);
67
72
  });
68
73
 
74
+ it('handles raw channel errors', () => {
75
+ ch.raw.on.mock.calls[0][1](new Error('broker failure'));
76
+ expect((mq as any).lastChannelError).toBeInstanceOf(Error);
77
+ });
78
+
69
79
  describe('assertQueue', () => {
70
80
  it('asserts queue without schema', async () => {
71
81
  await mq.assertQueue('plain-q');
@@ -124,6 +134,24 @@ describe('RocketMQ', () => {
124
134
  );
125
135
  });
126
136
 
137
+ it('supports schemaOverride and schemaDelete options', async () => {
138
+ @Schema()
139
+ class OverOpt {
140
+ @Field()
141
+ id!: string;
142
+ }
143
+ new OverOpt();
144
+
145
+ await mq.assertQueue('opt-q', OverOpt, {
146
+ schemaOverride: true,
147
+ schemaDelete: true,
148
+ });
149
+
150
+ const args = ch.assertQueue.mock.calls[0][1].arguments;
151
+ expect(args['x-schema-override']).toBe(true);
152
+ expect(args['x-schema-delete']).toBe(true);
153
+ });
154
+
127
155
  it('throws QueueError when channel rejects', async () => {
128
156
  @Schema()
129
157
  class QErr {
@@ -232,11 +260,25 @@ describe('RocketMQ', () => {
232
260
  });
233
261
  });
234
262
 
235
- describe('consume (untyped)', () => {
263
+ describe('consume (typed)', () => {
264
+ // Stub schema class for consume tests — schema arg is required.
265
+ class StubMsg {
266
+ id = '';
267
+ }
268
+
236
269
  it('subscribes and returns consumer tag', async () => {
237
270
  const handler = vi.fn();
238
- const tag = await mq.consume('q', handler);
271
+ const tag = await mq.consume('q', StubMsg, handler, {
272
+ arguments: { 'x-priority': 10 },
273
+ });
239
274
  expect(tag).toBe('tag-1');
275
+ expect(ch.consume).toHaveBeenCalledWith(
276
+ 'q',
277
+ expect.any(Function),
278
+ expect.objectContaining({
279
+ arguments: expect.objectContaining({ 'x-priority': 10 }),
280
+ }),
281
+ );
240
282
  });
241
283
 
242
284
  it('deserializes and calls handler', async () => {
@@ -245,7 +287,7 @@ describe('RocketMQ', () => {
245
287
  cb({ content: Buffer.from('{"a":1}'), fields: {}, properties: {} });
246
288
  return { consumerTag: 't' };
247
289
  });
248
- await mq.consume('q', handler);
290
+ await mq.consume('q', StubMsg, handler);
249
291
  expect(handler).toHaveBeenCalledWith({ a: 1 }, expect.anything());
250
292
  });
251
293
 
@@ -255,7 +297,7 @@ describe('RocketMQ', () => {
255
297
  cb(null);
256
298
  return { consumerTag: 't' };
257
299
  });
258
- await mq.consume('q', handler);
300
+ await mq.consume('q', StubMsg, handler);
259
301
  expect(handler).not.toHaveBeenCalled();
260
302
  });
261
303
 
@@ -266,7 +308,7 @@ describe('RocketMQ', () => {
266
308
  cb({ content: Buffer.from('not-json'), fields: {}, properties: {} });
267
309
  return { consumerTag: 't' };
268
310
  });
269
- await mq.consume('q', handler);
311
+ await mq.consume('q', StubMsg, handler);
270
312
  expect(handler).not.toHaveBeenCalled();
271
313
  expect(consoleSpy).toHaveBeenCalled();
272
314
  consoleSpy.mockRestore();
@@ -274,7 +316,62 @@ describe('RocketMQ', () => {
274
316
 
275
317
  it('throws ConsumeError when channel.consume rejects', async () => {
276
318
  ch.consume.mockRejectedValue(new Error('NOT_FOUND'));
277
- await expect(mq.consume('q', vi.fn())).rejects.toThrow(ConsumeError);
319
+ await expect(mq.consume('q', StubMsg, vi.fn())).rejects.toThrow(ConsumeError);
320
+ });
321
+
322
+ it('returns empty args when queue not in registry and no schema provided', async () => {
323
+ const handler = vi.fn();
324
+ await mq.consume('unregistered-fallback', undefined as never, handler);
325
+
326
+ const callArgs = ch.consume.mock.calls[0][2];
327
+ expect(callArgs.arguments['x-consumer-schema-message']).toBeUndefined();
328
+ });
329
+
330
+ it('falls back to registry when schema is undefined', async () => {
331
+ // Create a queue with no schema but with a registry entry
332
+ @Schema()
333
+ class FallbackMsg {
334
+ @Field() id!: string;
335
+ }
336
+ new FallbackMsg();
337
+ mq.assertQueue('fallback-q', FallbackMsg);
338
+
339
+ // Force consume to use the fallback by casting undefined
340
+ const handler = vi.fn();
341
+ await mq.consume('fallback-q', undefined as never, handler);
342
+
343
+ const callArgs = ch.consume.mock.calls[0][2];
344
+ expect(callArgs.arguments['x-consumer-schema-message']).toBe('FallbackMsg');
345
+ });
346
+
347
+ it('returns empty args when fallback registry fails to resolve proto', async () => {
348
+ // A class without fields fails proto resolution
349
+ class EmptyFallbackMsg {}
350
+ registry.register('empty-q', { ctor: EmptyFallbackMsg, name: 'Empty', fields: [] });
351
+
352
+ const handler = vi.fn();
353
+ await mq.consume('empty-q', undefined as never, handler);
354
+
355
+ const callArgs = ch.consume.mock.calls[0][2];
356
+ expect(callArgs.arguments['x-consumer-schema-message']).toBeUndefined();
357
+ });
358
+
359
+ it('returns empty args when resolveConsumerArgs fails', async () => {
360
+ // Explicit schema without fields fails proto resolution
361
+ class EmptyMsg {}
362
+ const handler = vi.fn();
363
+
364
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
365
+ await mq.consume('err-q', EmptyMsg as never, handler);
366
+
367
+ expect(consoleSpy).toHaveBeenCalledWith(
368
+ expect.stringContaining('failed to build consumer schema for EmptyMsg:'),
369
+ expect.any(String),
370
+ );
371
+ consoleSpy.mockRestore();
372
+
373
+ const callArgs = ch.consume.mock.calls[0][2];
374
+ expect(callArgs.arguments['x-consumer-schema-message']).toBeUndefined();
278
375
  });
279
376
  });
280
377
 
@@ -308,20 +405,166 @@ describe('RocketMQ', () => {
308
405
  });
309
406
  });
310
407
 
408
+ describe('RocketMQ (Zod schemas)', () => {
409
+ let ch: FakeAmqpChannel;
410
+ let conn: FakeAmqpConnection;
411
+ let registry: SchemaRegistry;
412
+ let mq: RocketMQ;
413
+
414
+ beforeEach(() => {
415
+ ch = new FakeAmqpChannel();
416
+ conn = new FakeAmqpConnection();
417
+ registry = new SchemaRegistry();
418
+ mq = new RocketMQ(conn as never, ch as never, registry, new JsonSerializer());
419
+ });
420
+
421
+ describe('assertQueue with ZodSchemaInput', () => {
422
+ it('sends proto3 from Zod schema to broker', async () => {
423
+ const zodInput = {
424
+ name: 'ZodNotification',
425
+ schema: z.object({ id: z.string(), content: z.string() }),
426
+ };
427
+
428
+ await mq.assertQueue('zod-q', zodInput);
429
+
430
+ expect(ch.assertQueue).toHaveBeenCalledWith(
431
+ 'zod-q',
432
+ expect.objectContaining({
433
+ arguments: expect.objectContaining({
434
+ 'x-schema': expect.stringContaining('proto3'),
435
+ 'x-schema-type': 'protobuf',
436
+ 'x-schema-message': 'ZodNotification',
437
+ }),
438
+ }),
439
+ );
440
+ });
441
+
442
+ it('generates correct proto field types from Zod', async () => {
443
+ const zodInput = {
444
+ name: 'ZodMixed',
445
+ schema: z.object({
446
+ name: z.string(),
447
+ age: z.number().int(),
448
+ active: z.boolean(),
449
+ }),
450
+ };
451
+
452
+ await mq.assertQueue('zod-mixed', zodInput);
453
+
454
+ const callArgs = ch.assertQueue.mock.calls[0][1];
455
+ const proto = callArgs.arguments['x-schema'];
456
+ expect(proto).toContain('string name = 1;');
457
+ expect(proto).toContain('int32 age = 2;');
458
+ expect(proto).toContain('bool active = 3;');
459
+ });
460
+
461
+ it('does not register in decorator registry for Zod schemas', async () => {
462
+ const zodInput = {
463
+ name: 'ZodOnly',
464
+ schema: z.object({ x: z.string() }),
465
+ };
466
+
467
+ await mq.assertQueue('zod-only', zodInput);
468
+
469
+ // Zod schemas have no constructor to register
470
+ expect(registry.lookup('zod-only')).toBeUndefined();
471
+ });
472
+
473
+ it('throws QueueError when channel rejects Zod schema', async () => {
474
+ const zodInput = {
475
+ name: 'ZodFail',
476
+ schema: z.object({ v: z.string() }),
477
+ };
478
+
479
+ ch.assertQueue.mockRejectedValue(new Error('PRECONDITION_FAILED'));
480
+ await expect(mq.assertQueue('fail-zod', zodInput)).rejects.toThrow(QueueError);
481
+ });
482
+ });
483
+
484
+ describe('consume with ZodSchemaInput', () => {
485
+ it('sends consumer schema args from Zod', async () => {
486
+ const zodInput = {
487
+ name: 'ZodConsumer',
488
+ schema: z.object({ id: z.string() }),
489
+ };
490
+
491
+ const handler = vi.fn();
492
+ await mq.consume('zod-consume', zodInput, handler);
493
+
494
+ expect(ch.consume).toHaveBeenCalledWith(
495
+ 'zod-consume',
496
+ expect.any(Function),
497
+ expect.objectContaining({
498
+ arguments: expect.objectContaining({
499
+ 'x-consumer-schema': expect.stringContaining('proto3'),
500
+ 'x-consumer-schema-message': 'ZodConsumer',
501
+ }),
502
+ }),
503
+ );
504
+ });
505
+
506
+ it('subscribes and returns consumer tag with Zod schema', async () => {
507
+ const zodInput = {
508
+ name: 'ZodTag',
509
+ schema: z.object({ id: z.string() }),
510
+ };
511
+
512
+ const handler = vi.fn();
513
+ const tag = await mq.consume('zod-tag', zodInput, handler);
514
+ expect(tag).toBe('tag-1');
515
+ });
516
+ });
517
+
518
+ describe('assertQueue with raw ZodObject', () => {
519
+ it('derives message name from queue name', async () => {
520
+ const schema = z.object({ title: z.string() });
521
+
522
+ await mq.assertQueue('my-events', schema);
523
+
524
+ const callArgs = ch.assertQueue.mock.calls[0][1];
525
+ expect(callArgs.arguments['x-schema-message']).toBe('MyEvents');
526
+ expect(callArgs.arguments['x-schema']).toContain('message MyEvents {');
527
+ });
528
+ });
529
+
530
+ describe('edge cases', () => {
531
+ it('logs error if consumer schema fails to resolve and has no name', async () => {
532
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
533
+ await mq.consume('q2', {} as any, vi.fn());
534
+ expect(consoleSpy).toHaveBeenCalled();
535
+ consoleSpy.mockRestore();
536
+ });
537
+ });
538
+
539
+ describe('consume with raw ZodObject', () => {
540
+ it('derives consumer message name from queue name', async () => {
541
+ const schema = z.object({ id: z.string() });
542
+ const handler = vi.fn();
543
+
544
+ await mq.consume('raw-zod-q', schema, handler);
545
+
546
+ expect(ch.consume).toHaveBeenCalledWith(
547
+ 'raw-zod-q',
548
+ expect.any(Function),
549
+ expect.objectContaining({
550
+ arguments: expect.objectContaining({
551
+ 'x-consumer-schema-message': 'RawZodQ',
552
+ }),
553
+ }),
554
+ );
555
+ });
556
+ });
557
+ });
558
+
311
559
  describe('connect()', () => {
312
560
  it('returns a RocketMQ instance on success', async () => {
313
561
  const mq = await connect({ url: 'amqp://localhost' });
314
562
  expect(mq).toBeInstanceOf(RocketMQ);
315
563
  });
316
564
 
317
- it('uses default URL when none provided', async () => {
318
- const mq = await connect();
319
- expect(mq).toBeInstanceOf(RocketMQ);
320
- });
321
-
322
565
  it('accepts a custom serializer', async () => {
323
566
  const custom = new JsonSerializer();
324
- const mq = await connect({ serializer: custom });
567
+ const mq = await connect({ url: 'amqp://localhost', serializer: custom });
325
568
  expect(mq).toBeInstanceOf(RocketMQ);
326
569
  });
327
570