@rocketmq/core 0.1.2 → 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/.turbo/turbo-build.log +10 -8
- package/CHANGELOG.md +14 -0
- package/README.md +67 -6
- package/dist/index.cjs +4829 -163
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +255 -33
- package/dist/index.d.ts +255 -33
- package/dist/index.js +4825 -165
- package/dist/index.js.map +1 -0
- package/package.json +6 -5
- package/src/client.test.ts +256 -13
- package/src/client.ts +195 -81
- package/src/error-codes.ts +30 -0
- package/src/error-parser.test.ts +223 -0
- package/src/error-parser.ts +189 -0
- package/src/errors.test.ts +14 -11
- package/src/errors.ts +40 -3
- package/src/index.ts +31 -1
- package/src/queue-handle.test.ts +14 -140
- package/src/queue-handle.ts +8 -66
- package/src/schema-resolver.test.ts +112 -0
- package/src/schema-resolver.ts +99 -0
- package/tsup.config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rocketmq/core",
|
|
3
|
-
"version": "0.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/
|
|
13
|
-
"@rocketmq/
|
|
14
|
-
"@rocketmq/
|
|
15
|
-
"@rocketmq/
|
|
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",
|
package/src/client.test.ts
CHANGED
|
@@ -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 =
|
|
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 (
|
|
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
|
|