@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.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Parses structured JSON errors from AMQP Channel.Close reply_text.
3
+ *
4
+ * The broker encodes a `BrokerError` JSON object into the reply_text
5
+ * field. This module detects JSON (starts with `{`), validates the shape,
6
+ * and returns a typed `BrokerErrorPayload`.
7
+ *
8
+ * It also handles proto → TypeScript type name mapping, since the broker
9
+ * returns raw proto names (e.g. "double") and the client translates them
10
+ * to language-specific names (e.g. "number").
11
+ *
12
+ * Usage:
13
+ * const parsed = parseBrokerError(replyText);
14
+ * if (parsed) throw SchemaValidationError.fromBrokerError(parsed);
15
+ */
16
+
17
+ import { BrokerErrorCode } from './error-codes.js';
18
+ import { SchemaValidationError } from './errors.js';
19
+
20
+ /** Structured error payload deserialized from broker JSON. */
21
+ export interface BrokerErrorPayload {
22
+ code: BrokerErrorCode;
23
+ queue: string;
24
+ fields: FieldErrorDetail[];
25
+ truncated: boolean;
26
+ }
27
+
28
+ /** Per-field error detail from the broker. */
29
+ export interface FieldErrorDetail {
30
+ /** Field name in the schema. */
31
+ name: string;
32
+ /** Proto type name that the queue schema expects (e.g. "double"). */
33
+ expected: string;
34
+ /** Proto type name that was actually received (e.g. "string"). */
35
+ got: string;
36
+ }
37
+
38
+ /**
39
+ * Maps proto3 type names to TypeScript-friendly equivalents.
40
+ *
41
+ * WHY here and not in the broker: the broker is language-agnostic.
42
+ * It returns raw proto kind names. Each client SDK maps them to the
43
+ * target language's type system.
44
+ *
45
+ * Usage:
46
+ * protoToTsType('double') // => 'number'
47
+ * protoToTsType('int32') // => 'number'
48
+ * protoToTsType('bool') // => 'boolean'
49
+ */
50
+ export function protoToTsType(protoType: string): string {
51
+ const mapping: Record<string, string> = {
52
+ double: 'number',
53
+ float: 'number',
54
+ int32: 'number',
55
+ int64: 'number',
56
+ uint32: 'number',
57
+ uint64: 'number',
58
+ sint32: 'number',
59
+ sint64: 'number',
60
+ fixed32: 'number',
61
+ fixed64: 'number',
62
+ sfixed32: 'number',
63
+ sfixed64: 'number',
64
+ bool: 'boolean',
65
+ string: 'string',
66
+ bytes: 'Uint8Array',
67
+ enum: 'enum',
68
+ message: 'object',
69
+ };
70
+ return mapping[protoType] ?? protoType;
71
+ }
72
+
73
+ /**
74
+ * Attempts to parse a structured broker error from AMQP reply_text.
75
+ *
76
+ * Returns `null` if the reply_text is a plain string (not JSON)
77
+ * or doesn't match the expected `BrokerErrorPayload` shape.
78
+ *
79
+ * Usage:
80
+ * const payload = parseBrokerError('{"code":"SchemaTypeMismatch",...}');
81
+ */
82
+ export function parseBrokerError(replyText: string): BrokerErrorPayload | null {
83
+ if (!replyText.startsWith('{')) {
84
+ return null;
85
+ }
86
+
87
+ try {
88
+ const raw: unknown = JSON.parse(replyText);
89
+ if (typeof raw !== 'object' || raw === null) {
90
+ return null;
91
+ }
92
+
93
+ const obj = raw as Record<string, unknown>;
94
+ if (typeof obj['code'] !== 'string' || typeof obj['queue'] !== 'string') {
95
+ return null;
96
+ }
97
+
98
+ return {
99
+ code: obj['code'] as BrokerErrorCode,
100
+ queue: obj['queue'] as string,
101
+ fields: Array.isArray(obj['fields']) ? (obj['fields'] as FieldErrorDetail[]) : [],
102
+ truncated: obj['truncated'] === true,
103
+ };
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Extracts the reply_text from an amqplib channel error message.
111
+ *
112
+ * amqplib formats errors as:
113
+ * 'Channel closed by server: 406 (PRECONDITION-FAILED) with message "..."'
114
+ *
115
+ * We extract the content between the last pair of double quotes.
116
+ *
117
+ * Usage:
118
+ * extractReplyText(new Error('...with message "{\"code\":...}"'))
119
+ */
120
+ export function extractReplyText(err: unknown): string | null {
121
+ if (!(err instanceof Error)) {
122
+ return null;
123
+ }
124
+
125
+ const match = err.message.match(/with message "(.+)"$/);
126
+ return match?.[1] ?? null;
127
+ }
128
+
129
+ /**
130
+ * Formats a `BrokerErrorPayload` into a developer-friendly message.
131
+ *
132
+ * Uses TypeScript type names (via `protoToTsType`) so the error
133
+ * reads naturally to TS developers.
134
+ *
135
+ * Usage:
136
+ * formatBrokerError(payload)
137
+ * // => "Queue 'orders': field 'id' is number in queue but got string"
138
+ */
139
+ export function formatBrokerError(payload: BrokerErrorPayload): string {
140
+ const header = `[${payload.code}] Queue '${payload.queue}'`;
141
+
142
+ if (payload.fields.length === 0) {
143
+ return header;
144
+ }
145
+
146
+ const details = payload.fields
147
+ .map((f) => ` • ${f.name}: expected ${protoToTsType(f.expected)}, got ${protoToTsType(f.got)}`)
148
+ .join('\n');
149
+
150
+ const suffix = payload.truncated ? '\n (some fields omitted)' : '';
151
+
152
+ return `${header}:\n${details}${suffix}`;
153
+ }
154
+
155
+ /**
156
+ * Extracts, parses, and converts an AMQP error into a typed SchemaValidationError.
157
+ * Returns null if the error is not a structured broker schema error.
158
+ */
159
+ export function wrapBrokerError(err: unknown): SchemaValidationError | null {
160
+ const replyText = extractReplyText(err);
161
+ if (!replyText) {
162
+ return null;
163
+ }
164
+
165
+ const parsed = parseBrokerError(replyText);
166
+ if (!parsed) {
167
+ return null;
168
+ }
169
+
170
+ const tsFields = parsed.fields.map((f) => ({
171
+ name: f.name,
172
+ expected: protoToTsType(f.expected),
173
+ got: protoToTsType(f.got),
174
+ }));
175
+
176
+ return new SchemaValidationError(parsed.code, parsed.queue, tsFields, formatBrokerError(parsed));
177
+ }
178
+
179
+ /**
180
+ * Throws a typed SchemaValidationError if the error is a broker schema error,
181
+ * otherwise throws the provided fallback error.
182
+ */
183
+ export function rethrowBrokerOr(err: unknown, fallback: Error): never {
184
+ const validationErr = wrapBrokerError(err);
185
+ if (validationErr) {
186
+ throw validationErr;
187
+ }
188
+ throw fallback;
189
+ }
@@ -61,44 +61,47 @@ describe('QueueError', () => {
61
61
  });
62
62
 
63
63
  describe('PublishError', () => {
64
- it('formats message with queue name', () => {
65
- const err = new PublishError('orders');
66
- expect(err.message).toBe("Failed to publish to 'orders'");
64
+ it('formats message with queue name and payload', () => {
65
+ const err = new PublishError('orders', { id: 1 });
66
+ expect(err.message).toBe('Failed to publish payload to \'orders\': {"id":1}');
67
67
  expect(err.queue).toBe('orders');
68
68
  expect(err.name).toBe('PublishError');
69
+ expect(err.payload).toEqual({ id: 1 });
69
70
  });
70
71
 
71
72
  it('preserves cause', () => {
72
73
  const cause = new Error('channel closed');
73
- const err = new PublishError('q', cause);
74
+ const err = new PublishError('q', { id: 1 }, cause);
74
75
  expect(err.cause).toBe(cause);
75
76
  });
76
77
  });
77
78
 
78
79
  describe('ConsumeError', () => {
79
80
  it('extends RocketMQError', () => {
80
- const err = new ConsumeError('consume fail');
81
+ const err = new ConsumeError('consume failed');
81
82
  expect(err).toBeInstanceOf(RocketMQError);
82
83
  expect(err.name).toBe('ConsumeError');
83
84
  });
84
85
 
85
86
  it('preserves cause', () => {
86
- const cause = new Error('NOT_FOUND');
87
- const err = new ConsumeError('failed', cause);
87
+ const cause = new Error('timeout');
88
+ const err = new ConsumeError('consume failed', cause);
88
89
  expect(err.cause).toBe(cause);
89
90
  });
90
91
  });
91
92
 
92
93
  describe('SerializationError', () => {
93
- it('extends RocketMQError', () => {
94
- const err = new SerializationError('bad json');
94
+ it('extends RocketMQError and includes payload', () => {
95
+ const err = new SerializationError({ bad: 'data' });
95
96
  expect(err).toBeInstanceOf(RocketMQError);
96
97
  expect(err.name).toBe('SerializationError');
98
+ expect(err.message).toBe('Serialization error for payload: {"bad":"data"}');
99
+ expect(err.payload).toEqual({ bad: 'data' });
97
100
  });
98
101
 
99
102
  it('preserves cause', () => {
100
- const cause = new SyntaxError('Unexpected token');
101
- const err = new SerializationError('parse failed', cause);
103
+ const cause = new Error('parse error');
104
+ const err = new SerializationError('data', cause);
102
105
  expect(err.cause).toBe(cause);
103
106
  });
104
107
  });
package/src/errors.ts CHANGED
@@ -34,9 +34,10 @@ export class QueueError extends RocketMQError {
34
34
  export class PublishError extends RocketMQError {
35
35
  constructor(
36
36
  public readonly queue: string,
37
+ public readonly payload: unknown,
37
38
  cause?: unknown,
38
39
  ) {
39
- super(`Failed to publish to '${queue}'`, cause);
40
+ super(`Failed to publish payload to '${queue}': ${JSON.stringify(payload)}`, cause);
40
41
  this.name = 'PublishError';
41
42
  }
42
43
  }
@@ -49,8 +50,11 @@ export class ConsumeError extends RocketMQError {
49
50
  }
50
51
 
51
52
  export class SerializationError extends RocketMQError {
52
- constructor(message: string, cause?: unknown) {
53
- super(message, cause);
53
+ constructor(
54
+ public readonly payload: unknown,
55
+ cause?: unknown,
56
+ ) {
57
+ super(`Serialization error for payload: ${JSON.stringify(payload)}`, cause);
54
58
  this.name = 'SerializationError';
55
59
  }
56
60
  }
@@ -62,6 +66,39 @@ export class SchemaError extends RocketMQError {
62
66
  }
63
67
  }
64
68
 
69
+ /**
70
+ * Schema-specific error with structured details from the broker.
71
+ *
72
+ * Contains the error code, queue name, and per-field details
73
+ * so callers can programmatically inspect what went wrong.
74
+ * Field types use TypeScript names (number, string, boolean).
75
+ *
76
+ * Usage:
77
+ * catch (err) {
78
+ * if (err instanceof SchemaValidationError) {
79
+ * console.log(err.code); // 'SchemaTypeMismatch'
80
+ * console.log(err.queue); // 'zod-notifications'
81
+ * console.log(err.fields); // [{ name: 'id', expected: 'number', got: 'string' }]
82
+ * }
83
+ * }
84
+ */
85
+ export class SchemaValidationError extends SchemaError {
86
+ constructor(
87
+ public readonly code: string,
88
+ public readonly queue: string,
89
+ public readonly fields: ReadonlyArray<{
90
+ readonly name: string;
91
+ readonly expected: string;
92
+ readonly got: string;
93
+ }>,
94
+ message: string,
95
+ cause?: unknown,
96
+ ) {
97
+ super(message, cause);
98
+ this.name = 'SchemaValidationError';
99
+ }
100
+ }
101
+
65
102
  export class TimeoutError extends RocketMQError {
66
103
  constructor(message: string, cause?: unknown) {
67
104
  super(message, cause);
package/src/index.ts CHANGED
@@ -2,6 +2,18 @@
2
2
  export { Schema, Field } from '@rocketmq/schema';
3
3
  export type { ProtoType, FieldMeta, SchemaEntry } from '@rocketmq/schema';
4
4
 
5
+ // Re-export Zod schema utilities so users can use either style
6
+ export {
7
+ zodToProto,
8
+ zodToFields,
9
+ isZodSchemaInput,
10
+ isRawZodObject,
11
+ type ZodSchemaInput,
12
+ } from '@rocketmq/zod';
13
+
14
+ // Re-export schema resolver types
15
+ export { type SchemaInput } from './schema-resolver.js';
16
+
5
17
  // Re-export serializer interface for custom implementations
6
18
  export type { Serializer } from '@rocketmq/serializer';
7
19
 
@@ -9,7 +21,13 @@ export type { Serializer } from '@rocketmq/serializer';
9
21
  export type { ConsumeMessage } from '@rocketmq/amqp';
10
22
 
11
23
  // Core API
12
- export { connect, RocketMQ, type RocketOptions } from './client.js';
24
+ export {
25
+ connect,
26
+ RocketMQ,
27
+ type RocketOptions,
28
+ type RocketAssertQueueOptions,
29
+ type RocketConsumeOptions,
30
+ } from './client.js';
13
31
  export { QueueHandle } from './queue-handle.js';
14
32
 
15
33
  // Errors
@@ -21,5 +39,17 @@ export {
21
39
  ConsumeError,
22
40
  SerializationError,
23
41
  SchemaError,
42
+ SchemaValidationError,
24
43
  TimeoutError,
25
44
  } from './errors.js';
45
+
46
+ // Error codes and parser for structured broker errors
47
+ export { BrokerErrorCode } from './error-codes.js';
48
+ export {
49
+ parseBrokerError,
50
+ extractReplyText,
51
+ formatBrokerError,
52
+ protoToTsType,
53
+ type BrokerErrorPayload,
54
+ type FieldErrorDetail,
55
+ } from './error-parser.js';
@@ -1,29 +1,10 @@
1
- /**
2
- * Tests for QueueHandle<T>.
3
- *
4
- * Uses FakeAmqpChannel and FakeSerializer mocks.
5
- * Covers: send (valid, invalid, serialization error),
6
- * consume (success, deserialization error, consume failure).
7
- */
8
-
9
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
10
2
  import { QueueHandle } from './queue-handle.js';
11
- import { SchemaRegistry } from '@rocketmq/schema';
12
- import type { SchemaEntry } from '@rocketmq/schema';
13
- import type { Serializer } from '@rocketmq/serializer';
14
- import { PublishError, ConsumeError } from './errors.js';
3
+ import type { RocketMQ } from './client.js';
15
4
 
16
- /** Named fake per user rules. */
17
- class FakeAmqpChannel {
5
+ class FakeRocketMQ {
18
6
  sendToQueue = vi.fn().mockReturnValue(true);
19
- consume = vi.fn().mockResolvedValue({ consumerTag: 'tag-1' });
20
- }
21
-
22
- /** Named fake serializer. */
23
- class FakeSerializer implements Serializer {
24
- readonly contentType = 'application/json';
25
- serialize = vi.fn((v: unknown) => Buffer.from(JSON.stringify(v)));
26
- deserialize = vi.fn((buf: Buffer) => JSON.parse(buf.toString()));
7
+ consume = vi.fn().mockResolvedValue('tag-1');
27
8
  }
28
9
 
29
10
  interface TestMsg {
@@ -31,60 +12,32 @@ interface TestMsg {
31
12
  qty: number;
32
13
  }
33
14
 
15
+ class TestSchema {}
16
+
34
17
  describe('QueueHandle', () => {
35
- let registry: SchemaRegistry;
36
- let channel: FakeAmqpChannel;
37
- let serializer: FakeSerializer;
18
+ let client: FakeRocketMQ;
38
19
  let handle: QueueHandle<TestMsg>;
39
20
 
40
21
  beforeEach(() => {
41
- registry = new SchemaRegistry();
42
- channel = new FakeAmqpChannel();
43
- serializer = new FakeSerializer();
44
-
45
- const entry: SchemaEntry = {
46
- ctor: class {},
47
- name: 'TestMsg',
48
- fields: [
49
- { name: 'id', protoType: 'string', number: 1 },
50
- { name: 'qty', protoType: 'int32', number: 2 },
51
- ],
52
- };
53
- registry.register('test-queue', entry);
54
-
55
- handle = new QueueHandle<TestMsg>('test-queue', channel as never, registry, serializer);
22
+ client = new FakeRocketMQ();
23
+ handle = new QueueHandle<TestMsg>('test-queue', client as unknown as RocketMQ, TestSchema);
56
24
  });
57
25
 
58
26
  describe('send', () => {
59
27
  it('serializes and sends a payload', () => {
60
28
  const result = handle.send({ id: '1', qty: 5 });
61
29
  expect(result).toBe(true);
62
- expect(serializer.serialize).toHaveBeenCalledWith({ id: '1', qty: 5 });
63
- expect(channel.sendToQueue).toHaveBeenCalledWith(
64
- 'test-queue',
65
- expect.any(Buffer),
66
- expect.objectContaining({
67
- contentType: 'application/json',
68
- persistent: true,
69
- }),
70
- );
30
+ expect(client.sendToQueue).toHaveBeenCalledWith('test-queue', { id: '1', qty: 5 }, undefined);
71
31
  });
72
32
 
73
33
  it('passes custom publish options', () => {
74
34
  handle.send({ id: '1', qty: 5 }, { priority: 5 });
75
- expect(channel.sendToQueue).toHaveBeenCalledWith(
35
+ expect(client.sendToQueue).toHaveBeenCalledWith(
76
36
  'test-queue',
77
- expect.any(Buffer),
78
- expect.objectContaining({ priority: 5 }),
37
+ { id: '1', qty: 5 },
38
+ { priority: 5 },
79
39
  );
80
40
  });
81
-
82
- it('throws PublishError when channel.sendToQueue throws', () => {
83
- channel.sendToQueue.mockImplementation(() => {
84
- throw new Error('channel closed');
85
- });
86
- expect(() => handle.send({ id: '1', qty: 5 })).toThrow(PublishError);
87
- });
88
41
  });
89
42
 
90
43
  describe('consume', () => {
@@ -92,94 +45,15 @@ describe('QueueHandle', () => {
92
45
  const handler = vi.fn();
93
46
  const tag = await handle.consume(handler);
94
47
  expect(tag).toBe('tag-1');
95
- expect(channel.consume).toHaveBeenCalledWith('test-queue', expect.any(Function), {
96
- arguments: {},
97
- });
98
- });
99
-
100
- it('deserializes and calls handler with typed message', async () => {
101
- const handler = vi.fn();
102
- // Capture the internal callback
103
- channel.consume.mockImplementation(async (_q: string, cb: Function) => {
104
- const raw = {
105
- content: Buffer.from(JSON.stringify({ id: 'x', qty: 3 })),
106
- fields: {},
107
- properties: {},
108
- };
109
- cb(raw);
110
- return { consumerTag: 'tag-2' };
111
- });
112
-
113
- await handle.consume(handler);
114
- expect(handler).toHaveBeenCalledWith(
115
- { id: 'x', qty: 3 },
116
- expect.objectContaining({ content: expect.any(Buffer) }),
117
- );
118
- });
119
-
120
- it('ignores null messages', async () => {
121
- const handler = vi.fn();
122
- channel.consume.mockImplementation(async (_q: string, cb: Function) => {
123
- cb(null);
124
- return { consumerTag: 'tag-3' };
125
- });
126
-
127
- await handle.consume(handler);
128
- expect(handler).not.toHaveBeenCalled();
129
- });
130
-
131
- it('logs deserialization errors without crashing', async () => {
132
- const handler = vi.fn();
133
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
134
-
135
- serializer.deserialize.mockImplementation(() => {
136
- throw new SyntaxError('bad json');
137
- });
138
-
139
- channel.consume.mockImplementation(async (_q: string, cb: Function) => {
140
- cb({ content: Buffer.from('not json'), fields: {}, properties: {} });
141
- return { consumerTag: 'tag-4' };
142
- });
143
-
144
- await handle.consume(handler);
145
- expect(handler).not.toHaveBeenCalled();
146
- expect(consoleSpy).toHaveBeenCalledWith(
147
- expect.stringContaining('deserialization error'),
148
- expect.any(SyntaxError),
149
- );
150
- consoleSpy.mockRestore();
151
- });
152
-
153
- it('throws ConsumeError when channel.consume rejects', async () => {
154
- channel.consume.mockRejectedValue(new Error('NOT_FOUND'));
155
- const handler = vi.fn();
156
- await expect(handle.consume(handler)).rejects.toThrow(ConsumeError);
48
+ expect(client.consume).toHaveBeenCalledWith('test-queue', TestSchema, handler, undefined);
157
49
  });
158
50
 
159
51
  it('passes consume options through', async () => {
160
52
  const handler = vi.fn();
161
53
  await handle.consume(handler, { noAck: true });
162
- expect(channel.consume).toHaveBeenCalledWith('test-queue', expect.any(Function), {
54
+ expect(client.consume).toHaveBeenCalledWith('test-queue', TestSchema, handler, {
163
55
  noAck: true,
164
- arguments: {},
165
56
  });
166
57
  });
167
58
  });
168
59
  });
169
-
170
- describe('QueueHandle without schema', () => {
171
- it('sends without errors when no schema registered', () => {
172
- const registry = new SchemaRegistry();
173
- const channel = new FakeAmqpChannel();
174
- const serializer = new FakeSerializer();
175
- const handle = new QueueHandle<Record<string, unknown>>(
176
- 'unregistered',
177
- channel as never,
178
- registry,
179
- serializer,
180
- );
181
-
182
- const result = handle.send({ anything: 'goes' });
183
- expect(result).toBe(true);
184
- });
185
- });
@@ -11,18 +11,15 @@
11
11
  * await orders.consume((msg) => console.log(msg.id));
12
12
  */
13
13
 
14
- import type { AmqpChannel, ConsumeMessage, ConsumeOptions, PublishOptions } from '@rocketmq/amqp';
15
- import type { SchemaRegistry } from '@rocketmq/schema';
16
- import type { Serializer } from '@rocketmq/serializer';
17
- import { toProto } from '@rocketmq/protobuf';
18
- import { PublishError, ConsumeError } from './errors.js';
14
+ import type { ConsumeOptions, PublishOptions } from '@rocketmq/amqp';
15
+ import type { RocketMQ, TypedConsumeHandler } from './client.js';
16
+ import type { SchemaInput } from './schema-resolver.js';
19
17
 
20
18
  export class QueueHandle<T> {
21
19
  constructor(
22
20
  private readonly queueName: string,
23
- private readonly channel: AmqpChannel,
24
- private readonly registry: SchemaRegistry,
25
- private readonly serializer: Serializer,
21
+ private readonly client: RocketMQ,
22
+ private readonly schema: SchemaInput<T>,
26
23
  ) {}
27
24
 
28
25
  /**
@@ -31,16 +28,7 @@ export class QueueHandle<T> {
31
28
  * Pipeline: serialize → send. Validation is broker-side.
32
29
  */
33
30
  send(payload: T, opts?: PublishOptions): boolean {
34
- try {
35
- const buf = this.serializer.serialize(payload);
36
- return this.channel.sendToQueue(this.queueName, buf, {
37
- contentType: this.serializer.contentType,
38
- persistent: true,
39
- ...opts,
40
- });
41
- } catch (err) {
42
- throw new PublishError(this.queueName, err);
43
- }
31
+ return this.client.sendToQueue(this.queueName, payload as Record<string, unknown>, opts);
44
32
  }
45
33
 
46
34
  /**
@@ -50,53 +38,7 @@ export class QueueHandle<T> {
50
38
  * arguments so the broker can verify schema subset compatibility.
51
39
  * Malformed messages are logged but not re-thrown to keep the loop alive.
52
40
  */
53
- async consume(
54
- handler: (msg: T, raw: ConsumeMessage) => void,
55
- opts?: ConsumeOptions,
56
- ): Promise<string> {
57
- const consumerArgs = this.buildConsumerSchemaArgs();
58
-
59
- try {
60
- const reply = await this.channel.consume(
61
- this.queueName,
62
- (raw) => {
63
- if (!raw) return;
64
- try {
65
- const body = this.serializer.deserialize(raw.content) as T;
66
- handler(body, raw);
67
- } catch (err) {
68
- // WHY: log instead of throw to keep the consumer loop alive
69
- console.error(`[rocketmq] deserialization error on queue '${this.queueName}':`, err);
70
- }
71
- },
72
- {
73
- ...opts,
74
- arguments: {
75
- ...opts?.arguments,
76
- ...consumerArgs,
77
- },
78
- },
79
- );
80
- return reply.consumerTag;
81
- } catch (err) {
82
- throw new ConsumeError(`Failed to consume from queue '${this.queueName}'`, err);
83
- }
84
- }
85
-
86
- /** Builds AMQP arguments for consumer schema subset checking. */
87
- private buildConsumerSchemaArgs(): Record<string, string> {
88
- const meta = this.registry.lookup(this.queueName);
89
- if (!meta) return {};
90
-
91
- try {
92
- const proto = toProto(meta.ctor);
93
- return {
94
- 'x-consumer-schema': proto,
95
- 'x-consumer-schema-message': meta.name,
96
- };
97
- } catch {
98
- // WHY: ctor may lack @Field() decorators (e.g. untyped consumers)
99
- return {};
100
- }
41
+ async consume(handler: TypedConsumeHandler<T>, opts?: ConsumeOptions): Promise<string> {
42
+ return this.client.consume(this.queueName, this.schema, handler, opts);
101
43
  }
102
44
  }