@relaybus/kafka 0.2.0 → 0.3.1
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/README.md +86 -48
- package/dist/index.cjs +595 -443
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -63
- package/dist/index.d.ts +125 -63
- package/dist/index.js +583 -441
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError, assertEventType,
|
|
1
|
+
import { RelayBusError, NonRetryableError, assertEventType, toErrorInfo, systemClock, ConfigurationError } from '@relaybus/core';
|
|
2
|
+
import { createCloudEventReaders, DecodedEventReader, mediaTypeOf, isJsonObject, observeRejection, createJsonEnvelopeFormat, SubscribingTransport, writeStructuredCloudEvent, schedule, BoundedInteger, PositiveInteger, ExponentialBackoff, Milliseconds } from '@relaybus/core/internal';
|
|
3
|
+
export { fromHeader, fromPayloadField } from '@relaybus/core/internal';
|
|
2
4
|
|
|
3
|
-
// src/
|
|
5
|
+
// src/errors.ts
|
|
4
6
|
var InvalidKafkaMessageError = class extends RelayBusError {
|
|
7
|
+
name = "InvalidKafkaMessageError";
|
|
8
|
+
code = "RELAYBUS_KAFKA_INVALID_MESSAGE";
|
|
5
9
|
};
|
|
6
10
|
var UnencodableEventError = class extends NonRetryableError {
|
|
7
11
|
name = "UnencodableEventError";
|
|
12
|
+
code = "RELAYBUS_KAFKA_UNENCODABLE_EVENT";
|
|
13
|
+
};
|
|
14
|
+
var PublishTimeoutError = class extends RelayBusError {
|
|
15
|
+
name = "PublishTimeoutError";
|
|
16
|
+
code = "RELAYBUS_KAFKA_PUBLISH_TIMEOUT";
|
|
17
|
+
};
|
|
18
|
+
var TransportClosedError = class extends RelayBusError {
|
|
19
|
+
name = "TransportClosedError";
|
|
20
|
+
code = "RELAYBUS_KAFKA_TRANSPORT_CLOSED";
|
|
21
|
+
};
|
|
22
|
+
var GroupJoinTimeoutError = class extends RelayBusError {
|
|
23
|
+
name = "GroupJoinTimeoutError";
|
|
24
|
+
code = "RELAYBUS_KAFKA_GROUP_JOIN_TIMEOUT";
|
|
25
|
+
};
|
|
26
|
+
var HandlerFailedError = class extends RelayBusError {
|
|
27
|
+
name = "HandlerFailedError";
|
|
28
|
+
code = "RELAYBUS_KAFKA_HANDLER_FAILED";
|
|
8
29
|
};
|
|
9
30
|
var SPEC_VERSION = "1.0";
|
|
10
31
|
var CONTENT_TYPE = "content-type";
|
|
11
32
|
var JSON_MEDIA_TYPE = "application/json";
|
|
33
|
+
var STRUCTURED_MEDIA_TYPE = "application/cloudevents+json";
|
|
12
34
|
var CLOUDEVENTS_PREFIX = "ce_";
|
|
13
35
|
var ESCAPE_PREFIX = "x-relaybus-h-";
|
|
14
36
|
var RESERVED_PREFIXES = [CLOUDEVENTS_PREFIX, "x-relaybus-"];
|
|
@@ -23,6 +45,7 @@ var HEADER = {
|
|
|
23
45
|
causationId: "ce_causationid"
|
|
24
46
|
};
|
|
25
47
|
var PROTOCOL_HEADERS = /* @__PURE__ */ new Set([...Object.values(HEADER), CONTENT_TYPE]);
|
|
48
|
+
var MAXIMUM_RFC3339_YEAR = 9999;
|
|
26
49
|
var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
27
50
|
var strictUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
28
51
|
var lenientUtf8 = new TextDecoder("utf-8");
|
|
@@ -50,25 +73,39 @@ function encodeEnvelope(envelope) {
|
|
|
50
73
|
}
|
|
51
74
|
return { key: envelope.key, value, headers: Object.fromEntries(entries) };
|
|
52
75
|
}
|
|
76
|
+
function encodeStructuredEnvelope(envelope) {
|
|
77
|
+
const value = writeStructuredCloudEvent(envelope, {
|
|
78
|
+
time: formatTime(envelope),
|
|
79
|
+
data: serializePayload(envelope)
|
|
80
|
+
});
|
|
81
|
+
const entries = Object.entries(envelope.headers).map(
|
|
82
|
+
([name, headerValue]) => [escapeUserHeaderName(name), headerValue]
|
|
83
|
+
);
|
|
84
|
+
entries.push([CONTENT_TYPE, STRUCTURED_MEDIA_TYPE]);
|
|
85
|
+
return { key: envelope.key, value, headers: Object.fromEntries(entries) };
|
|
86
|
+
}
|
|
53
87
|
function serializePayload(envelope) {
|
|
54
|
-
|
|
88
|
+
return serializeJson(envelope, envelope.payload);
|
|
89
|
+
}
|
|
90
|
+
function serializeJson(envelope, value) {
|
|
91
|
+
let text;
|
|
55
92
|
try {
|
|
56
|
-
|
|
93
|
+
text = JSON.stringify(value);
|
|
57
94
|
} catch (error) {
|
|
58
95
|
throw new UnencodableEventError(
|
|
59
96
|
`Event ${envelope.id} has a payload that JSON cannot represent: ${error.message}`
|
|
60
97
|
);
|
|
61
98
|
}
|
|
62
|
-
if (
|
|
99
|
+
if (text === void 0) {
|
|
63
100
|
throw new UnencodableEventError(
|
|
64
101
|
`Event ${envelope.id} has a payload that JSON cannot represent (undefined, a function or a symbol)`
|
|
65
102
|
);
|
|
66
103
|
}
|
|
67
|
-
return
|
|
104
|
+
return text;
|
|
68
105
|
}
|
|
69
106
|
function formatTime(envelope) {
|
|
70
107
|
const year = envelope.occurredAt.getUTCFullYear();
|
|
71
|
-
if (Number.isNaN(year) || year < 0 || year >
|
|
108
|
+
if (Number.isNaN(year) || year < 0 || year > MAXIMUM_RFC3339_YEAR) {
|
|
72
109
|
throw new UnencodableEventError(
|
|
73
110
|
`Event ${envelope.id} has an occurredAt that is not an RFC 3339 instant (invalid date or year outside 0..9999)`
|
|
74
111
|
);
|
|
@@ -116,11 +153,14 @@ function readHeaders(incoming) {
|
|
|
116
153
|
if (typeof raw !== "string" && !(raw instanceof Uint8Array)) {
|
|
117
154
|
throw invalid(`Header '${name}' is repeated`);
|
|
118
155
|
}
|
|
119
|
-
headers.set(
|
|
156
|
+
headers.set(
|
|
157
|
+
name,
|
|
158
|
+
decodeText(raw, { strict: PROTOCOL_HEADERS.has(name), label: `Header '${name}'` })
|
|
159
|
+
);
|
|
120
160
|
}
|
|
121
161
|
return headers;
|
|
122
162
|
}
|
|
123
|
-
function decodeText(raw, strict, label) {
|
|
163
|
+
function decodeText(raw, { strict, label }) {
|
|
124
164
|
if (typeof raw === "string") {
|
|
125
165
|
return raw;
|
|
126
166
|
}
|
|
@@ -143,19 +183,19 @@ function assertJsonContentType(contentType) {
|
|
|
143
183
|
throw invalid(`${CONTENT_TYPE} must be '${JSON_MEDIA_TYPE}', received '${contentType ?? ""}'`);
|
|
144
184
|
}
|
|
145
185
|
}
|
|
146
|
-
function resolveTime(text,
|
|
186
|
+
function resolveTime(text, recordTimestamp2) {
|
|
147
187
|
if (text !== void 0 && text.length > 0) {
|
|
148
188
|
return parseTime(text);
|
|
149
189
|
}
|
|
150
|
-
const time = readRecordTimestamp(
|
|
190
|
+
const time = readRecordTimestamp(recordTimestamp2);
|
|
151
191
|
if (time === void 0) {
|
|
152
192
|
throw invalid(`${HEADER.time} is missing and the record has no usable timestamp`);
|
|
153
193
|
}
|
|
154
194
|
return time;
|
|
155
195
|
}
|
|
156
|
-
function readRecordTimestamp(
|
|
157
|
-
const time = new Date(Number(
|
|
158
|
-
return
|
|
196
|
+
function readRecordTimestamp(recordTimestamp2) {
|
|
197
|
+
const time = new Date(Number(recordTimestamp2));
|
|
198
|
+
return recordTimestamp2 === void 0 || recordTimestamp2 === "" || Number.isNaN(time.getTime()) ? void 0 : time;
|
|
159
199
|
}
|
|
160
200
|
function parseTime(text, label = HEADER.time) {
|
|
161
201
|
const time = new Date(text);
|
|
@@ -168,7 +208,7 @@ function parsePayload(value) {
|
|
|
168
208
|
if (value === null) {
|
|
169
209
|
throw invalid("Message has no value");
|
|
170
210
|
}
|
|
171
|
-
const text = decodeText(value, true, "Message value");
|
|
211
|
+
const text = decodeText(value, { strict: true, label: "Message value" });
|
|
172
212
|
try {
|
|
173
213
|
return JSON.parse(text);
|
|
174
214
|
} catch {
|
|
@@ -183,7 +223,7 @@ function resolveKey(subject, recordKey) {
|
|
|
183
223
|
return subject ?? keyText;
|
|
184
224
|
}
|
|
185
225
|
function readRecordKey(recordKey) {
|
|
186
|
-
return recordKey === null ? null : decodeText(recordKey, true, "Message key");
|
|
226
|
+
return recordKey === null ? null : decodeText(recordKey, { strict: true, label: "Message key" });
|
|
187
227
|
}
|
|
188
228
|
function collectUserHeaders(headers) {
|
|
189
229
|
const entries = [];
|
|
@@ -212,7 +252,8 @@ function toOutgoingHeaders(incoming) {
|
|
|
212
252
|
function toBufferOrString(value) {
|
|
213
253
|
return typeof value === "string" ? value : Buffer.from(value);
|
|
214
254
|
}
|
|
215
|
-
var
|
|
255
|
+
var { optionalAttribute, requiredAttribute, assertValidType, assertJsonDataContentType } = createCloudEventReaders(invalid);
|
|
256
|
+
var STRUCTURED_MEDIA_TYPE2 = "application/cloudevents+json";
|
|
216
257
|
var exactUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
217
258
|
var SPEC_VERSION2 = "1.0";
|
|
218
259
|
var SPEC_VERSION_HEADER = "ce_specversion";
|
|
@@ -232,7 +273,7 @@ var STRUCTURED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
|
232
273
|
]);
|
|
233
274
|
function decodeCloudEvent(record) {
|
|
234
275
|
const headers = readHeaders(record.headers);
|
|
235
|
-
if (mediaTypeOf(headers.get(CONTENT_TYPE)) !==
|
|
276
|
+
if (mediaTypeOf(headers.get(CONTENT_TYPE)) !== STRUCTURED_MEDIA_TYPE2) {
|
|
236
277
|
return decodeEnvelope(record);
|
|
237
278
|
}
|
|
238
279
|
const body = parsePayload(record.value);
|
|
@@ -267,89 +308,59 @@ function plainJsonDecoder(options) {
|
|
|
267
308
|
}
|
|
268
309
|
return (record) => {
|
|
269
310
|
const headers = readHeadersLeniently(record.headers);
|
|
270
|
-
if (record.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(headers[CONTENT_TYPE]) ===
|
|
311
|
+
if (record.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(headers[CONTENT_TYPE]) === STRUCTURED_MEDIA_TYPE2) {
|
|
271
312
|
return decodeCloudEvent(record);
|
|
272
313
|
}
|
|
273
|
-
const plain =
|
|
274
|
-
|
|
275
|
-
partition: record.partition,
|
|
276
|
-
offset: record.offset,
|
|
277
|
-
key: readKeyLeniently(record.key),
|
|
278
|
-
headers,
|
|
279
|
-
payload: parsePayload(record.value),
|
|
280
|
-
timestamp: record.timestamp
|
|
281
|
-
};
|
|
282
|
-
const type = resolveText(options.type, plain, "type");
|
|
314
|
+
const plain = plainRecordOf(record, headers);
|
|
315
|
+
const type = KAFKA_EVENTS.text(options.type, { plain, name: "type" });
|
|
283
316
|
if (type === void 0) {
|
|
284
317
|
throw invalid("Record has no event type: the type option found none");
|
|
285
318
|
}
|
|
286
319
|
assertValidType(type, "The event type");
|
|
287
320
|
return {
|
|
288
|
-
id:
|
|
321
|
+
id: KAFKA_EVENTS.text(options.id, { plain, name: "id" }) ?? `kafka:${record.topic}:${record.partition}:${record.offset}`,
|
|
289
322
|
type,
|
|
290
|
-
source:
|
|
291
|
-
key: options.key === void 0 ? plain.key :
|
|
323
|
+
source: KAFKA_EVENTS.text(options.source, { plain, name: "source" }) ?? record.topic,
|
|
324
|
+
key: options.key === void 0 ? plain.key : KAFKA_EVENTS.text(options.key, { plain, name: "key" }) ?? null,
|
|
292
325
|
payload: plain.payload,
|
|
293
326
|
headers,
|
|
294
327
|
correlationId: null,
|
|
295
328
|
causationId: null,
|
|
296
|
-
occurredAt: resolveOccurredAt(options.occurredAt, plain, record)
|
|
329
|
+
occurredAt: resolveOccurredAt(options.occurredAt, { plain, record })
|
|
297
330
|
};
|
|
298
331
|
};
|
|
299
332
|
}
|
|
300
|
-
function
|
|
301
|
-
if (typeof decoded !== "object" || decoded === null) {
|
|
302
|
-
throw invalid("The decoder must return an event object synchronously");
|
|
303
|
-
}
|
|
304
|
-
if ("then" in decoded) {
|
|
305
|
-
observeRejection(decoded);
|
|
306
|
-
throw invalid("The decoder must return an event object synchronously");
|
|
307
|
-
}
|
|
308
|
-
const event = decoded;
|
|
333
|
+
function plainRecordOf(record, headers) {
|
|
309
334
|
return {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
key:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
causationId: event.causationId ?? null,
|
|
318
|
-
occurredAt: event.occurredAt ?? requireRecordTimestamp(record)
|
|
335
|
+
topic: record.topic,
|
|
336
|
+
partition: record.partition,
|
|
337
|
+
offset: record.offset,
|
|
338
|
+
key: readKeyLeniently(record.key),
|
|
339
|
+
headers,
|
|
340
|
+
payload: parsePayload(record.value),
|
|
341
|
+
timestamp: record.timestamp
|
|
319
342
|
};
|
|
320
343
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
function isJsonObject(value) {
|
|
325
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
326
|
-
}
|
|
327
|
-
function optionalAttribute(body, name) {
|
|
328
|
-
const value = body[name];
|
|
329
|
-
if (value !== void 0 && typeof value !== "string") {
|
|
330
|
-
throw invalid(`Attribute '${name}' must be a string`);
|
|
344
|
+
var KafkaDecodedEvents = class extends DecodedEventReader {
|
|
345
|
+
invalid(reason) {
|
|
346
|
+
return invalid(reason);
|
|
331
347
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const value = optionalAttribute(body, name);
|
|
336
|
-
if (value === void 0 || value.length === 0) {
|
|
337
|
-
throw invalid(`Attribute '${name}' is missing or empty`);
|
|
348
|
+
/** The topic. */
|
|
349
|
+
defaultSource(record) {
|
|
350
|
+
return record.topic;
|
|
338
351
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
assertEventType(type);
|
|
344
|
-
} catch (error) {
|
|
345
|
-
throw invalid(`${label} is not a valid event type: ${error.message}`);
|
|
352
|
+
/** The record key as UTF-8 text. */
|
|
353
|
+
defaultKey(record) {
|
|
354
|
+
return readRecordKey(record.key);
|
|
346
355
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (mediaType !== void 0 && mediaType !== "application/json" && mediaType !== "text/json" && !mediaType.endsWith("+json")) {
|
|
351
|
-
throw invalid(`datacontenttype must be JSON, received '${dataContentType}'`);
|
|
356
|
+
/** The record timestamp; a record without a usable one is malformed. */
|
|
357
|
+
defaultTime(record) {
|
|
358
|
+
return requireRecordTimestamp(record);
|
|
352
359
|
}
|
|
360
|
+
};
|
|
361
|
+
var KAFKA_EVENTS = new KafkaDecodedEvents();
|
|
362
|
+
function completeDecodedEvent(decoded, record) {
|
|
363
|
+
return KAFKA_EVENTS.complete(decoded, record);
|
|
353
364
|
}
|
|
354
365
|
function extensionAttributes(body) {
|
|
355
366
|
return Object.fromEntries(
|
|
@@ -396,18 +407,7 @@ function readKeyLeniently(key) {
|
|
|
396
407
|
return Buffer.from(key).toString("base64");
|
|
397
408
|
}
|
|
398
409
|
}
|
|
399
|
-
function
|
|
400
|
-
const value = typeof option === "function" ? option(record) : option;
|
|
401
|
-
if (value === void 0 || value === null) {
|
|
402
|
-
return void 0;
|
|
403
|
-
}
|
|
404
|
-
if (typeof value !== "string") {
|
|
405
|
-
observeRejection(value);
|
|
406
|
-
throw invalid(`The ${name} option returned ${typeof value}, expected a string`);
|
|
407
|
-
}
|
|
408
|
-
return value.length === 0 ? void 0 : value;
|
|
409
|
-
}
|
|
410
|
-
function resolveOccurredAt(option, plain, record) {
|
|
410
|
+
function resolveOccurredAt(option, { plain, record }) {
|
|
411
411
|
const value = option?.(plain);
|
|
412
412
|
if (value === void 0) {
|
|
413
413
|
return requireRecordTimestamp(record);
|
|
@@ -418,18 +418,205 @@ function resolveOccurredAt(option, plain, record) {
|
|
|
418
418
|
}
|
|
419
419
|
return value;
|
|
420
420
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
421
|
+
|
|
422
|
+
// src/codecs.ts
|
|
423
|
+
var cloudEventsBinary = Object.freeze({
|
|
424
|
+
encode: encodeEnvelope,
|
|
425
|
+
decode: decodeEnvelope
|
|
426
|
+
});
|
|
427
|
+
var cloudEventsStructured = Object.freeze({
|
|
428
|
+
encode: encodeStructuredEnvelope,
|
|
429
|
+
decode: decodeCloudEvent
|
|
430
|
+
});
|
|
431
|
+
var RECORD_POSITION = "record-position";
|
|
432
|
+
var EVENT_ID_HEADER = "relaybus-event-id";
|
|
433
|
+
var DEFAULT_CONTENT_TYPE = "application/json";
|
|
434
|
+
function jsonEnvelope(mapping) {
|
|
435
|
+
const format = createJsonEnvelopeFormat(mapping, {
|
|
436
|
+
transportIds: [RECORD_POSITION],
|
|
437
|
+
failures: { invalid, unencodable: (reason) => new UnencodableEventError(reason) }
|
|
438
|
+
});
|
|
439
|
+
const contentType = mapping.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
440
|
+
const stampsTime = mapping.occurredAt === void 0;
|
|
441
|
+
const ownHeaders = (envelope) => mapping.id === RECORD_POSITION ? { [CONTENT_TYPE]: contentType, [EVENT_ID_HEADER]: envelope.id } : { [CONTENT_TYPE]: contentType };
|
|
442
|
+
return {
|
|
443
|
+
encode: (envelope) => encodeJson(envelope, { format, stampsTime, ownHeaders }),
|
|
444
|
+
decode: (record) => decodeJson(record, format)
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function encodeJson(envelope, { format, stampsTime, ownHeaders }) {
|
|
448
|
+
assertRepresentable(envelope);
|
|
449
|
+
const { body, headers } = format.write(envelope);
|
|
450
|
+
const taken = [CONTENT_TYPE, EVENT_ID_HEADER].find((name) => Object.hasOwn(headers, name));
|
|
451
|
+
if (taken !== void 0) {
|
|
452
|
+
throw new UnencodableEventError(
|
|
453
|
+
`Event ${envelope.id} has a header '${taken}', which the codec writes itself`
|
|
454
|
+
);
|
|
424
455
|
}
|
|
456
|
+
return {
|
|
457
|
+
key: envelope.key,
|
|
458
|
+
value: serializeJson(envelope, body),
|
|
459
|
+
headers: { ...headers, ...ownHeaders(envelope) },
|
|
460
|
+
...stampsTime ? { timestamp: recordTimestamp(envelope) } : {}
|
|
461
|
+
};
|
|
425
462
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
463
|
+
function decodeJson(record, format) {
|
|
464
|
+
const { [EVENT_ID_HEADER]: eventId, ...headers } = withoutContentType(
|
|
465
|
+
readHeadersLeniently(record.headers)
|
|
466
|
+
);
|
|
467
|
+
const fields = format.read({ body: parsePayload(record.value), headers });
|
|
468
|
+
return {
|
|
469
|
+
...fields,
|
|
470
|
+
id: fields.id ?? (eventId === void 0 || eventId === "" ? `kafka:${record.topic}:${record.partition}:${record.offset}` : eventId)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function withoutContentType(headers) {
|
|
474
|
+
return Object.fromEntries(Object.entries(headers).filter(([name]) => name !== CONTENT_TYPE));
|
|
475
|
+
}
|
|
476
|
+
function assertRepresentable(envelope) {
|
|
477
|
+
const kind = typeof envelope.payload;
|
|
478
|
+
if (kind === "undefined" || kind === "function" || kind === "symbol") {
|
|
479
|
+
throw new UnencodableEventError(
|
|
480
|
+
`Event ${envelope.id} has a payload that JSON cannot represent (undefined, a function or a symbol)`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function recordTimestamp(envelope) {
|
|
485
|
+
formatTime(envelope);
|
|
486
|
+
return String(envelope.occurredAt.getTime());
|
|
487
|
+
}
|
|
488
|
+
function tracksMembership(consumer) {
|
|
489
|
+
return consumer.on !== void 0 && consumer.events?.GROUP_JOIN !== void 0;
|
|
490
|
+
}
|
|
491
|
+
var GroupJoinWatch = class {
|
|
492
|
+
#settings;
|
|
493
|
+
#waiter;
|
|
494
|
+
constructor(settings) {
|
|
495
|
+
this.#settings = settings;
|
|
496
|
+
}
|
|
497
|
+
/** The consumer joined its group. */
|
|
498
|
+
joined() {
|
|
499
|
+
this.#waiter?.resolve();
|
|
500
|
+
}
|
|
501
|
+
/** The consumer crashed while it was joining. */
|
|
502
|
+
failed(error) {
|
|
503
|
+
this.#waiter?.reject(error);
|
|
504
|
+
}
|
|
505
|
+
/** The transport closed while the consumer was joining. */
|
|
506
|
+
closed() {
|
|
507
|
+
this.#waiter?.reject(new TransportClosedError(`Transport '${this.#settings.name}' is closed`));
|
|
508
|
+
}
|
|
509
|
+
/** Connects and subscribes the consumer, runs it and resolves once it joined its group. */
|
|
510
|
+
async start(consumer, {
|
|
511
|
+
topics,
|
|
512
|
+
fromBeginning,
|
|
513
|
+
eachMessage
|
|
514
|
+
}) {
|
|
515
|
+
await consumer.connect();
|
|
516
|
+
await consumer.subscribe({ topics, fromBeginning });
|
|
517
|
+
const joined = this.#wait(consumer);
|
|
518
|
+
try {
|
|
519
|
+
await consumer.run({ eachMessage });
|
|
520
|
+
await joined.promise;
|
|
521
|
+
} finally {
|
|
522
|
+
joined.dispose();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
#wait(consumer) {
|
|
526
|
+
if (!tracksMembership(consumer)) {
|
|
527
|
+
return { promise: Promise.resolve(), dispose: () => void 0 };
|
|
528
|
+
}
|
|
529
|
+
const { name, groupId, timings } = this.#settings;
|
|
530
|
+
let cancel;
|
|
531
|
+
const promise = new Promise((resolve, reject) => {
|
|
532
|
+
this.#waiter = { resolve, reject };
|
|
533
|
+
cancel = schedule(systemClock, {
|
|
534
|
+
run: () => {
|
|
535
|
+
reject(
|
|
536
|
+
new GroupJoinTimeoutError(
|
|
537
|
+
`Consumer of transport '${name}' did not join group '${groupId}' within ${timings.startupTimeoutMs}ms`
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
},
|
|
541
|
+
delayMs: timings.startupTimeoutMs
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
promise.catch(() => void 0);
|
|
545
|
+
return {
|
|
546
|
+
promise,
|
|
547
|
+
dispose: () => {
|
|
548
|
+
cancel?.();
|
|
549
|
+
this.#waiter = void 0;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
430
553
|
};
|
|
431
|
-
var
|
|
432
|
-
|
|
554
|
+
var KafkaProducerSession = class {
|
|
555
|
+
#settings;
|
|
556
|
+
/** Whether the transport is closed: then nothing connects any more. */
|
|
557
|
+
#isClosed;
|
|
558
|
+
#producer;
|
|
559
|
+
#connected = false;
|
|
560
|
+
constructor(settings, isClosed) {
|
|
561
|
+
this.#settings = settings;
|
|
562
|
+
this.#isClosed = isClosed;
|
|
563
|
+
}
|
|
564
|
+
/** True once the producer connected, until the transport closes. */
|
|
565
|
+
get isConnected() {
|
|
566
|
+
return this.#connected;
|
|
567
|
+
}
|
|
568
|
+
/** The producer opened so far, if any: what closing must disconnect. */
|
|
569
|
+
get pending() {
|
|
570
|
+
return this.#producer;
|
|
571
|
+
}
|
|
572
|
+
markClosed() {
|
|
573
|
+
this.#connected = false;
|
|
574
|
+
}
|
|
575
|
+
connected() {
|
|
576
|
+
if (this.#isClosed()) {
|
|
577
|
+
return Promise.reject(
|
|
578
|
+
new TransportClosedError(`Transport '${this.#settings.name}' is closed`)
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
this.#producer ??= this.#open();
|
|
582
|
+
return this.#producer;
|
|
583
|
+
}
|
|
584
|
+
/** Sends with the publish timeout; the late outcome of a timed-out send is ignored. */
|
|
585
|
+
async send(producer, record) {
|
|
586
|
+
const { name, timings } = this.#settings;
|
|
587
|
+
let cancel;
|
|
588
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
589
|
+
cancel = schedule(systemClock, {
|
|
590
|
+
run: () => {
|
|
591
|
+
reject(
|
|
592
|
+
new PublishTimeoutError(
|
|
593
|
+
`Transport '${name}' got no broker ack for topic '${record.topic}' within ${timings.publishTimeoutMs}ms`
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
},
|
|
597
|
+
delayMs: timings.publishTimeoutMs
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
const sending = producer.send(record);
|
|
601
|
+
sending.catch(() => void 0);
|
|
602
|
+
try {
|
|
603
|
+
await Promise.race([sending, timeout]);
|
|
604
|
+
} finally {
|
|
605
|
+
cancel?.();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async #open() {
|
|
609
|
+
const { kafka, producerConfig, maxInFlightRequests } = this.#settings;
|
|
610
|
+
const producer = kafka.producer({ ...producerConfig, idempotent: true, maxInFlightRequests });
|
|
611
|
+
try {
|
|
612
|
+
await producer.connect();
|
|
613
|
+
} catch (error) {
|
|
614
|
+
this.#producer = void 0;
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
this.#connected = !this.#isClosed();
|
|
618
|
+
return producer;
|
|
619
|
+
}
|
|
433
620
|
};
|
|
434
621
|
var DEFAULTS = {
|
|
435
622
|
name: "kafka",
|
|
@@ -443,46 +630,224 @@ var DEFAULTS = {
|
|
|
443
630
|
restartInitialBackoff: "1s",
|
|
444
631
|
restartMaxBackoff: "30s"
|
|
445
632
|
};
|
|
446
|
-
var
|
|
633
|
+
var MAXIMUM_IN_FLIGHT_REQUESTS = 5;
|
|
634
|
+
function resolveKafkaSettings(options) {
|
|
635
|
+
return {
|
|
636
|
+
name: options.name ?? DEFAULTS.name,
|
|
637
|
+
kafka: options.kafka,
|
|
638
|
+
groupId: options.groupId,
|
|
639
|
+
fromBeginning: options.fromBeginning ?? true,
|
|
640
|
+
maxInFlightRequests: BoundedInteger.of(
|
|
641
|
+
options.maxInFlightRequests ?? DEFAULTS.maxInFlightRequests,
|
|
642
|
+
{ name: "maxInFlightRequests", minimum: 1, maximum: MAXIMUM_IN_FLIGHT_REQUESTS }
|
|
643
|
+
),
|
|
644
|
+
producerConfig: options.producerConfig ?? {},
|
|
645
|
+
consumerConfig: options.consumerConfig ?? {},
|
|
646
|
+
invalidPolicy: options.onInvalidMessage ?? "dead-letter",
|
|
647
|
+
codec: resolveCodec(options),
|
|
648
|
+
logger: options.logger ?? console,
|
|
649
|
+
timings: resolveTimings(options)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function resolveCodec({ codec, decode }) {
|
|
653
|
+
if (decode === void 0) {
|
|
654
|
+
return codec ?? cloudEventsBinary;
|
|
655
|
+
}
|
|
656
|
+
if (codec !== void 0) {
|
|
657
|
+
throw new ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
|
|
658
|
+
}
|
|
659
|
+
return { encode: encodeEnvelope, decode };
|
|
660
|
+
}
|
|
661
|
+
function resolveTimings(options) {
|
|
662
|
+
const processing = options.processing ?? {};
|
|
663
|
+
const maxAttempts = PositiveInteger.of(
|
|
664
|
+
processing.maxAttempts ?? DEFAULTS.maxAttempts,
|
|
665
|
+
"processing.maxAttempts"
|
|
666
|
+
);
|
|
667
|
+
return {
|
|
668
|
+
maxAttempts,
|
|
669
|
+
processingBackoff: new ExponentialBackoff({
|
|
670
|
+
initialMs: Milliseconds.of(
|
|
671
|
+
processing.initialBackoff ?? DEFAULTS.initialBackoff,
|
|
672
|
+
"processing.initialBackoff"
|
|
673
|
+
),
|
|
674
|
+
maxMs: Milliseconds.of(processing.maxBackoff ?? DEFAULTS.maxBackoff, "processing.maxBackoff")
|
|
675
|
+
}),
|
|
676
|
+
startupTimeoutMs: Milliseconds.of(
|
|
677
|
+
options.startupTimeout ?? DEFAULTS.startupTimeout,
|
|
678
|
+
"startupTimeout"
|
|
679
|
+
),
|
|
680
|
+
publishTimeoutMs: Milliseconds.of(
|
|
681
|
+
options.publishTimeout ?? DEFAULTS.publishTimeout,
|
|
682
|
+
"publishTimeout"
|
|
683
|
+
),
|
|
684
|
+
heartbeatIntervalMs: Math.max(
|
|
685
|
+
1,
|
|
686
|
+
Milliseconds.of(
|
|
687
|
+
processing.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
688
|
+
"processing.heartbeatInterval"
|
|
689
|
+
)
|
|
690
|
+
),
|
|
691
|
+
restartBackoff: resolveRestartBackoff(options.restart ?? {})
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function resolveRestartBackoff(restart) {
|
|
695
|
+
return new ExponentialBackoff({
|
|
696
|
+
initialMs: Milliseconds.of(
|
|
697
|
+
restart.initialBackoff ?? DEFAULTS.restartInitialBackoff,
|
|
698
|
+
"restart.initialBackoff"
|
|
699
|
+
),
|
|
700
|
+
maxMs: Milliseconds.of(restart.maxBackoff ?? DEFAULTS.restartMaxBackoff, "restart.maxBackoff")
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function deadLetterTopicFor(policy, sourceTopic) {
|
|
704
|
+
if (policy === "dead-letter") {
|
|
705
|
+
return `${sourceTopic}.dead`;
|
|
706
|
+
}
|
|
707
|
+
if (typeof policy === "object") {
|
|
708
|
+
const { deadLetterTopic } = policy;
|
|
709
|
+
return typeof deadLetterTopic === "function" ? deadLetterTopic(sourceTopic) : deadLetterTopic;
|
|
710
|
+
}
|
|
711
|
+
return void 0;
|
|
712
|
+
}
|
|
713
|
+
function toHandlerFailure(error) {
|
|
714
|
+
const { name, message } = toErrorInfo(error);
|
|
715
|
+
return new HandlerFailedError(`${name}: ${message}`);
|
|
716
|
+
}
|
|
717
|
+
function assertCanSubscribe(state, {
|
|
718
|
+
settings,
|
|
719
|
+
destinations
|
|
720
|
+
}) {
|
|
721
|
+
const { name, groupId, invalidPolicy } = settings;
|
|
722
|
+
if (state === "closed") {
|
|
723
|
+
throw new TransportClosedError(`Transport '${name}' is closed`);
|
|
724
|
+
}
|
|
725
|
+
if (state === "subscribed") {
|
|
726
|
+
throw new ConfigurationError(
|
|
727
|
+
`Transport '${name}' already subscribed; list every topic in one subscription or use one KafkaTransport per consumer group`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
if (groupId === void 0 || groupId.length === 0) {
|
|
731
|
+
throw new ConfigurationError(`Transport '${name}' needs a groupId to subscribe`);
|
|
732
|
+
}
|
|
733
|
+
if (destinations.length === 0) {
|
|
734
|
+
throw new ConfigurationError(`Transport '${name}' subscribed to no topics`);
|
|
735
|
+
}
|
|
736
|
+
for (const topic of destinations) {
|
|
737
|
+
const deadLetterTopic = deadLetterTopicFor(invalidPolicy, topic);
|
|
738
|
+
if (deadLetterTopic !== void 0 && destinations.includes(deadLetterTopic)) {
|
|
739
|
+
throw new ConfigurationError(
|
|
740
|
+
`Dead letter topic '${deadLetterTopic}' is also consumed, which would loop`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/kafka-records.ts
|
|
747
|
+
function decodeRecord(codec, { topic, partition, message }) {
|
|
748
|
+
if (codec === cloudEventsBinary) {
|
|
749
|
+
return decodeEnvelope(message);
|
|
750
|
+
}
|
|
751
|
+
const record = {
|
|
752
|
+
topic,
|
|
753
|
+
partition,
|
|
754
|
+
offset: message.offset,
|
|
755
|
+
key: message.key,
|
|
756
|
+
value: message.value,
|
|
757
|
+
headers: message.headers,
|
|
758
|
+
timestamp: message.timestamp
|
|
759
|
+
};
|
|
760
|
+
return completeDecodedEvent(codec.decode(record), record);
|
|
761
|
+
}
|
|
762
|
+
function deadLetterRecord({ topic, partition, message }, {
|
|
763
|
+
reason,
|
|
764
|
+
name,
|
|
765
|
+
code,
|
|
766
|
+
deadLetterTopic
|
|
767
|
+
}) {
|
|
768
|
+
const headers = {
|
|
769
|
+
...toOutgoingHeaders(message.headers),
|
|
770
|
+
"relaybus-invalid-reason": reason,
|
|
771
|
+
"relaybus-source-topic": topic,
|
|
772
|
+
"relaybus-source-partition": String(partition),
|
|
773
|
+
"relaybus-source-offset": message.offset,
|
|
774
|
+
"relaybus-error-name": name
|
|
775
|
+
};
|
|
776
|
+
if (code === void 0) {
|
|
777
|
+
delete headers["relaybus-error-code"];
|
|
778
|
+
} else {
|
|
779
|
+
headers["relaybus-error-code"] = code;
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
topic: deadLetterTopic,
|
|
783
|
+
acks: -1,
|
|
784
|
+
messages: [
|
|
785
|
+
{
|
|
786
|
+
key: message.key === null ? null : Buffer.from(message.key),
|
|
787
|
+
value: message.value === null ? null : Buffer.from(message.value),
|
|
788
|
+
headers
|
|
789
|
+
}
|
|
790
|
+
]
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
var WakeableSleeps = class {
|
|
794
|
+
#sleepers = /* @__PURE__ */ new Set();
|
|
795
|
+
sleep(milliseconds) {
|
|
796
|
+
return new Promise((resolve) => {
|
|
797
|
+
const wake = () => {
|
|
798
|
+
cancel();
|
|
799
|
+
this.#sleepers.delete(wake);
|
|
800
|
+
resolve();
|
|
801
|
+
};
|
|
802
|
+
const cancel = schedule(systemClock, { run: wake, delayMs: milliseconds });
|
|
803
|
+
this.#sleepers.add(wake);
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
wakeAll() {
|
|
807
|
+
for (const wake of this.#sleepers) {
|
|
808
|
+
wake();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Sleeps `milliseconds` in slices of at most `sliceMs`, running `between` after each, until done
|
|
813
|
+
* or `stopped`: a long backoff then never outlives the session timeout.
|
|
814
|
+
*/
|
|
815
|
+
async sleepInSlices(milliseconds, {
|
|
816
|
+
sliceMs,
|
|
817
|
+
between,
|
|
818
|
+
stopped
|
|
819
|
+
}) {
|
|
820
|
+
let remaining = milliseconds;
|
|
821
|
+
do {
|
|
822
|
+
const slice = Math.min(remaining, sliceMs);
|
|
823
|
+
await this.sleep(slice);
|
|
824
|
+
await between();
|
|
825
|
+
remaining -= slice;
|
|
826
|
+
} while (remaining > 0 && !stopped());
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// src/kafka-transport.ts
|
|
831
|
+
var KafkaTransport = class extends SubscribingTransport {
|
|
447
832
|
name;
|
|
448
|
-
|
|
449
|
-
#groupId;
|
|
450
|
-
#fromBeginning;
|
|
451
|
-
#maxInFlightRequests;
|
|
452
|
-
#producerConfig;
|
|
453
|
-
#consumerConfig;
|
|
454
|
-
#invalidPolicy;
|
|
455
|
-
#decode;
|
|
456
|
-
#logger;
|
|
833
|
+
system = "kafka";
|
|
457
834
|
#settings;
|
|
458
|
-
#sleepers = /* @__PURE__ */ new Set();
|
|
459
|
-
#state = "idle";
|
|
460
835
|
#producer;
|
|
836
|
+
#groupJoin;
|
|
837
|
+
#sleeps = new WakeableSleeps();
|
|
838
|
+
#state = "idle";
|
|
461
839
|
#consumer;
|
|
462
840
|
#groupJoined = false;
|
|
463
841
|
#consuming = false;
|
|
464
|
-
#producerConnected = false;
|
|
465
842
|
#restartLoop;
|
|
466
843
|
#stalledPartitions = /* @__PURE__ */ new Set();
|
|
467
844
|
#startConsuming;
|
|
468
|
-
#joinWaiter;
|
|
469
845
|
constructor(options) {
|
|
470
|
-
|
|
471
|
-
this.#
|
|
472
|
-
this
|
|
473
|
-
this.#
|
|
474
|
-
this.#
|
|
475
|
-
if (!Number.isInteger(this.#maxInFlightRequests) || this.#maxInFlightRequests < 1 || this.#maxInFlightRequests > 5) {
|
|
476
|
-
throw new ConfigurationError(
|
|
477
|
-
`maxInFlightRequests must be an integer from 1 to 5, received ${this.#maxInFlightRequests}`
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
this.#producerConfig = options.producerConfig ?? {};
|
|
481
|
-
this.#consumerConfig = options.consumerConfig ?? {};
|
|
482
|
-
this.#invalidPolicy = options.onInvalidMessage ?? "dead-letter";
|
|
483
|
-
this.#decode = options.decode;
|
|
484
|
-
this.#logger = options.logger ?? console;
|
|
485
|
-
this.#settings = resolveSettings(options);
|
|
846
|
+
super();
|
|
847
|
+
this.#settings = resolveKafkaSettings(options);
|
|
848
|
+
this.name = this.#settings.name;
|
|
849
|
+
this.#producer = new KafkaProducerSession(this.#settings, () => this.#state === "closed");
|
|
850
|
+
this.#groupJoin = new GroupJoinWatch(this.#settings);
|
|
486
851
|
}
|
|
487
852
|
/**
|
|
488
853
|
* True while the consumer is joined to its group, not between a crash or
|
|
@@ -499,22 +864,21 @@ var KafkaTransport = class {
|
|
|
499
864
|
* dead consumer is being restarted.
|
|
500
865
|
*/
|
|
501
866
|
isReady() {
|
|
502
|
-
if (this.#state === "closed" || !this.#
|
|
867
|
+
if (this.#state === "closed" || !this.#producer.isConnected) {
|
|
503
868
|
return false;
|
|
504
869
|
}
|
|
505
870
|
return this.#state === "idle" || this.isConsuming();
|
|
506
871
|
}
|
|
507
872
|
/** Connects the producer. Called by the bus on start; `publish` also connects on demand. */
|
|
508
873
|
async connect() {
|
|
509
|
-
await this.#
|
|
874
|
+
await this.#producer.connected();
|
|
510
875
|
}
|
|
511
876
|
async publish(envelope, destination) {
|
|
512
|
-
const producer = await this.#
|
|
513
|
-
|
|
514
|
-
await this.#send(producer, {
|
|
877
|
+
const producer = await this.#producer.connected();
|
|
878
|
+
await this.#producer.send(producer, {
|
|
515
879
|
topic: destination,
|
|
516
880
|
acks: -1,
|
|
517
|
-
messages: [
|
|
881
|
+
messages: [this.#settings.codec.encode(envelope)]
|
|
518
882
|
});
|
|
519
883
|
}
|
|
520
884
|
/**
|
|
@@ -524,13 +888,21 @@ var KafkaTransport = class {
|
|
|
524
888
|
* @throws ConfigurationError when `groupId` is missing, `destinations` is empty
|
|
525
889
|
* or the transport already subscribed (create one transport per subscription).
|
|
526
890
|
*/
|
|
527
|
-
|
|
528
|
-
this.#
|
|
891
|
+
openSubscription(subscription) {
|
|
892
|
+
return this.#subscribe(subscription);
|
|
893
|
+
}
|
|
894
|
+
async #subscribe({ destinations, onMessage, hooks }) {
|
|
895
|
+
assertCanSubscribe(this.#state, { settings: this.#settings, destinations });
|
|
529
896
|
this.#state = "subscribed";
|
|
530
|
-
const
|
|
897
|
+
const { kafka, consumerConfig, groupId } = this.#settings;
|
|
898
|
+
const consumer = kafka.consumer({ ...consumerConfig, groupId });
|
|
531
899
|
this.#consumer = consumer;
|
|
532
900
|
this.#observeConsumer(consumer);
|
|
533
|
-
const start = () => this.#
|
|
901
|
+
const start = () => this.#groupJoin.start(consumer, {
|
|
902
|
+
topics: [...destinations],
|
|
903
|
+
fromBeginning: this.#settings.fromBeginning,
|
|
904
|
+
eachMessage: (payload) => this.#handleMessage(payload, { onMessage, hooks })
|
|
905
|
+
});
|
|
534
906
|
this.#startConsuming = start;
|
|
535
907
|
try {
|
|
536
908
|
await start();
|
|
@@ -542,34 +914,21 @@ var KafkaTransport = class {
|
|
|
542
914
|
throw error;
|
|
543
915
|
}
|
|
544
916
|
this.#consuming = true;
|
|
545
|
-
this.#logger.log(
|
|
546
|
-
`[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${
|
|
917
|
+
this.#settings.logger.log(
|
|
918
|
+
`[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${groupId} topics=${destinations.join(",")}`
|
|
547
919
|
);
|
|
548
920
|
}
|
|
549
|
-
async #startConsumer(consumer, destinations, onMessage) {
|
|
550
|
-
await consumer.connect();
|
|
551
|
-
await consumer.subscribe({ topics: destinations, fromBeginning: this.#fromBeginning });
|
|
552
|
-
const joined = this.#waitForGroupJoin(consumer);
|
|
553
|
-
try {
|
|
554
|
-
await consumer.run({ eachMessage: (payload) => this.#handleMessage(payload, onMessage) });
|
|
555
|
-
await joined.promise;
|
|
556
|
-
} finally {
|
|
557
|
-
joined.dispose();
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
921
|
/** Stops consuming and disconnects the consumer and the producer. Safe to call twice. */
|
|
561
922
|
async close() {
|
|
562
923
|
if (this.#state === "closed") {
|
|
563
924
|
return;
|
|
564
925
|
}
|
|
565
926
|
this.#state = "closed";
|
|
566
|
-
const pendingProducer = this.#producer;
|
|
927
|
+
const pendingProducer = this.#producer.pending;
|
|
567
928
|
this.#groupJoined = false;
|
|
568
|
-
this.#
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
this.#joinWaiter?.reject(new RelayBusError(`Transport '${this.name}' is closed`));
|
|
929
|
+
this.#producer.markClosed();
|
|
930
|
+
this.#sleeps.wakeAll();
|
|
931
|
+
this.#groupJoin.closed();
|
|
573
932
|
await this.#restartLoop;
|
|
574
933
|
const producer = await pendingProducer?.catch(() => void 0);
|
|
575
934
|
const results = await Promise.allSettled([
|
|
@@ -581,130 +940,45 @@ var KafkaTransport = class {
|
|
|
581
940
|
throw failure.reason;
|
|
582
941
|
}
|
|
583
942
|
}
|
|
584
|
-
#
|
|
585
|
-
if (this.#state === "closed") {
|
|
586
|
-
throw new RelayBusError(`Transport '${this.name}' is closed`);
|
|
587
|
-
}
|
|
588
|
-
if (this.#state === "subscribed") {
|
|
589
|
-
throw new ConfigurationError(
|
|
590
|
-
`Transport '${this.name}' already subscribed; list every topic in one subscription or use one KafkaTransport per consumer group`
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
if (this.#groupId === void 0 || this.#groupId.length === 0) {
|
|
594
|
-
throw new ConfigurationError(`Transport '${this.name}' needs a groupId to subscribe`);
|
|
595
|
-
}
|
|
596
|
-
if (destinations.length === 0) {
|
|
597
|
-
throw new ConfigurationError(`Transport '${this.name}' subscribed to no topics`);
|
|
598
|
-
}
|
|
599
|
-
for (const topic of destinations) {
|
|
600
|
-
const deadLetterTopic = this.#deadLetterTopicFor(topic);
|
|
601
|
-
if (deadLetterTopic !== void 0 && destinations.includes(deadLetterTopic)) {
|
|
602
|
-
throw new ConfigurationError(
|
|
603
|
-
`Dead letter topic '${deadLetterTopic}' is also consumed, which would loop`
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
/** The dead-letter topic for records of `sourceTopic`, or undefined when the policy does not use one. */
|
|
609
|
-
#deadLetterTopicFor(sourceTopic) {
|
|
610
|
-
const policy = this.#invalidPolicy;
|
|
611
|
-
if (policy === "dead-letter") {
|
|
612
|
-
return `${sourceTopic}.dead`;
|
|
613
|
-
}
|
|
614
|
-
if (typeof policy === "object") {
|
|
615
|
-
const { deadLetterTopic } = policy;
|
|
616
|
-
return typeof deadLetterTopic === "function" ? deadLetterTopic(sourceTopic) : deadLetterTopic;
|
|
617
|
-
}
|
|
618
|
-
return void 0;
|
|
619
|
-
}
|
|
620
|
-
/** Sends with the publish timeout; the late outcome of a timed-out send is ignored. */
|
|
621
|
-
async #send(producer, record) {
|
|
622
|
-
let timer;
|
|
623
|
-
const timeout = new Promise((_resolve, reject) => {
|
|
624
|
-
timer = setTimeout(() => {
|
|
625
|
-
reject(
|
|
626
|
-
new PublishTimeoutError(
|
|
627
|
-
`Transport '${this.name}' got no broker ack for topic '${record.topic}' within ${this.#settings.publishTimeoutMs}ms`
|
|
628
|
-
)
|
|
629
|
-
);
|
|
630
|
-
}, this.#settings.publishTimeoutMs);
|
|
631
|
-
});
|
|
632
|
-
const sending = producer.send(record);
|
|
633
|
-
sending.catch(() => void 0);
|
|
634
|
-
try {
|
|
635
|
-
await Promise.race([sending, timeout]);
|
|
636
|
-
} finally {
|
|
637
|
-
clearTimeout(timer);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
#connectedProducer() {
|
|
641
|
-
if (this.#state === "closed") {
|
|
642
|
-
return Promise.reject(new RelayBusError(`Transport '${this.name}' is closed`));
|
|
643
|
-
}
|
|
644
|
-
this.#producer ??= this.#openProducer();
|
|
645
|
-
return this.#producer;
|
|
646
|
-
}
|
|
647
|
-
async #openProducer() {
|
|
648
|
-
const producer = this.#kafka.producer({
|
|
649
|
-
...this.#producerConfig,
|
|
650
|
-
idempotent: true,
|
|
651
|
-
maxInFlightRequests: this.#maxInFlightRequests
|
|
652
|
-
});
|
|
653
|
-
try {
|
|
654
|
-
await producer.connect();
|
|
655
|
-
} catch (error) {
|
|
656
|
-
this.#producer = void 0;
|
|
657
|
-
throw error;
|
|
658
|
-
}
|
|
659
|
-
this.#producerConnected = this.#state !== "closed";
|
|
660
|
-
return producer;
|
|
661
|
-
}
|
|
662
|
-
async #handleMessage(payload, onMessage) {
|
|
943
|
+
async #handleMessage(payload, route) {
|
|
663
944
|
let envelope;
|
|
664
945
|
try {
|
|
665
|
-
envelope = this.#
|
|
946
|
+
envelope = decodeRecord(this.#settings.codec, payload);
|
|
666
947
|
} catch (error) {
|
|
667
|
-
if (this.#
|
|
948
|
+
if (this.#settings.codec === cloudEventsBinary && !(error instanceof InvalidKafkaMessageError)) {
|
|
668
949
|
throw toHandlerFailure(error);
|
|
669
950
|
}
|
|
670
|
-
|
|
671
|
-
await this.#withRetries(payload,
|
|
951
|
+
route.hooks?.onInvalidMessage({ destination: payload.topic, error });
|
|
952
|
+
await this.#withRetries(payload, {
|
|
953
|
+
method: "handleInvalid",
|
|
954
|
+
operation: () => this.#handleInvalid(payload, error)
|
|
955
|
+
});
|
|
672
956
|
return;
|
|
673
957
|
}
|
|
674
|
-
await this.#withRetries(payload,
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
958
|
+
await this.#withRetries(payload, {
|
|
959
|
+
method: "deliver",
|
|
960
|
+
operation: async () => {
|
|
961
|
+
try {
|
|
962
|
+
await route.onMessage(envelope, { destination: payload.topic });
|
|
963
|
+
} catch (error) {
|
|
964
|
+
if (!(error instanceof NonRetryableError)) {
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
await this.#handleInvalid(payload, error);
|
|
680
968
|
}
|
|
681
|
-
await this.#handleInvalid(payload, error.message);
|
|
682
969
|
}
|
|
683
970
|
});
|
|
684
971
|
}
|
|
685
|
-
#
|
|
686
|
-
if (this.#decode === void 0) {
|
|
687
|
-
return decodeEnvelope(message);
|
|
688
|
-
}
|
|
689
|
-
const record = {
|
|
690
|
-
topic,
|
|
691
|
-
partition,
|
|
692
|
-
offset: message.offset,
|
|
693
|
-
key: message.key,
|
|
694
|
-
value: message.value,
|
|
695
|
-
headers: message.headers,
|
|
696
|
-
timestamp: message.timestamp
|
|
697
|
-
};
|
|
698
|
-
return completeDecodedEvent(this.#decode(record), record);
|
|
699
|
-
}
|
|
700
|
-
async #handleInvalid(payload, reason) {
|
|
972
|
+
async #handleInvalid(payload, error) {
|
|
701
973
|
const { topic, partition, message } = payload;
|
|
974
|
+
const { message: reason, name, code } = toErrorInfo(error);
|
|
702
975
|
const location = `topic=${topic} partition=${partition} offset=${message.offset}`;
|
|
703
|
-
const
|
|
704
|
-
const
|
|
976
|
+
const errorFields = `errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
|
|
977
|
+
const policy = this.#settings.invalidPolicy;
|
|
978
|
+
const deadLetterTopic = deadLetterTopicFor(policy, topic);
|
|
705
979
|
if (policy === "skip") {
|
|
706
|
-
this.#logger.error(
|
|
707
|
-
`[KafkaTransport](handleInvalid) Skipping malformed message | ${location} reason=${reason}`
|
|
980
|
+
this.#settings.logger.error(
|
|
981
|
+
`[KafkaTransport](handleInvalid) Skipping malformed message | ${location} reason=${reason} ${errorFields}`
|
|
708
982
|
);
|
|
709
983
|
} else if (typeof policy === "function") {
|
|
710
984
|
await policy({
|
|
@@ -716,26 +990,16 @@ var KafkaTransport = class {
|
|
|
716
990
|
value: message.value
|
|
717
991
|
});
|
|
718
992
|
} else {
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
{
|
|
725
|
-
key: message.key === null ? null : Buffer.from(message.key),
|
|
726
|
-
value: message.value === null ? null : Buffer.from(message.value),
|
|
727
|
-
headers: {
|
|
728
|
-
...toOutgoingHeaders(message.headers),
|
|
729
|
-
"relaybus-invalid-reason": reason,
|
|
730
|
-
"relaybus-source-topic": topic,
|
|
731
|
-
"relaybus-source-partition": String(partition),
|
|
732
|
-
"relaybus-source-offset": message.offset
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
]
|
|
993
|
+
const record = deadLetterRecord(payload, {
|
|
994
|
+
reason,
|
|
995
|
+
name,
|
|
996
|
+
code,
|
|
997
|
+
deadLetterTopic
|
|
736
998
|
});
|
|
737
|
-
this.#
|
|
738
|
-
|
|
999
|
+
const producer = await this.#producer.connected();
|
|
1000
|
+
await this.#producer.send(producer, record);
|
|
1001
|
+
this.#settings.logger.error(
|
|
1002
|
+
`[KafkaTransport](handleInvalid) Dead-lettered malformed message | ${location} deadLetterTopic=${deadLetterTopic} reason=${reason} ${errorFields}`
|
|
739
1003
|
);
|
|
740
1004
|
}
|
|
741
1005
|
}
|
|
@@ -744,8 +1008,9 @@ var KafkaTransport = class {
|
|
|
744
1008
|
* last error is rethrown so the client leaves the offset uncommitted and
|
|
745
1009
|
* redelivers the record; heartbeats between attempts keep the session alive.
|
|
746
1010
|
*/
|
|
747
|
-
async #withRetries(payload, method, operation) {
|
|
748
|
-
const { maxAttempts,
|
|
1011
|
+
async #withRetries(payload, { method, operation }) {
|
|
1012
|
+
const { maxAttempts, processingBackoff } = this.#settings.timings;
|
|
1013
|
+
const { logger } = this.#settings;
|
|
749
1014
|
const location = `topic=${payload.topic} partition=${payload.partition} offset=${payload.message.offset}`;
|
|
750
1015
|
const partitionKey = `${payload.topic}:${payload.partition}`;
|
|
751
1016
|
for (let attempt = 1; ; attempt += 1) {
|
|
@@ -757,51 +1022,28 @@ var KafkaTransport = class {
|
|
|
757
1022
|
const reason = toErrorInfo(error).message;
|
|
758
1023
|
if (attempt >= maxAttempts || this.#state === "closed") {
|
|
759
1024
|
this.#stalledPartitions.add(partitionKey);
|
|
760
|
-
|
|
761
|
-
`[KafkaTransport](
|
|
1025
|
+
logger.error(
|
|
1026
|
+
`[KafkaTransport](withRetries) Giving up for now, the record stays uncommitted and will be redelivered | operation=${method} ${location} attempts=${attempt} error=${reason}`
|
|
762
1027
|
);
|
|
763
1028
|
throw toHandlerFailure(error);
|
|
764
1029
|
}
|
|
765
|
-
|
|
766
|
-
`[KafkaTransport](
|
|
767
|
-
);
|
|
768
|
-
await this.#sleepWithHeartbeats(
|
|
769
|
-
Math.min(initialBackoffMs * 2 ** (attempt - 1), maxBackoffMs),
|
|
770
|
-
payload
|
|
1030
|
+
logger.warn(
|
|
1031
|
+
`[KafkaTransport](withRetries) Attempt failed, retrying | operation=${method} ${location} attempt=${attempt} error=${reason}`
|
|
771
1032
|
);
|
|
1033
|
+
await this.#sleeps.sleepInSlices(processingBackoff.delayFor(attempt), {
|
|
1034
|
+
sliceMs: this.#settings.timings.heartbeatIntervalMs,
|
|
1035
|
+
between: () => payload.heartbeat(),
|
|
1036
|
+
stopped: () => this.#state === "closed"
|
|
1037
|
+
});
|
|
772
1038
|
}
|
|
773
1039
|
}
|
|
774
1040
|
}
|
|
775
|
-
/** Sleeps in slices and heartbeats after each, so a long backoff never outlives the session timeout. */
|
|
776
|
-
async #sleepWithHeartbeats(milliseconds, payload) {
|
|
777
|
-
let remaining = milliseconds;
|
|
778
|
-
do {
|
|
779
|
-
const slice = Math.min(remaining, this.#settings.heartbeatIntervalMs);
|
|
780
|
-
await this.#sleep(slice);
|
|
781
|
-
await payload.heartbeat();
|
|
782
|
-
remaining -= slice;
|
|
783
|
-
} while (remaining > 0 && this.#state !== "closed");
|
|
784
|
-
}
|
|
785
|
-
#sleep(milliseconds) {
|
|
786
|
-
return new Promise((resolve) => {
|
|
787
|
-
const wake = () => {
|
|
788
|
-
clearTimeout(timer);
|
|
789
|
-
this.#sleepers.delete(wake);
|
|
790
|
-
resolve();
|
|
791
|
-
};
|
|
792
|
-
const timer = setTimeout(wake, milliseconds);
|
|
793
|
-
this.#sleepers.add(wake);
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
/** Whether the client exposes the instrumentation needed to follow the group membership. */
|
|
797
|
-
#tracksMembership(consumer) {
|
|
798
|
-
return consumer.on !== void 0 && consumer.events?.GROUP_JOIN !== void 0;
|
|
799
|
-
}
|
|
800
1041
|
#observeConsumer(consumer) {
|
|
801
|
-
if (!
|
|
1042
|
+
if (!tracksMembership(consumer)) {
|
|
802
1043
|
this.#groupJoined = true;
|
|
803
1044
|
return;
|
|
804
1045
|
}
|
|
1046
|
+
const { logger, groupId } = this.#settings;
|
|
805
1047
|
const names = consumer.events;
|
|
806
1048
|
const listen = (name, listener) => {
|
|
807
1049
|
if (name !== void 0) {
|
|
@@ -810,29 +1052,28 @@ var KafkaTransport = class {
|
|
|
810
1052
|
};
|
|
811
1053
|
listen(names.GROUP_JOIN, () => {
|
|
812
1054
|
this.#groupJoined = true;
|
|
813
|
-
this.#
|
|
814
|
-
|
|
1055
|
+
this.#groupJoin.joined();
|
|
1056
|
+
logger.log(`[KafkaTransport](observeConsumer) Joined group | group=${groupId}`);
|
|
815
1057
|
});
|
|
816
1058
|
listen(names.REBALANCING, () => {
|
|
817
1059
|
this.#groupJoined = false;
|
|
818
1060
|
this.#stalledPartitions.clear();
|
|
819
|
-
|
|
1061
|
+
logger.warn(`[KafkaTransport](observeConsumer) Rebalancing | group=${groupId}`);
|
|
820
1062
|
});
|
|
821
1063
|
listen(names.CRASH, (event) => this.#handleCrash(event));
|
|
822
|
-
|
|
1064
|
+
const leftGroup = () => {
|
|
823
1065
|
this.#groupJoined = false;
|
|
824
|
-
}
|
|
825
|
-
listen(names.
|
|
826
|
-
|
|
827
|
-
});
|
|
1066
|
+
};
|
|
1067
|
+
listen(names.STOP, leftGroup);
|
|
1068
|
+
listen(names.DISCONNECT, leftGroup);
|
|
828
1069
|
}
|
|
829
1070
|
#handleCrash(event) {
|
|
830
1071
|
const { error, restart } = event.payload ?? {};
|
|
831
1072
|
this.#groupJoined = false;
|
|
832
|
-
this.#logger.error(
|
|
833
|
-
`[KafkaTransport](handleCrash) Consumer crashed | group=${this.#groupId} restart=${String(restart)} error=${toErrorInfo(error).message}`
|
|
1073
|
+
this.#settings.logger.error(
|
|
1074
|
+
`[KafkaTransport](handleCrash) Consumer crashed | group=${this.#settings.groupId} restart=${String(restart)} error=${toErrorInfo(error).message}`
|
|
834
1075
|
);
|
|
835
|
-
this.#
|
|
1076
|
+
this.#groupJoin.failed(error);
|
|
836
1077
|
if (restart === false && this.#consuming && this.#restartLoop === void 0) {
|
|
837
1078
|
this.#restartLoop = this.#restartConsumer().finally(() => {
|
|
838
1079
|
this.#restartLoop = void 0;
|
|
@@ -844,128 +1085,29 @@ var KafkaTransport = class {
|
|
|
844
1085
|
* again, with bounded backoff, until it joined or the transport closed.
|
|
845
1086
|
*/
|
|
846
1087
|
async #restartConsumer() {
|
|
847
|
-
const {
|
|
1088
|
+
const { logger, groupId, timings } = this.#settings;
|
|
848
1089
|
const consumer = this.#consumer;
|
|
849
1090
|
for (let attempt = 1; this.#state === "subscribed"; attempt += 1) {
|
|
850
|
-
await this.#sleep(
|
|
851
|
-
Math.min(restartInitialBackoffMs * 2 ** (attempt - 1), restartMaxBackoffMs)
|
|
852
|
-
);
|
|
1091
|
+
await this.#sleeps.sleep(timings.restartBackoff.delayFor(attempt));
|
|
853
1092
|
if (this.#state !== "subscribed") {
|
|
854
1093
|
return;
|
|
855
1094
|
}
|
|
856
1095
|
await consumer.disconnect().catch(() => void 0);
|
|
857
1096
|
try {
|
|
858
1097
|
await this.#startConsuming();
|
|
859
|
-
|
|
860
|
-
`[KafkaTransport](restartConsumer) Consumer restarted | group=${
|
|
1098
|
+
logger.log(
|
|
1099
|
+
`[KafkaTransport](restartConsumer) Consumer restarted | group=${groupId} attempt=${attempt}`
|
|
861
1100
|
);
|
|
862
1101
|
return;
|
|
863
1102
|
} catch (error) {
|
|
864
|
-
|
|
865
|
-
`[KafkaTransport](restartConsumer) Restart failed, will retry | group=${
|
|
1103
|
+
logger.error(
|
|
1104
|
+
`[KafkaTransport](restartConsumer) Restart failed, will retry | group=${groupId} attempt=${attempt} error=${toErrorInfo(error).message}`
|
|
866
1105
|
);
|
|
867
1106
|
}
|
|
868
1107
|
}
|
|
869
1108
|
}
|
|
870
|
-
#waitForGroupJoin(consumer) {
|
|
871
|
-
if (!this.#tracksMembership(consumer)) {
|
|
872
|
-
return { promise: Promise.resolve(), dispose: () => void 0 };
|
|
873
|
-
}
|
|
874
|
-
let timer;
|
|
875
|
-
const promise = new Promise((resolve, reject) => {
|
|
876
|
-
this.#joinWaiter = { resolve, reject };
|
|
877
|
-
timer = setTimeout(() => {
|
|
878
|
-
reject(
|
|
879
|
-
new RelayBusError(
|
|
880
|
-
`Consumer of transport '${this.name}' did not join group '${this.#groupId}' within ${this.#settings.startupTimeoutMs}ms`
|
|
881
|
-
)
|
|
882
|
-
);
|
|
883
|
-
}, this.#settings.startupTimeoutMs);
|
|
884
|
-
});
|
|
885
|
-
promise.catch(() => void 0);
|
|
886
|
-
return {
|
|
887
|
-
promise,
|
|
888
|
-
dispose: () => {
|
|
889
|
-
clearTimeout(timer);
|
|
890
|
-
this.#joinWaiter = void 0;
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
1109
|
};
|
|
895
|
-
function resolveSettings(options) {
|
|
896
|
-
const maxAttempts = options.processing?.maxAttempts ?? DEFAULTS.maxAttempts;
|
|
897
|
-
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
|
|
898
|
-
throw new ConfigurationError(
|
|
899
|
-
`processing.maxAttempts must be a positive integer, received ${maxAttempts}`
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
return {
|
|
903
|
-
maxAttempts,
|
|
904
|
-
initialBackoffMs: toMilliseconds(
|
|
905
|
-
options.processing?.initialBackoff ?? DEFAULTS.initialBackoff,
|
|
906
|
-
"processing.initialBackoff"
|
|
907
|
-
),
|
|
908
|
-
maxBackoffMs: toMilliseconds(
|
|
909
|
-
options.processing?.maxBackoff ?? DEFAULTS.maxBackoff,
|
|
910
|
-
"processing.maxBackoff"
|
|
911
|
-
),
|
|
912
|
-
startupTimeoutMs: toMilliseconds(
|
|
913
|
-
options.startupTimeout ?? DEFAULTS.startupTimeout,
|
|
914
|
-
"startupTimeout"
|
|
915
|
-
),
|
|
916
|
-
publishTimeoutMs: toMilliseconds(
|
|
917
|
-
options.publishTimeout ?? DEFAULTS.publishTimeout,
|
|
918
|
-
"publishTimeout"
|
|
919
|
-
),
|
|
920
|
-
heartbeatIntervalMs: Math.max(
|
|
921
|
-
1,
|
|
922
|
-
toMilliseconds(
|
|
923
|
-
options.processing?.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
924
|
-
"processing.heartbeatInterval"
|
|
925
|
-
)
|
|
926
|
-
),
|
|
927
|
-
restartInitialBackoffMs: toMilliseconds(
|
|
928
|
-
options.restart?.initialBackoff ?? DEFAULTS.restartInitialBackoff,
|
|
929
|
-
"restart.initialBackoff"
|
|
930
|
-
),
|
|
931
|
-
restartMaxBackoffMs: toMilliseconds(
|
|
932
|
-
options.restart?.maxBackoff ?? DEFAULTS.restartMaxBackoff,
|
|
933
|
-
"restart.maxBackoff"
|
|
934
|
-
)
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
function toHandlerFailure(error) {
|
|
938
|
-
const { name, message } = toErrorInfo(error);
|
|
939
|
-
return new HandlerFailedError(`${name}: ${message}`);
|
|
940
|
-
}
|
|
941
|
-
function fromHeader(name) {
|
|
942
|
-
if (name.length === 0) {
|
|
943
|
-
throw new ConfigurationError("fromHeader needs a header name");
|
|
944
|
-
}
|
|
945
|
-
return (message) => Object.hasOwn(message.headers, name) ? message.headers[name] : void 0;
|
|
946
|
-
}
|
|
947
|
-
function fromPayloadField(path) {
|
|
948
|
-
const segments = path.split(".");
|
|
949
|
-
if (segments.some((segment) => segment.length === 0)) {
|
|
950
|
-
throw new ConfigurationError(
|
|
951
|
-
`fromPayloadField needs a dotted path of non-empty names, received '${path}'`
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
return (message) => {
|
|
955
|
-
let value = message.payload;
|
|
956
|
-
for (const segment of segments) {
|
|
957
|
-
if (typeof value !== "object" || value === null || !Object.hasOwn(value, segment)) {
|
|
958
|
-
return void 0;
|
|
959
|
-
}
|
|
960
|
-
value = value[segment];
|
|
961
|
-
}
|
|
962
|
-
if (typeof value === "string") {
|
|
963
|
-
return value;
|
|
964
|
-
}
|
|
965
|
-
return Number.isSafeInteger(value) ? String(value) : void 0;
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
1110
|
|
|
969
|
-
export { HandlerFailedError, InvalidKafkaMessageError, KafkaTransport, PublishTimeoutError, UnencodableEventError, decodeCloudEvent, decodeEnvelope, encodeEnvelope,
|
|
1111
|
+
export { GroupJoinTimeoutError, HandlerFailedError, InvalidKafkaMessageError, KafkaTransport, PublishTimeoutError, TransportClosedError, UnencodableEventError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, decodeEnvelope, encodeEnvelope, jsonEnvelope, plainJsonDecoder };
|
|
970
1112
|
//# sourceMappingURL=index.js.map
|
|
971
1113
|
//# sourceMappingURL=index.js.map
|