@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
|
@@ -1,7 +1,16 @@
|
|
|
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';
|
|
6
|
+
/**
|
|
7
|
+
* Context passed to subscriber callbacks.
|
|
8
|
+
* Provides access to the Matador instance for sending additional events.
|
|
9
|
+
*/
|
|
10
|
+
export interface CallbackContext {
|
|
11
|
+
/** Matador dispatcher for sending additional events from within a subscriber */
|
|
12
|
+
readonly matador: Dispatcher;
|
|
13
|
+
}
|
|
5
14
|
/**
|
|
6
15
|
* Helper type to get the envelope type for a subscriber callback.
|
|
7
16
|
* Extracts the data type from a MatadorEvent and wraps it in an Envelope.
|
|
@@ -14,14 +23,27 @@ import type { MatadorEvent } from './event.js';
|
|
|
14
23
|
export type EnvelopeOf<T extends MatadorEvent> = Envelope<T['data']>;
|
|
15
24
|
/**
|
|
16
25
|
* Callback function executed when an event is received (standard subscribers).
|
|
17
|
-
* Receives the full envelope containing id, data, and docket
|
|
26
|
+
* Receives the full envelope containing id, data, and docket, plus a context
|
|
27
|
+
* with access to the matador instance for sending additional events.
|
|
28
|
+
*
|
|
29
|
+
* The callback may optionally return a value, which will be included in the
|
|
30
|
+
* onWorkerSuccess hook context for logging or observability purposes.
|
|
18
31
|
*/
|
|
19
|
-
export type StandardCallback<T = unknown> = (envelope: Envelope<T
|
|
32
|
+
export type StandardCallback<T = unknown> = (envelope: Envelope<T>, context: CallbackContext) => Promise<unknown> | unknown | void;
|
|
33
|
+
/**
|
|
34
|
+
* Context for resumable subscriber callbacks.
|
|
35
|
+
* Combines checkpoint operations (io, all) with matador access.
|
|
36
|
+
*/
|
|
37
|
+
export type ResumableCallbackContext = SubscriberContext & CallbackContext;
|
|
20
38
|
/**
|
|
21
39
|
* Callback function for resumable subscribers.
|
|
22
|
-
* Receives the envelope and a
|
|
40
|
+
* Receives the envelope and a context with io() for checkpointed operations
|
|
41
|
+
* and matador for sending additional events.
|
|
42
|
+
*
|
|
43
|
+
* The callback may optionally return a value, which will be included in the
|
|
44
|
+
* onWorkerSuccess hook context for logging or observability purposes.
|
|
23
45
|
*/
|
|
24
|
-
export type ResumableCallback<T = unknown> = (envelope: Envelope<T>, context:
|
|
46
|
+
export type ResumableCallback<T = unknown> = (envelope: Envelope<T>, context: ResumableCallbackContext) => Promise<unknown> | unknown | void;
|
|
25
47
|
/**
|
|
26
48
|
* Callback function executed when an event is received.
|
|
27
49
|
* @deprecated Use StandardCallback or ResumableCallback instead.
|
|
@@ -86,7 +108,7 @@ export type Subscriber<T extends MatadorEvent> = StandardSubscriber<T> | Resumab
|
|
|
86
108
|
* is in a remote service. Declares the subscriber contract without providing
|
|
87
109
|
* the callback.
|
|
88
110
|
*/
|
|
89
|
-
export interface SubscriberStub extends StandardSubscriberOptions {
|
|
111
|
+
export interface SubscriberStub extends Omit<StandardSubscriberOptions, 'description'> {
|
|
90
112
|
/** Human-readable name for the subscriber */
|
|
91
113
|
readonly name: string;
|
|
92
114
|
/** Indicates this is a stub without implementation */
|
|
@@ -175,7 +197,6 @@ export declare function createSubscriber<T extends MatadorEvent>(input: CreateSu
|
|
|
175
197
|
*/
|
|
176
198
|
export interface CreateSubscriberStubInput {
|
|
177
199
|
readonly name: string;
|
|
178
|
-
readonly description: string;
|
|
179
200
|
readonly idempotent?: 'yes' | 'no' | 'unknown' | undefined;
|
|
180
201
|
readonly importance?: Importance | undefined;
|
|
181
202
|
readonly targetQueue?: string | undefined;
|
|
@@ -188,7 +209,6 @@ export interface CreateSubscriberStubInput {
|
|
|
188
209
|
* ```typescript
|
|
189
210
|
* const stub = createSubscriberStub({
|
|
190
211
|
* name: 'remote-analytics',
|
|
191
|
-
* description: 'Sends events to remote analytics service',
|
|
192
212
|
* targetQueue: 'analytics-worker',
|
|
193
213
|
* });
|
|
194
214
|
* ```
|
|
@@ -199,7 +219,7 @@ export declare function createSubscriberStub(input: CreateSubscriberStubInput):
|
|
|
199
219
|
*/
|
|
200
220
|
export interface SubscriberDefinition {
|
|
201
221
|
readonly name: string;
|
|
202
|
-
readonly description
|
|
222
|
+
readonly description?: string | undefined;
|
|
203
223
|
readonly idempotent: Idempotency;
|
|
204
224
|
readonly importance: Importance;
|
|
205
225
|
readonly targetQueue?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../src/types/subscriber.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,YAAY,IAAI,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAErE
|
|
1
|
+
{"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../src/types/subscriber.ts"],"names":[],"mappings":"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"}
|
package/dist/types/subscriber.js
CHANGED
|
@@ -79,7 +79,6 @@ export function createSubscriber(input) {
|
|
|
79
79
|
* ```typescript
|
|
80
80
|
* const stub = createSubscriberStub({
|
|
81
81
|
* name: 'remote-analytics',
|
|
82
|
-
* description: 'Sends events to remote analytics service',
|
|
83
82
|
* targetQueue: 'analytics-worker',
|
|
84
83
|
* });
|
|
85
84
|
* ```
|
|
@@ -87,7 +86,6 @@ export function createSubscriber(input) {
|
|
|
87
86
|
export function createSubscriberStub(input) {
|
|
88
87
|
return {
|
|
89
88
|
name: input.name,
|
|
90
|
-
description: input.description,
|
|
91
89
|
isStub: true,
|
|
92
90
|
idempotent: input.idempotent ?? 'unknown',
|
|
93
91
|
importance: input.importance ?? 'should-investigate',
|
package/package.json
CHANGED
package/src/core/fanout.test.ts
CHANGED
package/src/core/matador.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { getQualifiedQueueName } from '../topology/index.js';
|
|
|
18
18
|
import type { Subscription, Transport } from '../transport/index.js';
|
|
19
19
|
import type {
|
|
20
20
|
AnySubscriber,
|
|
21
|
+
Dispatcher,
|
|
21
22
|
Event,
|
|
22
23
|
EventClass,
|
|
23
24
|
EventOptions,
|
|
@@ -71,7 +72,7 @@ export interface MatadorConfig {
|
|
|
71
72
|
* - Fanout: Event sending
|
|
72
73
|
* - Shutdown: Graceful termination
|
|
73
74
|
*/
|
|
74
|
-
export class Matador {
|
|
75
|
+
export class Matador implements Dispatcher {
|
|
75
76
|
private readonly transport: Transport;
|
|
76
77
|
private readonly topology: Topology;
|
|
77
78
|
private readonly schema;
|
|
@@ -114,6 +115,7 @@ export class Matador {
|
|
|
114
115
|
retryPolicy: this.retryPolicy,
|
|
115
116
|
hooks: this.hooks,
|
|
116
117
|
checkpointStore: config.checkpointStore,
|
|
118
|
+
dispatcher: this,
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
// Create fanout engine
|
package/src/errors/index.ts
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { createEnvelope } from '../types/envelope.js';
|
|
3
|
+
import {
|
|
4
|
+
assertEvent,
|
|
5
|
+
DontRetry,
|
|
6
|
+
DoRetry,
|
|
7
|
+
EventAssertionError,
|
|
8
|
+
isAssertionError,
|
|
9
|
+
isDontRetry,
|
|
10
|
+
isDoRetry,
|
|
11
|
+
} from './retry-errors.js';
|
|
12
|
+
|
|
13
|
+
function createTestEnvelope(data: unknown = { foo: 'bar' }) {
|
|
14
|
+
return createEnvelope({
|
|
15
|
+
data,
|
|
16
|
+
eventKey: 'test.event',
|
|
17
|
+
targetSubscriber: 'test-subscriber',
|
|
18
|
+
importance: 'can-ignore',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('retry-errors', () => {
|
|
23
|
+
describe('DoRetry', () => {
|
|
24
|
+
it('should have correct name', () => {
|
|
25
|
+
const error = new DoRetry('test message');
|
|
26
|
+
expect(error.name).toBe('DoRetry');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have description', () => {
|
|
30
|
+
const error = new DoRetry('test message');
|
|
31
|
+
expect(error.description).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should serialize to JSON', () => {
|
|
35
|
+
const error = new DoRetry('test message');
|
|
36
|
+
const json = error.toJSON();
|
|
37
|
+
expect(json.name).toBe('DoRetry');
|
|
38
|
+
expect(json.message).toBe('test message');
|
|
39
|
+
expect(json.description).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('DontRetry', () => {
|
|
44
|
+
it('should have correct name', () => {
|
|
45
|
+
const error = new DontRetry('test message');
|
|
46
|
+
expect(error.name).toBe('DontRetry');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should have description', () => {
|
|
50
|
+
const error = new DontRetry('test message');
|
|
51
|
+
expect(error.description).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should serialize to JSON', () => {
|
|
55
|
+
const error = new DontRetry('test message');
|
|
56
|
+
const json = error.toJSON();
|
|
57
|
+
expect(json.name).toBe('DontRetry');
|
|
58
|
+
expect(json.message).toBe('test message');
|
|
59
|
+
expect(json.description).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('EventAssertionError', () => {
|
|
64
|
+
it('should have correct name', () => {
|
|
65
|
+
const envelope = createTestEnvelope();
|
|
66
|
+
const error = new EventAssertionError(envelope, 'assertion failed');
|
|
67
|
+
expect(error.name).toBe('EventAssertionError');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should store the envelope', () => {
|
|
71
|
+
const envelope = createTestEnvelope();
|
|
72
|
+
const error = new EventAssertionError(envelope, 'assertion failed');
|
|
73
|
+
expect(error.envelope).toBe(envelope);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should have description', () => {
|
|
77
|
+
const envelope = createTestEnvelope();
|
|
78
|
+
const error = new EventAssertionError(envelope, 'assertion failed');
|
|
79
|
+
expect(error.description).toBeDefined();
|
|
80
|
+
expect(error.description).toContain('NOT to retry');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should serialize to JSON with envelope', () => {
|
|
84
|
+
const envelope = createTestEnvelope();
|
|
85
|
+
const error = new EventAssertionError(envelope, 'assertion failed');
|
|
86
|
+
const json = error.toJSON();
|
|
87
|
+
expect(json.name).toBe('EventAssertionError');
|
|
88
|
+
expect(json.message).toBe('assertion failed');
|
|
89
|
+
expect(json.envelope).toBe(envelope);
|
|
90
|
+
expect(json.description).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('assertEvent', () => {
|
|
95
|
+
it('should not throw when value is truthy', () => {
|
|
96
|
+
const envelope = createTestEnvelope();
|
|
97
|
+
expect(() => assertEvent(envelope, true, 'should be true')).not.toThrow();
|
|
98
|
+
expect(() =>
|
|
99
|
+
assertEvent(envelope, 'string', 'should be string'),
|
|
100
|
+
).not.toThrow();
|
|
101
|
+
expect(() => assertEvent(envelope, 1, 'should be number')).not.toThrow();
|
|
102
|
+
expect(() => assertEvent(envelope, {}, 'should be object')).not.toThrow();
|
|
103
|
+
expect(() => assertEvent(envelope, [], 'should be array')).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should throw EventAssertionError when value is falsy', () => {
|
|
107
|
+
const envelope = createTestEnvelope();
|
|
108
|
+
|
|
109
|
+
expect(() => assertEvent(envelope, false, 'value was false')).toThrow(
|
|
110
|
+
EventAssertionError,
|
|
111
|
+
);
|
|
112
|
+
expect(() => assertEvent(envelope, null, 'value was null')).toThrow(
|
|
113
|
+
EventAssertionError,
|
|
114
|
+
);
|
|
115
|
+
expect(() =>
|
|
116
|
+
assertEvent(envelope, undefined, 'value was undefined'),
|
|
117
|
+
).toThrow(EventAssertionError);
|
|
118
|
+
expect(() => assertEvent(envelope, 0, 'value was zero')).toThrow(
|
|
119
|
+
EventAssertionError,
|
|
120
|
+
);
|
|
121
|
+
expect(() => assertEvent(envelope, '', 'value was empty string')).toThrow(
|
|
122
|
+
EventAssertionError,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should include envelope in thrown error', () => {
|
|
127
|
+
const envelope = createTestEnvelope();
|
|
128
|
+
try {
|
|
129
|
+
assertEvent(envelope, false, 'assertion failed');
|
|
130
|
+
expect.unreachable('should have thrown');
|
|
131
|
+
} catch (error) {
|
|
132
|
+
expect(error).toBeInstanceOf(EventAssertionError);
|
|
133
|
+
expect((error as EventAssertionError).envelope).toBe(envelope);
|
|
134
|
+
expect((error as EventAssertionError).message).toBe('assertion failed');
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should include message in thrown error', () => {
|
|
139
|
+
const envelope = createTestEnvelope();
|
|
140
|
+
try {
|
|
141
|
+
assertEvent(envelope, null, 'userId is required');
|
|
142
|
+
expect.unreachable('should have thrown');
|
|
143
|
+
} catch (error) {
|
|
144
|
+
expect(error).toBeInstanceOf(EventAssertionError);
|
|
145
|
+
expect((error as EventAssertionError).message).toBe('userId is required');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('type guards', () => {
|
|
151
|
+
it('isDoRetry should identify DoRetry errors', () => {
|
|
152
|
+
expect(isDoRetry(new DoRetry('test'))).toBe(true);
|
|
153
|
+
expect(isDoRetry(new DontRetry('test'))).toBe(false);
|
|
154
|
+
expect(isDoRetry(new Error('test'))).toBe(false);
|
|
155
|
+
expect(isDoRetry(null)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('isDontRetry should identify DontRetry errors', () => {
|
|
159
|
+
expect(isDontRetry(new DontRetry('test'))).toBe(true);
|
|
160
|
+
expect(isDontRetry(new DoRetry('test'))).toBe(false);
|
|
161
|
+
expect(isDontRetry(new Error('test'))).toBe(false);
|
|
162
|
+
expect(isDontRetry(null)).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('isAssertionError should identify EventAssertionError', () => {
|
|
166
|
+
const envelope = createTestEnvelope();
|
|
167
|
+
expect(isAssertionError(new EventAssertionError(envelope, 'test'))).toBe(
|
|
168
|
+
true,
|
|
169
|
+
);
|
|
170
|
+
expect(isAssertionError(new DoRetry('test'))).toBe(false);
|
|
171
|
+
expect(isAssertionError(new Error('test'))).toBe(false);
|
|
172
|
+
expect(isAssertionError(null)).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import type { Envelope } from '../types/envelope.js';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Base class for retry control errors.
|
|
3
6
|
* These errors control the retry behavior of message processing.
|
|
@@ -76,24 +79,37 @@ export class DontRetry extends RetryControlError {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
82
|
+
* Thrown by `assertEvent` in the event of a failed assertion.
|
|
83
|
+
*
|
|
84
|
+
* This error indicates that an event was in an unexpected state when it reached the subscriber.
|
|
85
|
+
* Throwing this error will indicate to Matador NOT to retry the event - it will be sent
|
|
86
|
+
* directly to the undeliverable dead-letter queue.
|
|
81
87
|
*
|
|
82
|
-
*
|
|
83
|
-
* in
|
|
88
|
+
* Use `assertEvent` in your subscribers to assert properties about an event payload.
|
|
89
|
+
* This is useful in scenarios where you would expect an event payload to contain a field
|
|
90
|
+
* based on the types, but something unexpected caused it to not be present.
|
|
91
|
+
*
|
|
92
|
+
* @see assertEvent
|
|
84
93
|
*/
|
|
85
94
|
export class EventAssertionError extends Error {
|
|
86
95
|
declare readonly name: string;
|
|
87
96
|
|
|
88
97
|
readonly description =
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'
|
|
98
|
+
'Thrown to indicate that an event was in an unexpected state when it reached the subscriber. ' +
|
|
99
|
+
'Throwing this error will indicate to Matador NOT to retry the event. ' +
|
|
100
|
+
'Matador does not throw this error directly - it is thrown by assertEvent, which you can ' +
|
|
101
|
+
'use in your subscribers to assert properties about an event. ' +
|
|
102
|
+
'This can be useful in scenarios where you would expect an event payload to contain a field ' +
|
|
103
|
+
'based on the types, but something unexpected caused it to not be present. ' +
|
|
104
|
+
'Using this instead of the NodeJS assert allows Matador to not retry the event subscriber.';
|
|
93
105
|
|
|
94
|
-
|
|
106
|
+
/** The envelope that failed the assertion */
|
|
107
|
+
readonly envelope: Envelope<unknown>;
|
|
108
|
+
|
|
109
|
+
constructor(envelope: Envelope<unknown>, message: string) {
|
|
95
110
|
super(message);
|
|
96
111
|
this.name = 'EventAssertionError';
|
|
112
|
+
this.envelope = envelope;
|
|
97
113
|
Object.defineProperty(this, 'name', {
|
|
98
114
|
value: 'EventAssertionError',
|
|
99
115
|
enumerable: true,
|
|
@@ -107,6 +123,7 @@ export class EventAssertionError extends Error {
|
|
|
107
123
|
name: this.name,
|
|
108
124
|
message: this.message,
|
|
109
125
|
description: this.description,
|
|
126
|
+
envelope: this.envelope,
|
|
110
127
|
stack: this.stack,
|
|
111
128
|
};
|
|
112
129
|
}
|
|
@@ -132,3 +149,40 @@ export function isDontRetry(error: unknown): error is DontRetry {
|
|
|
132
149
|
export function isAssertionError(error: unknown): error is EventAssertionError {
|
|
133
150
|
return error instanceof EventAssertionError;
|
|
134
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Same as Node.js `assert`, but throws an `EventAssertionError` if the assertion fails.
|
|
155
|
+
*
|
|
156
|
+
* `EventAssertionError`s thrown within a subscriber do not cause the subscriber to be retried.
|
|
157
|
+
* The event will be delivered to the undeliverable dead-letter queue instead.
|
|
158
|
+
*
|
|
159
|
+
* @see https://nodejs.org/api/assert.html#assertvalue-message
|
|
160
|
+
* @param envelope - The envelope this assertion relates to.
|
|
161
|
+
* @param value - The value to assert. If falsy, the assertion fails.
|
|
162
|
+
* @param message - The message to include in the error if the assertion fails.
|
|
163
|
+
* @throws {EventAssertionError} If the assertion fails.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const subscriber = createSubscriber<MyEvent>({
|
|
168
|
+
* name: 'my-subscriber',
|
|
169
|
+
* description: 'Processes MyEvent',
|
|
170
|
+
* callback: async (envelope) => {
|
|
171
|
+
* // Assert that userId exists - if not, event goes to DLQ without retry
|
|
172
|
+
* assertEvent(envelope, envelope.data.userId, 'userId is required');
|
|
173
|
+
* // ... rest of processing
|
|
174
|
+
* },
|
|
175
|
+
* });
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export function assertEvent(
|
|
179
|
+
envelope: Envelope<unknown>,
|
|
180
|
+
value: unknown,
|
|
181
|
+
message: string,
|
|
182
|
+
): asserts value {
|
|
183
|
+
try {
|
|
184
|
+
assert(value, message);
|
|
185
|
+
} catch {
|
|
186
|
+
throw new EventAssertionError(envelope, message);
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,8 +19,10 @@ export {
|
|
|
19
19
|
export type {
|
|
20
20
|
AnySubscriber,
|
|
21
21
|
BaseSubscriberOptions,
|
|
22
|
+
CallbackContext,
|
|
22
23
|
CreateEnvelopeOptions,
|
|
23
24
|
DeliveryMode,
|
|
25
|
+
Dispatcher,
|
|
24
26
|
Docket,
|
|
25
27
|
Envelope,
|
|
26
28
|
EnvelopeOf,
|
|
@@ -33,6 +35,9 @@ export type {
|
|
|
33
35
|
Idempotency,
|
|
34
36
|
Importance,
|
|
35
37
|
JsonRecord,
|
|
38
|
+
ResumableCallback,
|
|
39
|
+
ResumableCallbackContext,
|
|
40
|
+
StandardCallback,
|
|
36
41
|
Subscriber,
|
|
37
42
|
SubscriberCallback,
|
|
38
43
|
SubscriberDefinition,
|
|
@@ -43,6 +48,7 @@ export type {
|
|
|
43
48
|
} from './types/index.js';
|
|
44
49
|
export {
|
|
45
50
|
MatadorEvent,
|
|
51
|
+
createDummyEnvelope,
|
|
46
52
|
createEnvelope,
|
|
47
53
|
createSubscriber,
|
|
48
54
|
createSubscriberStub,
|
|
@@ -91,13 +97,17 @@ export type {
|
|
|
91
97
|
DeadLetterQueueConfig,
|
|
92
98
|
QueueDefinition,
|
|
93
99
|
QueueOptions,
|
|
100
|
+
RabbitMQQueueDefinition,
|
|
101
|
+
RabbitMQQueueOptions,
|
|
94
102
|
RetryConfig,
|
|
95
103
|
Topology,
|
|
104
|
+
TransportQueueOptions,
|
|
96
105
|
} from './topology/index.js';
|
|
97
106
|
export {
|
|
98
107
|
getDeadLetterQueueName,
|
|
99
108
|
getQualifiedQueueName,
|
|
100
109
|
getRetryQueueName,
|
|
110
|
+
resolveQueueName,
|
|
101
111
|
TopologyBuilder,
|
|
102
112
|
TopologyValidationError,
|
|
103
113
|
} from './topology/index.js';
|
|
@@ -162,6 +172,7 @@ export { ProcessingPipeline } from './pipeline/index.js';
|
|
|
162
172
|
export type { HasDescription } from './errors/index.js';
|
|
163
173
|
export {
|
|
164
174
|
// Retry control errors
|
|
175
|
+
assertEvent,
|
|
165
176
|
DontRetry,
|
|
166
177
|
DoRetry,
|
|
167
178
|
EventAssertionError,
|
|
@@ -10,7 +10,11 @@ import type { RetryDecision, RetryPolicy } from '../retry/index.js';
|
|
|
10
10
|
import { StandardRetryPolicy } 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
|
+
Dispatcher,
|
|
15
|
+
Envelope,
|
|
16
|
+
SubscriberDefinition,
|
|
17
|
+
} from '../types/index.js';
|
|
14
18
|
import { createEnvelope } from '../types/index.js';
|
|
15
19
|
import type { PipelineConfig } from './pipeline.js';
|
|
16
20
|
import { ProcessingPipeline } from './pipeline.js';
|
|
@@ -324,7 +328,13 @@ describe('ProcessingPipeline', () => {
|
|
|
324
328
|
const result = await pipeline.process(new Uint8Array(), receipt);
|
|
325
329
|
|
|
326
330
|
expect(result.success).toBe(true);
|
|
327
|
-
expect(callbackMock).
|
|
331
|
+
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
332
|
+
// Callback receives envelope as first argument and context (with matador) as second
|
|
333
|
+
const calls = callbackMock.mock.calls as unknown as [
|
|
334
|
+
[Envelope, { matador: Dispatcher }],
|
|
335
|
+
];
|
|
336
|
+
expect(calls[0][0]).toEqual(envelope);
|
|
337
|
+
expect(calls[0][1]).toHaveProperty('matador');
|
|
328
338
|
});
|
|
329
339
|
|
|
330
340
|
it('should handle callback throwing error', async () => {
|
|
@@ -532,6 +542,44 @@ describe('ProcessingPipeline', () => {
|
|
|
532
542
|
});
|
|
533
543
|
});
|
|
534
544
|
|
|
545
|
+
it('should pass subscriber return value to onWorkerSuccess', async () => {
|
|
546
|
+
const envelope = createTestEnvelope();
|
|
547
|
+
const subscriberDef = createSubscriberDefinition();
|
|
548
|
+
const onWorkerSuccessMock = mock(async () => {});
|
|
549
|
+
const returnValue = { processed: true, count: 42 };
|
|
550
|
+
|
|
551
|
+
const config = createMockConfig({
|
|
552
|
+
codec: {
|
|
553
|
+
decode: mock(() => envelope),
|
|
554
|
+
},
|
|
555
|
+
schema: {
|
|
556
|
+
getSubscriberDefinition: mock(() => subscriberDef),
|
|
557
|
+
getExecutableSubscriber: mock(() => ({
|
|
558
|
+
name: 'test-subscriber',
|
|
559
|
+
description: 'Test subscriber',
|
|
560
|
+
idempotent: 'unknown' as const,
|
|
561
|
+
callback: mock(async () => returnValue),
|
|
562
|
+
})),
|
|
563
|
+
},
|
|
564
|
+
hooks: {
|
|
565
|
+
onWorkerSuccess: onWorkerSuccessMock,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const pipeline = new ProcessingPipeline(config);
|
|
570
|
+
const receipt = createReceipt();
|
|
571
|
+
|
|
572
|
+
await pipeline.process(new Uint8Array(), receipt);
|
|
573
|
+
|
|
574
|
+
expect(onWorkerSuccessMock).toHaveBeenCalledWith({
|
|
575
|
+
envelope,
|
|
576
|
+
subscriber: subscriberDef,
|
|
577
|
+
result: returnValue,
|
|
578
|
+
durationMs: expect.any(Number),
|
|
579
|
+
transport: 'mock',
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
535
583
|
it('should call onWorkerError after failed callback', async () => {
|
|
536
584
|
const envelope = createTestEnvelope();
|
|
537
585
|
const subscriberDef = createSubscriberDefinition();
|
|
@@ -1189,6 +1237,7 @@ function createMockConfig(
|
|
|
1189
1237
|
codec: Partial<Codec>;
|
|
1190
1238
|
retryPolicy: RetryPolicy | Partial<RetryPolicy>;
|
|
1191
1239
|
hooks: Partial<SafeHooks>;
|
|
1240
|
+
dispatcher: Partial<Dispatcher>;
|
|
1192
1241
|
}> = {},
|
|
1193
1242
|
): PipelineConfig {
|
|
1194
1243
|
const defaultTransport: Transport = {
|
|
@@ -1272,12 +1321,23 @@ function createMockConfig(
|
|
|
1272
1321
|
...overrides.hooks,
|
|
1273
1322
|
} as SafeHooks;
|
|
1274
1323
|
|
|
1324
|
+
const defaultDispatcher: Dispatcher = {
|
|
1325
|
+
send: mock(async () => ({
|
|
1326
|
+
eventKey: 'test.event',
|
|
1327
|
+
subscribersSent: 0,
|
|
1328
|
+
subscribersSkipped: 0,
|
|
1329
|
+
errors: [],
|
|
1330
|
+
})),
|
|
1331
|
+
...overrides.dispatcher,
|
|
1332
|
+
} as Dispatcher;
|
|
1333
|
+
|
|
1275
1334
|
return {
|
|
1276
1335
|
transport: defaultTransport,
|
|
1277
1336
|
schema: { ...defaultSchema, ...overrides.schema } as SchemaRegistry,
|
|
1278
1337
|
codec: defaultCodec,
|
|
1279
1338
|
retryPolicy: defaultRetryPolicy,
|
|
1280
1339
|
hooks: defaultHooks,
|
|
1340
|
+
dispatcher: defaultDispatcher,
|
|
1281
1341
|
};
|
|
1282
1342
|
}
|
|
1283
1343
|
|