@relaybus/kafka 0.3.0 → 0.4.0
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 +91 -37
- package/dist/index.cjs +605 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -21
- package/dist/index.d.ts +118 -21
- package/dist/index.js +606 -437
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError,
|
|
2
|
-
import { createCloudEventReaders, mediaTypeOf, isJsonObject, createJsonEnvelopeFormat, schedule,
|
|
1
|
+
import { RelayBusError, NonRetryableError, assertEventType, ConfigurationError, toErrorInfo, systemClock } from '@relaybus/core';
|
|
2
|
+
import { createCloudEventReaders, DecodedEventReader, mediaTypeOf, isJsonObject, observeRejection, createJsonEnvelopeFormat, SubscribingTransport, writeStructuredCloudEvent, schedule, BoundedInteger, PositiveInteger, ExponentialBackoff, Milliseconds } from '@relaybus/core/internal';
|
|
3
3
|
export { fromHeader, fromPayloadField } from '@relaybus/core/internal';
|
|
4
4
|
|
|
5
5
|
// src/errors.ts
|
|
@@ -311,25 +311,17 @@ function plainJsonDecoder(options) {
|
|
|
311
311
|
if (record.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(headers[CONTENT_TYPE]) === STRUCTURED_MEDIA_TYPE2) {
|
|
312
312
|
return decodeCloudEvent(record);
|
|
313
313
|
}
|
|
314
|
-
const plain =
|
|
315
|
-
|
|
316
|
-
partition: record.partition,
|
|
317
|
-
offset: record.offset,
|
|
318
|
-
key: readKeyLeniently(record.key),
|
|
319
|
-
headers,
|
|
320
|
-
payload: parsePayload(record.value),
|
|
321
|
-
timestamp: record.timestamp
|
|
322
|
-
};
|
|
323
|
-
const type = resolveText(options.type, { record: plain, name: "type" });
|
|
314
|
+
const plain = plainRecordOf(record, headers);
|
|
315
|
+
const type = KAFKA_EVENTS.text(options.type, { plain, name: "type" });
|
|
324
316
|
if (type === void 0) {
|
|
325
317
|
throw invalid("Record has no event type: the type option found none");
|
|
326
318
|
}
|
|
327
319
|
assertValidType(type, "The event type");
|
|
328
320
|
return {
|
|
329
|
-
id:
|
|
321
|
+
id: KAFKA_EVENTS.text(options.id, { plain, name: "id" }) ?? `kafka:${record.topic}:${record.partition}:${record.offset}`,
|
|
330
322
|
type,
|
|
331
|
-
source:
|
|
332
|
-
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,
|
|
333
325
|
payload: plain.payload,
|
|
334
326
|
headers,
|
|
335
327
|
correlationId: null,
|
|
@@ -338,27 +330,38 @@ function plainJsonDecoder(options) {
|
|
|
338
330
|
};
|
|
339
331
|
};
|
|
340
332
|
}
|
|
341
|
-
function
|
|
342
|
-
if (typeof decoded !== "object" || decoded === null) {
|
|
343
|
-
throw invalid("The decoder must return an event object synchronously");
|
|
344
|
-
}
|
|
345
|
-
if ("then" in decoded) {
|
|
346
|
-
observeRejection(decoded);
|
|
347
|
-
throw invalid("The decoder must return an event object synchronously");
|
|
348
|
-
}
|
|
349
|
-
const event = decoded;
|
|
333
|
+
function plainRecordOf(record, headers) {
|
|
350
334
|
return {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
key:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
causationId: event.causationId ?? null,
|
|
359
|
-
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
|
|
360
342
|
};
|
|
361
343
|
}
|
|
344
|
+
var KafkaDecodedEvents = class extends DecodedEventReader {
|
|
345
|
+
invalid(reason) {
|
|
346
|
+
return invalid(reason);
|
|
347
|
+
}
|
|
348
|
+
/** The topic. */
|
|
349
|
+
defaultSource(record) {
|
|
350
|
+
return record.topic;
|
|
351
|
+
}
|
|
352
|
+
/** The record key as UTF-8 text. */
|
|
353
|
+
defaultKey(record) {
|
|
354
|
+
return readRecordKey(record.key);
|
|
355
|
+
}
|
|
356
|
+
/** The record timestamp; a record without a usable one is malformed. */
|
|
357
|
+
defaultTime(record) {
|
|
358
|
+
return requireRecordTimestamp(record);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
var KAFKA_EVENTS = new KafkaDecodedEvents();
|
|
362
|
+
function completeDecodedEvent(decoded, record) {
|
|
363
|
+
return KAFKA_EVENTS.complete(decoded, record);
|
|
364
|
+
}
|
|
362
365
|
function extensionAttributes(body) {
|
|
363
366
|
return Object.fromEntries(
|
|
364
367
|
Object.entries(body).filter(
|
|
@@ -404,17 +407,6 @@ function readKeyLeniently(key) {
|
|
|
404
407
|
return Buffer.from(key).toString("base64");
|
|
405
408
|
}
|
|
406
409
|
}
|
|
407
|
-
function resolveText(option, { record, name }) {
|
|
408
|
-
const value = typeof option === "function" ? option(record) : option;
|
|
409
|
-
if (value === void 0 || value === null) {
|
|
410
|
-
return void 0;
|
|
411
|
-
}
|
|
412
|
-
if (typeof value !== "string") {
|
|
413
|
-
observeRejection(value);
|
|
414
|
-
throw invalid(`The ${name} option returned ${typeof value}, expected a string`);
|
|
415
|
-
}
|
|
416
|
-
return value.length === 0 ? void 0 : value;
|
|
417
|
-
}
|
|
418
410
|
function resolveOccurredAt(option, { plain, record }) {
|
|
419
411
|
const value = option?.(plain);
|
|
420
412
|
if (value === void 0) {
|
|
@@ -426,11 +418,6 @@ function resolveOccurredAt(option, { plain, record }) {
|
|
|
426
418
|
}
|
|
427
419
|
return value;
|
|
428
420
|
}
|
|
429
|
-
function observeRejection(value) {
|
|
430
|
-
if (typeof value === "object" && value !== null && "then" in value) {
|
|
431
|
-
Promise.resolve(value).catch(() => void 0);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
421
|
|
|
435
422
|
// src/codecs.ts
|
|
436
423
|
var cloudEventsBinary = Object.freeze({
|
|
@@ -449,36 +436,53 @@ function jsonEnvelope(mapping) {
|
|
|
449
436
|
transportIds: [RECORD_POSITION],
|
|
450
437
|
failures: { invalid, unencodable: (reason) => new UnencodableEventError(reason) }
|
|
451
438
|
});
|
|
452
|
-
const contentType = mapping.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
439
|
+
const contentType = mapping.contentTypeHeader === false ? {} : { [CONTENT_TYPE]: mapping.contentType ?? DEFAULT_CONTENT_TYPE };
|
|
453
440
|
const stampsTime = mapping.occurredAt === void 0;
|
|
454
|
-
const ownHeaders = (envelope) => mapping.id === RECORD_POSITION ? {
|
|
441
|
+
const ownHeaders = (envelope) => mapping.id === RECORD_POSITION ? { ...contentType, [EVENT_ID_HEADER]: envelope.id } : contentType;
|
|
442
|
+
const recordKey = resolveRecordKey(mapping.recordKey);
|
|
455
443
|
return {
|
|
456
|
-
encode: (envelope) => {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
|
|
444
|
+
encode: (envelope) => encodeJson(envelope, { format, stampsTime, ownHeaders, recordKey }),
|
|
445
|
+
decode: (record) => decodeJson(record, format)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function resolveRecordKey(recordKey) {
|
|
449
|
+
if (recordKey === void 0 || recordKey === "event-key") {
|
|
450
|
+
return (envelope) => envelope.key;
|
|
451
|
+
}
|
|
452
|
+
if (recordKey === "none") {
|
|
453
|
+
return () => null;
|
|
454
|
+
}
|
|
455
|
+
if (typeof recordKey === "function") {
|
|
456
|
+
return recordKey;
|
|
457
|
+
}
|
|
458
|
+
throw new ConfigurationError(
|
|
459
|
+
`jsonEnvelope recordKey must be 'event-key', 'none' or a function, received '${String(recordKey)}'`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
function encodeJson(envelope, { format, stampsTime, ownHeaders, recordKey }) {
|
|
463
|
+
assertRepresentable(envelope);
|
|
464
|
+
const { body, headers } = format.write(envelope);
|
|
465
|
+
const taken = [CONTENT_TYPE, EVENT_ID_HEADER].find((name) => Object.hasOwn(headers, name));
|
|
466
|
+
if (taken !== void 0) {
|
|
467
|
+
throw new UnencodableEventError(
|
|
468
|
+
`Event ${envelope.id} has a header '${taken}', which the codec writes itself`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
key: recordKey(envelope),
|
|
473
|
+
value: serializeJson(envelope, body),
|
|
474
|
+
headers: { ...headers, ...ownHeaders(envelope) },
|
|
475
|
+
...stampsTime ? { timestamp: recordTimestamp(envelope) } : {}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function decodeJson(record, format) {
|
|
479
|
+
const { [EVENT_ID_HEADER]: eventId, ...headers } = withoutContentType(
|
|
480
|
+
readHeadersLeniently(record.headers)
|
|
481
|
+
);
|
|
482
|
+
const fields = format.read({ body: parsePayload(record.value), headers });
|
|
483
|
+
return {
|
|
484
|
+
...fields,
|
|
485
|
+
id: fields.id ?? (eventId === void 0 || eventId === "" ? `kafka:${record.topic}:${record.partition}:${record.offset}` : eventId)
|
|
482
486
|
};
|
|
483
487
|
}
|
|
484
488
|
function withoutContentType(headers) {
|
|
@@ -496,6 +500,222 @@ function recordTimestamp(envelope) {
|
|
|
496
500
|
formatTime(envelope);
|
|
497
501
|
return String(envelope.occurredAt.getTime());
|
|
498
502
|
}
|
|
503
|
+
var GroupJoinWatch = class {
|
|
504
|
+
#settings;
|
|
505
|
+
#waiter;
|
|
506
|
+
constructor(settings) {
|
|
507
|
+
this.#settings = settings;
|
|
508
|
+
}
|
|
509
|
+
/** The consumer joined its group. */
|
|
510
|
+
joined() {
|
|
511
|
+
this.#waiter?.resolve();
|
|
512
|
+
}
|
|
513
|
+
/** The consumer crashed while it was joining. */
|
|
514
|
+
failed(error) {
|
|
515
|
+
this.#waiter?.reject(error);
|
|
516
|
+
}
|
|
517
|
+
/** The transport closed while the consumer was joining. */
|
|
518
|
+
closed() {
|
|
519
|
+
this.#waiter?.reject(new TransportClosedError(`Transport '${this.#settings.name}' is closed`));
|
|
520
|
+
}
|
|
521
|
+
/** Connects and subscribes the consumer, runs it and resolves once it joined its group. */
|
|
522
|
+
async start(consumer, {
|
|
523
|
+
topics,
|
|
524
|
+
fromBeginning,
|
|
525
|
+
eachMessage
|
|
526
|
+
}) {
|
|
527
|
+
await consumer.connect();
|
|
528
|
+
await consumer.subscribe(this.#settings.dialect.subscription(topics, fromBeginning));
|
|
529
|
+
const joined = this.#wait(consumer);
|
|
530
|
+
try {
|
|
531
|
+
await consumer.run({ eachMessage });
|
|
532
|
+
await joined.promise;
|
|
533
|
+
} finally {
|
|
534
|
+
joined.dispose();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
#wait(consumer) {
|
|
538
|
+
if (!this.#settings.dialect.tracksMembership(consumer)) {
|
|
539
|
+
return { promise: Promise.resolve(), dispose: () => void 0 };
|
|
540
|
+
}
|
|
541
|
+
const { name, groupId, timings } = this.#settings;
|
|
542
|
+
let cancel;
|
|
543
|
+
const promise = new Promise((resolve, reject) => {
|
|
544
|
+
this.#waiter = { resolve, reject };
|
|
545
|
+
cancel = schedule(systemClock, {
|
|
546
|
+
run: () => {
|
|
547
|
+
reject(
|
|
548
|
+
new GroupJoinTimeoutError(
|
|
549
|
+
`Consumer of transport '${name}' did not join group '${groupId}' within ${timings.startupTimeoutMs}ms`
|
|
550
|
+
)
|
|
551
|
+
);
|
|
552
|
+
},
|
|
553
|
+
delayMs: timings.startupTimeoutMs
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
promise.catch(() => void 0);
|
|
557
|
+
return {
|
|
558
|
+
promise,
|
|
559
|
+
dispose: () => {
|
|
560
|
+
cancel?.();
|
|
561
|
+
this.#waiter = void 0;
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
var KafkaProducerSession = class {
|
|
567
|
+
#settings;
|
|
568
|
+
/** Whether the transport is closed: then nothing connects any more. */
|
|
569
|
+
#isClosed;
|
|
570
|
+
#producer;
|
|
571
|
+
#connected = false;
|
|
572
|
+
constructor(settings, isClosed) {
|
|
573
|
+
this.#settings = settings;
|
|
574
|
+
this.#isClosed = isClosed;
|
|
575
|
+
}
|
|
576
|
+
/** True once the producer connected, until the transport closes. */
|
|
577
|
+
get isConnected() {
|
|
578
|
+
return this.#connected;
|
|
579
|
+
}
|
|
580
|
+
/** The producer opened so far, if any: what closing must disconnect. */
|
|
581
|
+
get pending() {
|
|
582
|
+
return this.#producer;
|
|
583
|
+
}
|
|
584
|
+
markClosed() {
|
|
585
|
+
this.#connected = false;
|
|
586
|
+
}
|
|
587
|
+
connected() {
|
|
588
|
+
if (this.#isClosed()) {
|
|
589
|
+
return Promise.reject(
|
|
590
|
+
new TransportClosedError(`Transport '${this.#settings.name}' is closed`)
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
this.#producer ??= this.#open();
|
|
594
|
+
return this.#producer;
|
|
595
|
+
}
|
|
596
|
+
/** Sends with the publish timeout; the late outcome of a timed-out send is ignored. */
|
|
597
|
+
async send(producer, record) {
|
|
598
|
+
const { name, timings } = this.#settings;
|
|
599
|
+
let cancel;
|
|
600
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
601
|
+
cancel = schedule(systemClock, {
|
|
602
|
+
run: () => {
|
|
603
|
+
reject(
|
|
604
|
+
new PublishTimeoutError(
|
|
605
|
+
`Transport '${name}' got no broker ack for topic '${record.topic}' within ${timings.publishTimeoutMs}ms`
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
},
|
|
609
|
+
delayMs: timings.publishTimeoutMs
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
const sending = producer.send(this.#settings.dialect.record(record));
|
|
613
|
+
sending.catch(() => void 0);
|
|
614
|
+
try {
|
|
615
|
+
await Promise.race([sending, timeout]);
|
|
616
|
+
} finally {
|
|
617
|
+
cancel?.();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async #open() {
|
|
621
|
+
const { kafka, dialect, injectedProducer } = this.#settings;
|
|
622
|
+
if (injectedProducer !== void 0) {
|
|
623
|
+
this.#connected = true;
|
|
624
|
+
return withoutLifecycle(injectedProducer);
|
|
625
|
+
}
|
|
626
|
+
const producer = kafka.producer(dialect.producerConfig(this.#settings));
|
|
627
|
+
try {
|
|
628
|
+
await producer.connect();
|
|
629
|
+
} catch (error) {
|
|
630
|
+
this.#producer = void 0;
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
this.#connected = !this.#isClosed();
|
|
634
|
+
return producer;
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
function withoutLifecycle(producer) {
|
|
638
|
+
return {
|
|
639
|
+
send: (record) => producer.send(record),
|
|
640
|
+
disconnect: () => Promise.resolve()
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
var KAFKAJS_DIALECT = {
|
|
644
|
+
producerConfig: ({ producerConfig, maxInFlightRequests }) => ({
|
|
645
|
+
...producerConfig,
|
|
646
|
+
idempotent: true,
|
|
647
|
+
maxInFlightRequests
|
|
648
|
+
}),
|
|
649
|
+
consumerConfig: ({ consumerConfig, groupId }) => ({ ...consumerConfig, groupId }),
|
|
650
|
+
subscription: (topics, fromBeginning) => ({ topics, fromBeginning }),
|
|
651
|
+
record: (record) => record,
|
|
652
|
+
tracksMembership: (consumer) => consumer.on !== void 0 && consumer.events?.GROUP_JOIN !== void 0,
|
|
653
|
+
observe: (consumer, membership) => {
|
|
654
|
+
if (!KAFKAJS_DIALECT.tracksMembership(consumer)) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const names = consumer.events;
|
|
658
|
+
const listen = (name, listener) => {
|
|
659
|
+
if (name !== void 0) {
|
|
660
|
+
consumer.on(name, listener);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
listen(names.GROUP_JOIN, () => membership.joined());
|
|
664
|
+
listen(names.REBALANCING, () => membership.rebalancing());
|
|
665
|
+
listen(names.CRASH, (event) => membership.crashed(event));
|
|
666
|
+
listen(names.STOP, () => membership.left());
|
|
667
|
+
listen(names.DISCONNECT, () => membership.left());
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
var ASSIGN_PARTITIONS = -175;
|
|
671
|
+
var REVOKE_PARTITIONS = -174;
|
|
672
|
+
var CONFLUENT_DIALECT = {
|
|
673
|
+
producerConfig: ({ producerConfig, maxInFlightRequests }) => {
|
|
674
|
+
const user = producerConfig;
|
|
675
|
+
return {
|
|
676
|
+
...user,
|
|
677
|
+
kafkaJS: { ...user.kafkaJS, idempotent: true, maxInFlightRequests, acks: -1 },
|
|
678
|
+
"enable.idempotence": true,
|
|
679
|
+
"max.in.flight": maxInFlightRequests,
|
|
680
|
+
acks: -1
|
|
681
|
+
};
|
|
682
|
+
},
|
|
683
|
+
consumerConfig: ({ consumerConfig, groupId, fromBeginning }, membership) => {
|
|
684
|
+
const user = consumerConfig;
|
|
685
|
+
const userCallback = typeof user.rebalance_cb === "function" ? user.rebalance_cb : void 0;
|
|
686
|
+
return {
|
|
687
|
+
...user,
|
|
688
|
+
kafkaJS: { ...user.kafkaJS, groupId, fromBeginning },
|
|
689
|
+
"group.id": groupId,
|
|
690
|
+
"auto.offset.reset": fromBeginning ? "earliest" : "latest",
|
|
691
|
+
rebalance_cb: (...args) => {
|
|
692
|
+
if (args[0].code === ASSIGN_PARTITIONS) {
|
|
693
|
+
membership.joined();
|
|
694
|
+
} else if (args[0].code === REVOKE_PARTITIONS) {
|
|
695
|
+
membership.rebalancing();
|
|
696
|
+
}
|
|
697
|
+
return userCallback?.(...args);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
subscription: (topics) => ({ topics }),
|
|
702
|
+
record: ({ acks: _acks, ...record }) => record,
|
|
703
|
+
tracksMembership: () => true,
|
|
704
|
+
observe: () => void 0
|
|
705
|
+
};
|
|
706
|
+
function clientDialect(client) {
|
|
707
|
+
if (client === void 0 || client === "kafkajs") {
|
|
708
|
+
return KAFKAJS_DIALECT;
|
|
709
|
+
}
|
|
710
|
+
if (client === "confluent") {
|
|
711
|
+
return CONFLUENT_DIALECT;
|
|
712
|
+
}
|
|
713
|
+
throw new ConfigurationError(
|
|
714
|
+
`Unknown Kafka client '${String(client)}'; use 'kafkajs' or 'confluent'`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/kafka-settings.ts
|
|
499
719
|
var DEFAULTS = {
|
|
500
720
|
name: "kafka",
|
|
501
721
|
maxInFlightRequests: 1,
|
|
@@ -508,44 +728,248 @@ var DEFAULTS = {
|
|
|
508
728
|
restartInitialBackoff: "1s",
|
|
509
729
|
restartMaxBackoff: "30s"
|
|
510
730
|
};
|
|
511
|
-
var
|
|
731
|
+
var MAXIMUM_IN_FLIGHT_REQUESTS = 5;
|
|
732
|
+
function resolveKafkaSettings(options) {
|
|
733
|
+
return {
|
|
734
|
+
name: options.name ?? DEFAULTS.name,
|
|
735
|
+
kafka: options.kafka,
|
|
736
|
+
injectedProducer: resolveInjectedProducer(options),
|
|
737
|
+
dialect: clientDialect(options.client),
|
|
738
|
+
groupId: options.groupId,
|
|
739
|
+
fromBeginning: options.fromBeginning ?? true,
|
|
740
|
+
maxInFlightRequests: BoundedInteger.of(
|
|
741
|
+
options.maxInFlightRequests ?? DEFAULTS.maxInFlightRequests,
|
|
742
|
+
{ name: "maxInFlightRequests", minimum: 1, maximum: MAXIMUM_IN_FLIGHT_REQUESTS }
|
|
743
|
+
),
|
|
744
|
+
producerConfig: options.producerConfig ?? {},
|
|
745
|
+
consumerConfig: options.consumerConfig ?? {},
|
|
746
|
+
invalidPolicy: options.onInvalidMessage ?? "dead-letter",
|
|
747
|
+
codec: resolveCodec(options),
|
|
748
|
+
logger: options.logger ?? console,
|
|
749
|
+
timings: resolveTimings(options)
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function resolveInjectedProducer(options) {
|
|
753
|
+
const { producer, kafka, producerConfig, maxInFlightRequests } = options;
|
|
754
|
+
if (producer === void 0) {
|
|
755
|
+
if (kafka === void 0) {
|
|
756
|
+
throw new ConfigurationError(
|
|
757
|
+
"Give 'kafka', or an already-connected 'producer' to publish with"
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
return void 0;
|
|
761
|
+
}
|
|
762
|
+
if (producerConfig !== void 0 || maxInFlightRequests !== void 0) {
|
|
763
|
+
throw new ConfigurationError(
|
|
764
|
+
"Give 'producerConfig' and 'maxInFlightRequests' to a producer you inject yourself"
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return producer;
|
|
768
|
+
}
|
|
769
|
+
function resolveCodec({ codec, decode }) {
|
|
770
|
+
if (decode === void 0) {
|
|
771
|
+
return codec ?? cloudEventsBinary;
|
|
772
|
+
}
|
|
773
|
+
if (codec !== void 0) {
|
|
774
|
+
throw new ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
|
|
775
|
+
}
|
|
776
|
+
return { encode: encodeEnvelope, decode };
|
|
777
|
+
}
|
|
778
|
+
function resolveTimings(options) {
|
|
779
|
+
const processing = options.processing ?? {};
|
|
780
|
+
const maxAttempts = PositiveInteger.of(
|
|
781
|
+
processing.maxAttempts ?? DEFAULTS.maxAttempts,
|
|
782
|
+
"processing.maxAttempts"
|
|
783
|
+
);
|
|
784
|
+
return {
|
|
785
|
+
maxAttempts,
|
|
786
|
+
processingBackoff: new ExponentialBackoff({
|
|
787
|
+
initialMs: Milliseconds.of(
|
|
788
|
+
processing.initialBackoff ?? DEFAULTS.initialBackoff,
|
|
789
|
+
"processing.initialBackoff"
|
|
790
|
+
),
|
|
791
|
+
maxMs: Milliseconds.of(processing.maxBackoff ?? DEFAULTS.maxBackoff, "processing.maxBackoff")
|
|
792
|
+
}),
|
|
793
|
+
startupTimeoutMs: Milliseconds.of(
|
|
794
|
+
options.startupTimeout ?? DEFAULTS.startupTimeout,
|
|
795
|
+
"startupTimeout"
|
|
796
|
+
),
|
|
797
|
+
publishTimeoutMs: Milliseconds.of(
|
|
798
|
+
options.publishTimeout ?? DEFAULTS.publishTimeout,
|
|
799
|
+
"publishTimeout"
|
|
800
|
+
),
|
|
801
|
+
heartbeatIntervalMs: Math.max(
|
|
802
|
+
1,
|
|
803
|
+
Milliseconds.of(
|
|
804
|
+
processing.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
805
|
+
"processing.heartbeatInterval"
|
|
806
|
+
)
|
|
807
|
+
),
|
|
808
|
+
restartBackoff: resolveRestartBackoff(options.restart ?? {})
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function resolveRestartBackoff(restart) {
|
|
812
|
+
return new ExponentialBackoff({
|
|
813
|
+
initialMs: Milliseconds.of(
|
|
814
|
+
restart.initialBackoff ?? DEFAULTS.restartInitialBackoff,
|
|
815
|
+
"restart.initialBackoff"
|
|
816
|
+
),
|
|
817
|
+
maxMs: Milliseconds.of(restart.maxBackoff ?? DEFAULTS.restartMaxBackoff, "restart.maxBackoff")
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
function deadLetterTopicFor(policy, sourceTopic) {
|
|
821
|
+
if (policy === "dead-letter") {
|
|
822
|
+
return `${sourceTopic}.dead`;
|
|
823
|
+
}
|
|
824
|
+
if (typeof policy === "object") {
|
|
825
|
+
const { deadLetterTopic } = policy;
|
|
826
|
+
return typeof deadLetterTopic === "function" ? deadLetterTopic(sourceTopic) : deadLetterTopic;
|
|
827
|
+
}
|
|
828
|
+
return void 0;
|
|
829
|
+
}
|
|
830
|
+
function toHandlerFailure(error) {
|
|
831
|
+
const { name, message } = toErrorInfo(error);
|
|
832
|
+
return new HandlerFailedError(`${name}: ${message}`);
|
|
833
|
+
}
|
|
834
|
+
function assertCanSubscribe(state, {
|
|
835
|
+
settings,
|
|
836
|
+
destinations
|
|
837
|
+
}) {
|
|
838
|
+
const { name, groupId, invalidPolicy, kafka } = settings;
|
|
839
|
+
if (state === "closed") {
|
|
840
|
+
throw new TransportClosedError(`Transport '${name}' is closed`);
|
|
841
|
+
}
|
|
842
|
+
if (state === "subscribed") {
|
|
843
|
+
throw new ConfigurationError(
|
|
844
|
+
`Transport '${name}' already subscribed; list every topic in one subscription or use one KafkaTransport per consumer group`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
if (kafka === void 0) {
|
|
848
|
+
throw new ConfigurationError(`Transport '${name}' needs 'kafka' to create its consumer`);
|
|
849
|
+
}
|
|
850
|
+
if (groupId === void 0 || groupId.length === 0) {
|
|
851
|
+
throw new ConfigurationError(`Transport '${name}' needs a groupId to subscribe`);
|
|
852
|
+
}
|
|
853
|
+
if (destinations.length === 0) {
|
|
854
|
+
throw new ConfigurationError(`Transport '${name}' subscribed to no topics`);
|
|
855
|
+
}
|
|
856
|
+
for (const topic of destinations) {
|
|
857
|
+
const deadLetterTopic = deadLetterTopicFor(invalidPolicy, topic);
|
|
858
|
+
if (deadLetterTopic !== void 0 && destinations.includes(deadLetterTopic)) {
|
|
859
|
+
throw new ConfigurationError(
|
|
860
|
+
`Dead letter topic '${deadLetterTopic}' is also consumed, which would loop`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/kafka-records.ts
|
|
867
|
+
function decodeRecord(codec, { topic, partition, message }) {
|
|
868
|
+
return decodeKafkaRecord(codec, {
|
|
869
|
+
topic,
|
|
870
|
+
partition,
|
|
871
|
+
offset: message.offset,
|
|
872
|
+
key: message.key,
|
|
873
|
+
value: message.value,
|
|
874
|
+
headers: message.headers,
|
|
875
|
+
timestamp: message.timestamp
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
function decodeKafkaRecord(codec, record) {
|
|
879
|
+
if (codec === cloudEventsBinary) {
|
|
880
|
+
return decodeEnvelope(record);
|
|
881
|
+
}
|
|
882
|
+
return completeDecodedEvent(codec.decode(record), record);
|
|
883
|
+
}
|
|
884
|
+
function deadLetterRecord({ topic, partition, message }, {
|
|
885
|
+
reason,
|
|
886
|
+
name,
|
|
887
|
+
code,
|
|
888
|
+
deadLetterTopic
|
|
889
|
+
}) {
|
|
890
|
+
const headers = {
|
|
891
|
+
...toOutgoingHeaders(message.headers),
|
|
892
|
+
"relaybus-invalid-reason": reason,
|
|
893
|
+
"relaybus-source-topic": topic,
|
|
894
|
+
"relaybus-source-partition": String(partition),
|
|
895
|
+
"relaybus-source-offset": message.offset,
|
|
896
|
+
"relaybus-error-name": name
|
|
897
|
+
};
|
|
898
|
+
if (code === void 0) {
|
|
899
|
+
delete headers["relaybus-error-code"];
|
|
900
|
+
} else {
|
|
901
|
+
headers["relaybus-error-code"] = code;
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
topic: deadLetterTopic,
|
|
905
|
+
acks: -1,
|
|
906
|
+
messages: [
|
|
907
|
+
{
|
|
908
|
+
key: message.key === null ? null : Buffer.from(message.key),
|
|
909
|
+
value: message.value === null ? null : Buffer.from(message.value),
|
|
910
|
+
headers
|
|
911
|
+
}
|
|
912
|
+
]
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
var WakeableSleeps = class {
|
|
916
|
+
#sleepers = /* @__PURE__ */ new Set();
|
|
917
|
+
sleep(milliseconds) {
|
|
918
|
+
return new Promise((resolve) => {
|
|
919
|
+
const wake = () => {
|
|
920
|
+
cancel();
|
|
921
|
+
this.#sleepers.delete(wake);
|
|
922
|
+
resolve();
|
|
923
|
+
};
|
|
924
|
+
const cancel = schedule(systemClock, { run: wake, delayMs: milliseconds });
|
|
925
|
+
this.#sleepers.add(wake);
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
wakeAll() {
|
|
929
|
+
for (const wake of this.#sleepers) {
|
|
930
|
+
wake();
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Sleeps `milliseconds` in slices of at most `sliceMs`, running `between` after each, until done
|
|
935
|
+
* or `stopped`: a long backoff then never outlives the session timeout.
|
|
936
|
+
*/
|
|
937
|
+
async sleepInSlices(milliseconds, {
|
|
938
|
+
sliceMs,
|
|
939
|
+
between,
|
|
940
|
+
stopped
|
|
941
|
+
}) {
|
|
942
|
+
let remaining = milliseconds;
|
|
943
|
+
do {
|
|
944
|
+
const slice = Math.min(remaining, sliceMs);
|
|
945
|
+
await this.sleep(slice);
|
|
946
|
+
await between();
|
|
947
|
+
remaining -= slice;
|
|
948
|
+
} while (remaining > 0 && !stopped());
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/kafka-transport.ts
|
|
953
|
+
var KafkaTransport = class extends SubscribingTransport {
|
|
512
954
|
name;
|
|
513
955
|
system = "kafka";
|
|
514
|
-
#kafka;
|
|
515
|
-
#groupId;
|
|
516
|
-
#fromBeginning;
|
|
517
|
-
#maxInFlightRequests;
|
|
518
|
-
#producerConfig;
|
|
519
|
-
#consumerConfig;
|
|
520
|
-
#invalidPolicy;
|
|
521
|
-
#codec;
|
|
522
|
-
#logger;
|
|
523
956
|
#settings;
|
|
524
|
-
#sleepers = /* @__PURE__ */ new Set();
|
|
525
|
-
#state = "idle";
|
|
526
957
|
#producer;
|
|
958
|
+
#groupJoin;
|
|
959
|
+
#sleeps = new WakeableSleeps();
|
|
960
|
+
#state = "idle";
|
|
527
961
|
#consumer;
|
|
528
962
|
#groupJoined = false;
|
|
529
963
|
#consuming = false;
|
|
530
|
-
#producerConnected = false;
|
|
531
964
|
#restartLoop;
|
|
532
965
|
#stalledPartitions = /* @__PURE__ */ new Set();
|
|
533
966
|
#startConsuming;
|
|
534
|
-
#joinWaiter;
|
|
535
967
|
constructor(options) {
|
|
536
|
-
|
|
537
|
-
this.#
|
|
538
|
-
this
|
|
539
|
-
this.#
|
|
540
|
-
this.#
|
|
541
|
-
options.maxInFlightRequests ?? DEFAULTS.maxInFlightRequests
|
|
542
|
-
);
|
|
543
|
-
this.#producerConfig = options.producerConfig ?? {};
|
|
544
|
-
this.#consumerConfig = options.consumerConfig ?? {};
|
|
545
|
-
this.#invalidPolicy = options.onInvalidMessage ?? "dead-letter";
|
|
546
|
-
this.#codec = resolveCodec(options);
|
|
547
|
-
this.#logger = options.logger ?? console;
|
|
548
|
-
this.#settings = resolveSettings(options);
|
|
968
|
+
super();
|
|
969
|
+
this.#settings = resolveKafkaSettings(options);
|
|
970
|
+
this.name = this.#settings.name;
|
|
971
|
+
this.#producer = new KafkaProducerSession(this.#settings, () => this.#state === "closed");
|
|
972
|
+
this.#groupJoin = new GroupJoinWatch(this.#settings);
|
|
549
973
|
}
|
|
550
974
|
/**
|
|
551
975
|
* True while the consumer is joined to its group, not between a crash or
|
|
@@ -562,21 +986,21 @@ var KafkaTransport = class {
|
|
|
562
986
|
* dead consumer is being restarted.
|
|
563
987
|
*/
|
|
564
988
|
isReady() {
|
|
565
|
-
if (this.#state === "closed" || !this.#
|
|
989
|
+
if (this.#state === "closed" || !this.#producer.isConnected) {
|
|
566
990
|
return false;
|
|
567
991
|
}
|
|
568
992
|
return this.#state === "idle" || this.isConsuming();
|
|
569
993
|
}
|
|
570
994
|
/** Connects the producer. Called by the bus on start; `publish` also connects on demand. */
|
|
571
995
|
async connect() {
|
|
572
|
-
await this.#
|
|
996
|
+
await this.#producer.connected();
|
|
573
997
|
}
|
|
574
998
|
async publish(envelope, destination) {
|
|
575
|
-
const producer = await this.#
|
|
576
|
-
await this.#send(producer, {
|
|
999
|
+
const producer = await this.#producer.connected();
|
|
1000
|
+
await this.#producer.send(producer, {
|
|
577
1001
|
topic: destination,
|
|
578
1002
|
acks: -1,
|
|
579
|
-
messages: [this.#codec.encode(envelope)]
|
|
1003
|
+
messages: [this.#settings.codec.encode(envelope)]
|
|
580
1004
|
});
|
|
581
1005
|
}
|
|
582
1006
|
/**
|
|
@@ -586,14 +1010,23 @@ var KafkaTransport = class {
|
|
|
586
1010
|
* @throws ConfigurationError when `groupId` is missing, `destinations` is empty
|
|
587
1011
|
* or the transport already subscribed (create one transport per subscription).
|
|
588
1012
|
*/
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1013
|
+
openSubscription(subscription) {
|
|
1014
|
+
return this.#subscribe(subscription);
|
|
1015
|
+
}
|
|
1016
|
+
async #subscribe({ destinations, onMessage, hooks }) {
|
|
1017
|
+
assertCanSubscribe(this.#state, { settings: this.#settings, destinations });
|
|
592
1018
|
this.#state = "subscribed";
|
|
593
|
-
const
|
|
1019
|
+
const { kafka, dialect, groupId } = this.#settings;
|
|
1020
|
+
const membership = this.#observeConsumer();
|
|
1021
|
+
const consumer = kafka.consumer(dialect.consumerConfig(this.#settings, membership));
|
|
594
1022
|
this.#consumer = consumer;
|
|
595
|
-
this.#
|
|
596
|
-
|
|
1023
|
+
this.#groupJoined = !dialect.tracksMembership(consumer);
|
|
1024
|
+
dialect.observe(consumer, membership);
|
|
1025
|
+
const start = () => this.#groupJoin.start(consumer, {
|
|
1026
|
+
topics: [...destinations],
|
|
1027
|
+
fromBeginning: this.#settings.fromBeginning,
|
|
1028
|
+
eachMessage: (payload) => this.#handleMessage(payload, { onMessage, hooks })
|
|
1029
|
+
});
|
|
597
1030
|
this.#startConsuming = start;
|
|
598
1031
|
try {
|
|
599
1032
|
await start();
|
|
@@ -605,40 +1038,21 @@ var KafkaTransport = class {
|
|
|
605
1038
|
throw error;
|
|
606
1039
|
}
|
|
607
1040
|
this.#consuming = true;
|
|
608
|
-
this.#logger.log(
|
|
609
|
-
`[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${
|
|
1041
|
+
this.#settings.logger.log(
|
|
1042
|
+
`[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${groupId} topics=${destinations.join(",")}`
|
|
610
1043
|
);
|
|
611
1044
|
}
|
|
612
|
-
async #startConsumer(consumer, {
|
|
613
|
-
destinations,
|
|
614
|
-
onMessage,
|
|
615
|
-
hooks
|
|
616
|
-
}) {
|
|
617
|
-
await consumer.connect();
|
|
618
|
-
await consumer.subscribe({ topics: destinations, fromBeginning: this.#fromBeginning });
|
|
619
|
-
const joined = this.#waitForGroupJoin(consumer);
|
|
620
|
-
try {
|
|
621
|
-
await consumer.run({
|
|
622
|
-
eachMessage: (payload) => this.#handleMessage(payload, { onMessage, hooks })
|
|
623
|
-
});
|
|
624
|
-
await joined.promise;
|
|
625
|
-
} finally {
|
|
626
|
-
joined.dispose();
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
1045
|
/** Stops consuming and disconnects the consumer and the producer. Safe to call twice. */
|
|
630
1046
|
async close() {
|
|
631
1047
|
if (this.#state === "closed") {
|
|
632
1048
|
return;
|
|
633
1049
|
}
|
|
634
1050
|
this.#state = "closed";
|
|
635
|
-
const pendingProducer = this.#producer;
|
|
1051
|
+
const pendingProducer = this.#producer.pending;
|
|
636
1052
|
this.#groupJoined = false;
|
|
637
|
-
this.#
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
this.#joinWaiter?.reject(new TransportClosedError(`Transport '${this.name}' is closed`));
|
|
1053
|
+
this.#producer.markClosed();
|
|
1054
|
+
this.#sleeps.wakeAll();
|
|
1055
|
+
this.#groupJoin.closed();
|
|
642
1056
|
await this.#restartLoop;
|
|
643
1057
|
const producer = await pendingProducer?.catch(() => void 0);
|
|
644
1058
|
const results = await Promise.allSettled([
|
|
@@ -650,99 +1064,15 @@ var KafkaTransport = class {
|
|
|
650
1064
|
throw failure.reason;
|
|
651
1065
|
}
|
|
652
1066
|
}
|
|
653
|
-
#
|
|
654
|
-
if (this.#state === "closed") {
|
|
655
|
-
throw new TransportClosedError(`Transport '${this.name}' is closed`);
|
|
656
|
-
}
|
|
657
|
-
if (this.#state === "subscribed") {
|
|
658
|
-
throw new ConfigurationError(
|
|
659
|
-
`Transport '${this.name}' already subscribed; list every topic in one subscription or use one KafkaTransport per consumer group`
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
if (this.#groupId === void 0 || this.#groupId.length === 0) {
|
|
663
|
-
throw new ConfigurationError(`Transport '${this.name}' needs a groupId to subscribe`);
|
|
664
|
-
}
|
|
665
|
-
if (destinations.length === 0) {
|
|
666
|
-
throw new ConfigurationError(`Transport '${this.name}' subscribed to no topics`);
|
|
667
|
-
}
|
|
668
|
-
for (const topic of destinations) {
|
|
669
|
-
const deadLetterTopic = this.#deadLetterTopicFor(topic);
|
|
670
|
-
if (deadLetterTopic !== void 0 && destinations.includes(deadLetterTopic)) {
|
|
671
|
-
throw new ConfigurationError(
|
|
672
|
-
`Dead letter topic '${deadLetterTopic}' is also consumed, which would loop`
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
/** The dead-letter topic for records of `sourceTopic`, or undefined when the policy does not use one. */
|
|
678
|
-
#deadLetterTopicFor(sourceTopic) {
|
|
679
|
-
const policy = this.#invalidPolicy;
|
|
680
|
-
if (policy === "dead-letter") {
|
|
681
|
-
return `${sourceTopic}.dead`;
|
|
682
|
-
}
|
|
683
|
-
if (typeof policy === "object") {
|
|
684
|
-
const { deadLetterTopic } = policy;
|
|
685
|
-
return typeof deadLetterTopic === "function" ? deadLetterTopic(sourceTopic) : deadLetterTopic;
|
|
686
|
-
}
|
|
687
|
-
return void 0;
|
|
688
|
-
}
|
|
689
|
-
/** Sends with the publish timeout; the late outcome of a timed-out send is ignored. */
|
|
690
|
-
async #send(producer, record) {
|
|
691
|
-
let cancel;
|
|
692
|
-
const timeout = new Promise((_resolve, reject) => {
|
|
693
|
-
cancel = schedule(systemClock, {
|
|
694
|
-
run: () => {
|
|
695
|
-
reject(
|
|
696
|
-
new PublishTimeoutError(
|
|
697
|
-
`Transport '${this.name}' got no broker ack for topic '${record.topic}' within ${this.#settings.publishTimeoutMs}ms`
|
|
698
|
-
)
|
|
699
|
-
);
|
|
700
|
-
},
|
|
701
|
-
delayMs: this.#settings.publishTimeoutMs
|
|
702
|
-
});
|
|
703
|
-
});
|
|
704
|
-
const sending = producer.send(record);
|
|
705
|
-
sending.catch(() => void 0);
|
|
706
|
-
try {
|
|
707
|
-
await Promise.race([sending, timeout]);
|
|
708
|
-
} finally {
|
|
709
|
-
cancel?.();
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
#connectedProducer() {
|
|
713
|
-
if (this.#state === "closed") {
|
|
714
|
-
return Promise.reject(new TransportClosedError(`Transport '${this.name}' is closed`));
|
|
715
|
-
}
|
|
716
|
-
this.#producer ??= this.#openProducer();
|
|
717
|
-
return this.#producer;
|
|
718
|
-
}
|
|
719
|
-
async #openProducer() {
|
|
720
|
-
const producer = this.#kafka.producer({
|
|
721
|
-
...this.#producerConfig,
|
|
722
|
-
idempotent: true,
|
|
723
|
-
maxInFlightRequests: this.#maxInFlightRequests
|
|
724
|
-
});
|
|
725
|
-
try {
|
|
726
|
-
await producer.connect();
|
|
727
|
-
} catch (error) {
|
|
728
|
-
this.#producer = void 0;
|
|
729
|
-
throw error;
|
|
730
|
-
}
|
|
731
|
-
this.#producerConnected = this.#state !== "closed";
|
|
732
|
-
return producer;
|
|
733
|
-
}
|
|
734
|
-
async #handleMessage(payload, {
|
|
735
|
-
onMessage,
|
|
736
|
-
hooks
|
|
737
|
-
}) {
|
|
1067
|
+
async #handleMessage(payload, route) {
|
|
738
1068
|
let envelope;
|
|
739
1069
|
try {
|
|
740
|
-
envelope = this.#
|
|
1070
|
+
envelope = decodeRecord(this.#settings.codec, payload);
|
|
741
1071
|
} catch (error) {
|
|
742
|
-
if (this.#codec === cloudEventsBinary && !(error instanceof InvalidKafkaMessageError)) {
|
|
1072
|
+
if (this.#settings.codec === cloudEventsBinary && !(error instanceof InvalidKafkaMessageError)) {
|
|
743
1073
|
throw toHandlerFailure(error);
|
|
744
1074
|
}
|
|
745
|
-
hooks?.onInvalidMessage({ destination: payload.topic, error });
|
|
1075
|
+
route.hooks?.onInvalidMessage({ destination: payload.topic, error });
|
|
746
1076
|
await this.#withRetries(payload, {
|
|
747
1077
|
method: "handleInvalid",
|
|
748
1078
|
operation: () => this.#handleInvalid(payload, error)
|
|
@@ -753,7 +1083,7 @@ var KafkaTransport = class {
|
|
|
753
1083
|
method: "deliver",
|
|
754
1084
|
operation: async () => {
|
|
755
1085
|
try {
|
|
756
|
-
await onMessage(envelope, { destination: payload.topic });
|
|
1086
|
+
await route.onMessage(envelope, { destination: payload.topic });
|
|
757
1087
|
} catch (error) {
|
|
758
1088
|
if (!(error instanceof NonRetryableError)) {
|
|
759
1089
|
throw error;
|
|
@@ -763,30 +1093,15 @@ var KafkaTransport = class {
|
|
|
763
1093
|
}
|
|
764
1094
|
});
|
|
765
1095
|
}
|
|
766
|
-
#decodeRecord({ topic, partition, message }) {
|
|
767
|
-
if (this.#codec === cloudEventsBinary) {
|
|
768
|
-
return decodeEnvelope(message);
|
|
769
|
-
}
|
|
770
|
-
const record = {
|
|
771
|
-
topic,
|
|
772
|
-
partition,
|
|
773
|
-
offset: message.offset,
|
|
774
|
-
key: message.key,
|
|
775
|
-
value: message.value,
|
|
776
|
-
headers: message.headers,
|
|
777
|
-
timestamp: message.timestamp
|
|
778
|
-
};
|
|
779
|
-
return completeDecodedEvent(this.#codec.decode(record), record);
|
|
780
|
-
}
|
|
781
1096
|
async #handleInvalid(payload, error) {
|
|
782
1097
|
const { topic, partition, message } = payload;
|
|
783
1098
|
const { message: reason, name, code } = toErrorInfo(error);
|
|
784
1099
|
const location = `topic=${topic} partition=${partition} offset=${message.offset}`;
|
|
785
1100
|
const errorFields = `errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
|
|
786
|
-
const policy = this.#invalidPolicy;
|
|
787
|
-
const deadLetterTopic =
|
|
1101
|
+
const policy = this.#settings.invalidPolicy;
|
|
1102
|
+
const deadLetterTopic = deadLetterTopicFor(policy, topic);
|
|
788
1103
|
if (policy === "skip") {
|
|
789
|
-
this.#logger.error(
|
|
1104
|
+
this.#settings.logger.error(
|
|
790
1105
|
`[KafkaTransport](handleInvalid) Skipping malformed message | ${location} reason=${reason} ${errorFields}`
|
|
791
1106
|
);
|
|
792
1107
|
} else if (typeof policy === "function") {
|
|
@@ -799,32 +1114,15 @@ var KafkaTransport = class {
|
|
|
799
1114
|
value: message.value
|
|
800
1115
|
});
|
|
801
1116
|
} else {
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
"relaybus-source-offset": message.offset,
|
|
808
|
-
"relaybus-error-name": name
|
|
809
|
-
};
|
|
810
|
-
if (code === void 0) {
|
|
811
|
-
delete headers["relaybus-error-code"];
|
|
812
|
-
} else {
|
|
813
|
-
headers["relaybus-error-code"] = code;
|
|
814
|
-
}
|
|
815
|
-
const producer = await this.#connectedProducer();
|
|
816
|
-
await this.#send(producer, {
|
|
817
|
-
topic: deadLetterTopic,
|
|
818
|
-
acks: -1,
|
|
819
|
-
messages: [
|
|
820
|
-
{
|
|
821
|
-
key: message.key === null ? null : Buffer.from(message.key),
|
|
822
|
-
value: message.value === null ? null : Buffer.from(message.value),
|
|
823
|
-
headers
|
|
824
|
-
}
|
|
825
|
-
]
|
|
1117
|
+
const record = deadLetterRecord(payload, {
|
|
1118
|
+
reason,
|
|
1119
|
+
name,
|
|
1120
|
+
code,
|
|
1121
|
+
deadLetterTopic
|
|
826
1122
|
});
|
|
827
|
-
this.#
|
|
1123
|
+
const producer = await this.#producer.connected();
|
|
1124
|
+
await this.#producer.send(producer, record);
|
|
1125
|
+
this.#settings.logger.error(
|
|
828
1126
|
`[KafkaTransport](handleInvalid) Dead-lettered malformed message | ${location} deadLetterTopic=${deadLetterTopic} reason=${reason} ${errorFields}`
|
|
829
1127
|
);
|
|
830
1128
|
}
|
|
@@ -835,7 +1133,8 @@ var KafkaTransport = class {
|
|
|
835
1133
|
* redelivers the record; heartbeats between attempts keep the session alive.
|
|
836
1134
|
*/
|
|
837
1135
|
async #withRetries(payload, { method, operation }) {
|
|
838
|
-
const { maxAttempts,
|
|
1136
|
+
const { maxAttempts, processingBackoff } = this.#settings.timings;
|
|
1137
|
+
const { logger } = this.#settings;
|
|
839
1138
|
const location = `topic=${payload.topic} partition=${payload.partition} offset=${payload.message.offset}`;
|
|
840
1139
|
const partitionKey = `${payload.topic}:${payload.partition}`;
|
|
841
1140
|
for (let attempt = 1; ; attempt += 1) {
|
|
@@ -847,82 +1146,49 @@ var KafkaTransport = class {
|
|
|
847
1146
|
const reason = toErrorInfo(error).message;
|
|
848
1147
|
if (attempt >= maxAttempts || this.#state === "closed") {
|
|
849
1148
|
this.#stalledPartitions.add(partitionKey);
|
|
850
|
-
|
|
1149
|
+
logger.error(
|
|
851
1150
|
`[KafkaTransport](withRetries) Giving up for now, the record stays uncommitted and will be redelivered | operation=${method} ${location} attempts=${attempt} error=${reason}`
|
|
852
1151
|
);
|
|
853
1152
|
throw toHandlerFailure(error);
|
|
854
1153
|
}
|
|
855
|
-
|
|
1154
|
+
logger.warn(
|
|
856
1155
|
`[KafkaTransport](withRetries) Attempt failed, retrying | operation=${method} ${location} attempt=${attempt} error=${reason}`
|
|
857
1156
|
);
|
|
858
|
-
await this.#
|
|
859
|
-
|
|
860
|
-
payload
|
|
861
|
-
|
|
1157
|
+
await this.#sleeps.sleepInSlices(processingBackoff.delayFor(attempt), {
|
|
1158
|
+
sliceMs: this.#settings.timings.heartbeatIntervalMs,
|
|
1159
|
+
between: () => payload.heartbeat(),
|
|
1160
|
+
stopped: () => this.#state === "closed"
|
|
1161
|
+
});
|
|
862
1162
|
}
|
|
863
1163
|
}
|
|
864
1164
|
}
|
|
865
|
-
/**
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const cancel = schedule(systemClock, { run: wake, delayMs: milliseconds });
|
|
883
|
-
this.#sleepers.add(wake);
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
/** Whether the client exposes the instrumentation needed to follow the group membership. */
|
|
887
|
-
#tracksMembership(consumer) {
|
|
888
|
-
return consumer.on !== void 0 && consumer.events?.GROUP_JOIN !== void 0;
|
|
889
|
-
}
|
|
890
|
-
#observeConsumer(consumer) {
|
|
891
|
-
if (!this.#tracksMembership(consumer)) {
|
|
892
|
-
this.#groupJoined = true;
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
const names = consumer.events;
|
|
896
|
-
const listen = (name, listener) => {
|
|
897
|
-
if (name !== void 0) {
|
|
898
|
-
consumer.on(name, listener);
|
|
1165
|
+
/** What the transport does when the consumer joins, rebalances, crashes or leaves its group. */
|
|
1166
|
+
#observeConsumer() {
|
|
1167
|
+
const { logger, groupId } = this.#settings;
|
|
1168
|
+
return {
|
|
1169
|
+
joined: () => {
|
|
1170
|
+
this.#groupJoined = true;
|
|
1171
|
+
this.#groupJoin.joined();
|
|
1172
|
+
logger.log(`[KafkaTransport](observeConsumer) Joined group | group=${groupId}`);
|
|
1173
|
+
},
|
|
1174
|
+
rebalancing: () => {
|
|
1175
|
+
this.#groupJoined = false;
|
|
1176
|
+
this.#stalledPartitions.clear();
|
|
1177
|
+
logger.warn(`[KafkaTransport](observeConsumer) Rebalancing | group=${groupId}`);
|
|
1178
|
+
},
|
|
1179
|
+
crashed: (event) => this.#handleCrash(event),
|
|
1180
|
+
left: () => {
|
|
1181
|
+
this.#groupJoined = false;
|
|
899
1182
|
}
|
|
900
1183
|
};
|
|
901
|
-
listen(names.GROUP_JOIN, () => {
|
|
902
|
-
this.#groupJoined = true;
|
|
903
|
-
this.#joinWaiter?.resolve();
|
|
904
|
-
this.#logger.log(`[KafkaTransport](observeConsumer) Joined group | group=${this.#groupId}`);
|
|
905
|
-
});
|
|
906
|
-
listen(names.REBALANCING, () => {
|
|
907
|
-
this.#groupJoined = false;
|
|
908
|
-
this.#stalledPartitions.clear();
|
|
909
|
-
this.#logger.warn(`[KafkaTransport](observeConsumer) Rebalancing | group=${this.#groupId}`);
|
|
910
|
-
});
|
|
911
|
-
listen(names.CRASH, (event) => this.#handleCrash(event));
|
|
912
|
-
listen(names.STOP, () => {
|
|
913
|
-
this.#groupJoined = false;
|
|
914
|
-
});
|
|
915
|
-
listen(names.DISCONNECT, () => {
|
|
916
|
-
this.#groupJoined = false;
|
|
917
|
-
});
|
|
918
1184
|
}
|
|
919
1185
|
#handleCrash(event) {
|
|
920
1186
|
const { error, restart } = event.payload ?? {};
|
|
921
1187
|
this.#groupJoined = false;
|
|
922
|
-
this.#logger.error(
|
|
923
|
-
`[KafkaTransport](handleCrash) Consumer crashed | group=${this.#groupId} restart=${String(restart)} error=${toErrorInfo(error).message}`
|
|
1188
|
+
this.#settings.logger.error(
|
|
1189
|
+
`[KafkaTransport](handleCrash) Consumer crashed | group=${this.#settings.groupId} restart=${String(restart)} error=${toErrorInfo(error).message}`
|
|
924
1190
|
);
|
|
925
|
-
this.#
|
|
1191
|
+
this.#groupJoin.failed(error);
|
|
926
1192
|
if (restart === false && this.#consuming && this.#restartLoop === void 0) {
|
|
927
1193
|
this.#restartLoop = this.#restartConsumer().finally(() => {
|
|
928
1194
|
this.#restartLoop = void 0;
|
|
@@ -934,126 +1200,29 @@ var KafkaTransport = class {
|
|
|
934
1200
|
* again, with bounded backoff, until it joined or the transport closed.
|
|
935
1201
|
*/
|
|
936
1202
|
async #restartConsumer() {
|
|
937
|
-
const {
|
|
1203
|
+
const { logger, groupId, timings } = this.#settings;
|
|
938
1204
|
const consumer = this.#consumer;
|
|
939
1205
|
for (let attempt = 1; this.#state === "subscribed"; attempt += 1) {
|
|
940
|
-
await this.#sleep(
|
|
941
|
-
Math.min(restartInitialBackoffMs * 2 ** (attempt - 1), restartMaxBackoffMs)
|
|
942
|
-
);
|
|
1206
|
+
await this.#sleeps.sleep(timings.restartBackoff.delayFor(attempt));
|
|
943
1207
|
if (this.#state !== "subscribed") {
|
|
944
1208
|
return;
|
|
945
1209
|
}
|
|
946
1210
|
await consumer.disconnect().catch(() => void 0);
|
|
947
1211
|
try {
|
|
948
1212
|
await this.#startConsuming();
|
|
949
|
-
|
|
950
|
-
`[KafkaTransport](restartConsumer) Consumer restarted | group=${
|
|
1213
|
+
logger.log(
|
|
1214
|
+
`[KafkaTransport](restartConsumer) Consumer restarted | group=${groupId} attempt=${attempt}`
|
|
951
1215
|
);
|
|
952
1216
|
return;
|
|
953
1217
|
} catch (error) {
|
|
954
|
-
|
|
955
|
-
`[KafkaTransport](restartConsumer) Restart failed, will retry | group=${
|
|
1218
|
+
logger.error(
|
|
1219
|
+
`[KafkaTransport](restartConsumer) Restart failed, will retry | group=${groupId} attempt=${attempt} error=${toErrorInfo(error).message}`
|
|
956
1220
|
);
|
|
957
1221
|
}
|
|
958
1222
|
}
|
|
959
1223
|
}
|
|
960
|
-
#waitForGroupJoin(consumer) {
|
|
961
|
-
if (!this.#tracksMembership(consumer)) {
|
|
962
|
-
return { promise: Promise.resolve(), dispose: () => void 0 };
|
|
963
|
-
}
|
|
964
|
-
let cancel;
|
|
965
|
-
const promise = new Promise((resolve, reject) => {
|
|
966
|
-
this.#joinWaiter = { resolve, reject };
|
|
967
|
-
cancel = schedule(systemClock, {
|
|
968
|
-
run: () => {
|
|
969
|
-
reject(
|
|
970
|
-
new GroupJoinTimeoutError(
|
|
971
|
-
`Consumer of transport '${this.name}' did not join group '${this.#groupId}' within ${this.#settings.startupTimeoutMs}ms`
|
|
972
|
-
)
|
|
973
|
-
);
|
|
974
|
-
},
|
|
975
|
-
delayMs: this.#settings.startupTimeoutMs
|
|
976
|
-
});
|
|
977
|
-
});
|
|
978
|
-
promise.catch(() => void 0);
|
|
979
|
-
return {
|
|
980
|
-
promise,
|
|
981
|
-
dispose: () => {
|
|
982
|
-
cancel?.();
|
|
983
|
-
this.#joinWaiter = void 0;
|
|
984
|
-
}
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
1224
|
};
|
|
988
|
-
function resolveCodec({ codec, decode }) {
|
|
989
|
-
if (decode === void 0) {
|
|
990
|
-
return codec ?? cloudEventsBinary;
|
|
991
|
-
}
|
|
992
|
-
if (codec !== void 0) {
|
|
993
|
-
throw new ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
|
|
994
|
-
}
|
|
995
|
-
return { encode: encodeEnvelope, decode };
|
|
996
|
-
}
|
|
997
|
-
function resolveSettings(options) {
|
|
998
|
-
const processing = options.processing ?? {};
|
|
999
|
-
const maxAttempts = assertPositiveInteger(
|
|
1000
|
-
processing.maxAttempts ?? DEFAULTS.maxAttempts,
|
|
1001
|
-
"processing.maxAttempts"
|
|
1002
|
-
);
|
|
1003
|
-
return {
|
|
1004
|
-
maxAttempts,
|
|
1005
|
-
initialBackoffMs: toMilliseconds(
|
|
1006
|
-
processing.initialBackoff ?? DEFAULTS.initialBackoff,
|
|
1007
|
-
"processing.initialBackoff"
|
|
1008
|
-
),
|
|
1009
|
-
maxBackoffMs: toMilliseconds(
|
|
1010
|
-
processing.maxBackoff ?? DEFAULTS.maxBackoff,
|
|
1011
|
-
"processing.maxBackoff"
|
|
1012
|
-
),
|
|
1013
|
-
startupTimeoutMs: toMilliseconds(
|
|
1014
|
-
options.startupTimeout ?? DEFAULTS.startupTimeout,
|
|
1015
|
-
"startupTimeout"
|
|
1016
|
-
),
|
|
1017
|
-
publishTimeoutMs: toMilliseconds(
|
|
1018
|
-
options.publishTimeout ?? DEFAULTS.publishTimeout,
|
|
1019
|
-
"publishTimeout"
|
|
1020
|
-
),
|
|
1021
|
-
heartbeatIntervalMs: Math.max(
|
|
1022
|
-
1,
|
|
1023
|
-
toMilliseconds(
|
|
1024
|
-
processing.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
1025
|
-
"processing.heartbeatInterval"
|
|
1026
|
-
)
|
|
1027
|
-
),
|
|
1028
|
-
...resolveRestartSettings(options.restart ?? {})
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
function resolveRestartSettings(restart) {
|
|
1032
|
-
return {
|
|
1033
|
-
restartInitialBackoffMs: toMilliseconds(
|
|
1034
|
-
restart.initialBackoff ?? DEFAULTS.restartInitialBackoff,
|
|
1035
|
-
"restart.initialBackoff"
|
|
1036
|
-
),
|
|
1037
|
-
restartMaxBackoffMs: toMilliseconds(
|
|
1038
|
-
restart.maxBackoff ?? DEFAULTS.restartMaxBackoff,
|
|
1039
|
-
"restart.maxBackoff"
|
|
1040
|
-
)
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
var MAXIMUM_IN_FLIGHT_REQUESTS = 5;
|
|
1044
|
-
function assertMaxInFlightRequests(value) {
|
|
1045
|
-
if (!Number.isInteger(value) || value < 1 || value > MAXIMUM_IN_FLIGHT_REQUESTS) {
|
|
1046
|
-
throw new ConfigurationError(
|
|
1047
|
-
`maxInFlightRequests must be an integer from 1 to 5, received ${value}`
|
|
1048
|
-
);
|
|
1049
|
-
}
|
|
1050
|
-
return value;
|
|
1051
|
-
}
|
|
1052
|
-
function toHandlerFailure(error) {
|
|
1053
|
-
const { name, message } = toErrorInfo(error);
|
|
1054
|
-
return new HandlerFailedError(`${name}: ${message}`);
|
|
1055
|
-
}
|
|
1056
1225
|
|
|
1057
|
-
export { GroupJoinTimeoutError, HandlerFailedError, InvalidKafkaMessageError, KafkaTransport, PublishTimeoutError, TransportClosedError, UnencodableEventError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, decodeEnvelope, encodeEnvelope, jsonEnvelope, plainJsonDecoder };
|
|
1226
|
+
export { GroupJoinTimeoutError, HandlerFailedError, InvalidKafkaMessageError, KafkaTransport, PublishTimeoutError, TransportClosedError, UnencodableEventError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, decodeEnvelope, decodeKafkaRecord, encodeEnvelope, jsonEnvelope, plainJsonDecoder };
|
|
1058
1227
|
//# sourceMappingURL=index.js.map
|
|
1059
1228
|
//# sourceMappingURL=index.js.map
|