@relaybus/kafka 0.3.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/dist/index.cjs +469 -415
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +470 -416
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError, assertEventType,
|
|
2
|
-
import { createCloudEventReaders, mediaTypeOf, isJsonObject, createJsonEnvelopeFormat, schedule,
|
|
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
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({
|
|
@@ -453,32 +440,34 @@ function jsonEnvelope(mapping) {
|
|
|
453
440
|
const stampsTime = mapping.occurredAt === void 0;
|
|
454
441
|
const ownHeaders = (envelope) => mapping.id === RECORD_POSITION ? { [CONTENT_TYPE]: contentType, [EVENT_ID_HEADER]: envelope.id } : { [CONTENT_TYPE]: contentType };
|
|
455
442
|
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
|
-
|
|
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
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
key: envelope.key,
|
|
458
|
+
value: serializeJson(envelope, body),
|
|
459
|
+
headers: { ...headers, ...ownHeaders(envelope) },
|
|
460
|
+
...stampsTime ? { timestamp: recordTimestamp(envelope) } : {}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
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)
|
|
482
471
|
};
|
|
483
472
|
}
|
|
484
473
|
function withoutContentType(headers) {
|
|
@@ -496,6 +485,139 @@ function recordTimestamp(envelope) {
|
|
|
496
485
|
formatTime(envelope);
|
|
497
486
|
return String(envelope.occurredAt.getTime());
|
|
498
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
|
+
}
|
|
553
|
+
};
|
|
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
|
+
}
|
|
620
|
+
};
|
|
499
621
|
var DEFAULTS = {
|
|
500
622
|
name: "kafka",
|
|
501
623
|
maxInFlightRequests: 1,
|
|
@@ -508,44 +630,224 @@ var DEFAULTS = {
|
|
|
508
630
|
restartInitialBackoff: "1s",
|
|
509
631
|
restartMaxBackoff: "30s"
|
|
510
632
|
};
|
|
511
|
-
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 {
|
|
512
832
|
name;
|
|
513
833
|
system = "kafka";
|
|
514
|
-
#kafka;
|
|
515
|
-
#groupId;
|
|
516
|
-
#fromBeginning;
|
|
517
|
-
#maxInFlightRequests;
|
|
518
|
-
#producerConfig;
|
|
519
|
-
#consumerConfig;
|
|
520
|
-
#invalidPolicy;
|
|
521
|
-
#codec;
|
|
522
|
-
#logger;
|
|
523
834
|
#settings;
|
|
524
|
-
#sleepers = /* @__PURE__ */ new Set();
|
|
525
|
-
#state = "idle";
|
|
526
835
|
#producer;
|
|
836
|
+
#groupJoin;
|
|
837
|
+
#sleeps = new WakeableSleeps();
|
|
838
|
+
#state = "idle";
|
|
527
839
|
#consumer;
|
|
528
840
|
#groupJoined = false;
|
|
529
841
|
#consuming = false;
|
|
530
|
-
#producerConnected = false;
|
|
531
842
|
#restartLoop;
|
|
532
843
|
#stalledPartitions = /* @__PURE__ */ new Set();
|
|
533
844
|
#startConsuming;
|
|
534
|
-
#joinWaiter;
|
|
535
845
|
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);
|
|
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);
|
|
549
851
|
}
|
|
550
852
|
/**
|
|
551
853
|
* True while the consumer is joined to its group, not between a crash or
|
|
@@ -562,21 +864,21 @@ var KafkaTransport = class {
|
|
|
562
864
|
* dead consumer is being restarted.
|
|
563
865
|
*/
|
|
564
866
|
isReady() {
|
|
565
|
-
if (this.#state === "closed" || !this.#
|
|
867
|
+
if (this.#state === "closed" || !this.#producer.isConnected) {
|
|
566
868
|
return false;
|
|
567
869
|
}
|
|
568
870
|
return this.#state === "idle" || this.isConsuming();
|
|
569
871
|
}
|
|
570
872
|
/** Connects the producer. Called by the bus on start; `publish` also connects on demand. */
|
|
571
873
|
async connect() {
|
|
572
|
-
await this.#
|
|
874
|
+
await this.#producer.connected();
|
|
573
875
|
}
|
|
574
876
|
async publish(envelope, destination) {
|
|
575
|
-
const producer = await this.#
|
|
576
|
-
await this.#send(producer, {
|
|
877
|
+
const producer = await this.#producer.connected();
|
|
878
|
+
await this.#producer.send(producer, {
|
|
577
879
|
topic: destination,
|
|
578
880
|
acks: -1,
|
|
579
|
-
messages: [this.#codec.encode(envelope)]
|
|
881
|
+
messages: [this.#settings.codec.encode(envelope)]
|
|
580
882
|
});
|
|
581
883
|
}
|
|
582
884
|
/**
|
|
@@ -586,14 +888,21 @@ var KafkaTransport = class {
|
|
|
586
888
|
* @throws ConfigurationError when `groupId` is missing, `destinations` is empty
|
|
587
889
|
* or the transport already subscribed (create one transport per subscription).
|
|
588
890
|
*/
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
891
|
+
openSubscription(subscription) {
|
|
892
|
+
return this.#subscribe(subscription);
|
|
893
|
+
}
|
|
894
|
+
async #subscribe({ destinations, onMessage, hooks }) {
|
|
895
|
+
assertCanSubscribe(this.#state, { settings: this.#settings, destinations });
|
|
592
896
|
this.#state = "subscribed";
|
|
593
|
-
const
|
|
897
|
+
const { kafka, consumerConfig, groupId } = this.#settings;
|
|
898
|
+
const consumer = kafka.consumer({ ...consumerConfig, groupId });
|
|
594
899
|
this.#consumer = consumer;
|
|
595
900
|
this.#observeConsumer(consumer);
|
|
596
|
-
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
|
+
});
|
|
597
906
|
this.#startConsuming = start;
|
|
598
907
|
try {
|
|
599
908
|
await start();
|
|
@@ -605,40 +914,21 @@ var KafkaTransport = class {
|
|
|
605
914
|
throw error;
|
|
606
915
|
}
|
|
607
916
|
this.#consuming = true;
|
|
608
|
-
this.#logger.log(
|
|
609
|
-
`[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${
|
|
917
|
+
this.#settings.logger.log(
|
|
918
|
+
`[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${groupId} topics=${destinations.join(",")}`
|
|
610
919
|
);
|
|
611
920
|
}
|
|
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
921
|
/** Stops consuming and disconnects the consumer and the producer. Safe to call twice. */
|
|
630
922
|
async close() {
|
|
631
923
|
if (this.#state === "closed") {
|
|
632
924
|
return;
|
|
633
925
|
}
|
|
634
926
|
this.#state = "closed";
|
|
635
|
-
const pendingProducer = this.#producer;
|
|
927
|
+
const pendingProducer = this.#producer.pending;
|
|
636
928
|
this.#groupJoined = false;
|
|
637
|
-
this.#
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
this.#joinWaiter?.reject(new TransportClosedError(`Transport '${this.name}' is closed`));
|
|
929
|
+
this.#producer.markClosed();
|
|
930
|
+
this.#sleeps.wakeAll();
|
|
931
|
+
this.#groupJoin.closed();
|
|
642
932
|
await this.#restartLoop;
|
|
643
933
|
const producer = await pendingProducer?.catch(() => void 0);
|
|
644
934
|
const results = await Promise.allSettled([
|
|
@@ -650,99 +940,15 @@ var KafkaTransport = class {
|
|
|
650
940
|
throw failure.reason;
|
|
651
941
|
}
|
|
652
942
|
}
|
|
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
|
-
}) {
|
|
943
|
+
async #handleMessage(payload, route) {
|
|
738
944
|
let envelope;
|
|
739
945
|
try {
|
|
740
|
-
envelope = this.#
|
|
946
|
+
envelope = decodeRecord(this.#settings.codec, payload);
|
|
741
947
|
} catch (error) {
|
|
742
|
-
if (this.#codec === cloudEventsBinary && !(error instanceof InvalidKafkaMessageError)) {
|
|
948
|
+
if (this.#settings.codec === cloudEventsBinary && !(error instanceof InvalidKafkaMessageError)) {
|
|
743
949
|
throw toHandlerFailure(error);
|
|
744
950
|
}
|
|
745
|
-
hooks?.onInvalidMessage({ destination: payload.topic, error });
|
|
951
|
+
route.hooks?.onInvalidMessage({ destination: payload.topic, error });
|
|
746
952
|
await this.#withRetries(payload, {
|
|
747
953
|
method: "handleInvalid",
|
|
748
954
|
operation: () => this.#handleInvalid(payload, error)
|
|
@@ -753,7 +959,7 @@ var KafkaTransport = class {
|
|
|
753
959
|
method: "deliver",
|
|
754
960
|
operation: async () => {
|
|
755
961
|
try {
|
|
756
|
-
await onMessage(envelope, { destination: payload.topic });
|
|
962
|
+
await route.onMessage(envelope, { destination: payload.topic });
|
|
757
963
|
} catch (error) {
|
|
758
964
|
if (!(error instanceof NonRetryableError)) {
|
|
759
965
|
throw error;
|
|
@@ -763,30 +969,15 @@ var KafkaTransport = class {
|
|
|
763
969
|
}
|
|
764
970
|
});
|
|
765
971
|
}
|
|
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
972
|
async #handleInvalid(payload, error) {
|
|
782
973
|
const { topic, partition, message } = payload;
|
|
783
974
|
const { message: reason, name, code } = toErrorInfo(error);
|
|
784
975
|
const location = `topic=${topic} partition=${partition} offset=${message.offset}`;
|
|
785
976
|
const errorFields = `errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
|
|
786
|
-
const policy = this.#invalidPolicy;
|
|
787
|
-
const deadLetterTopic =
|
|
977
|
+
const policy = this.#settings.invalidPolicy;
|
|
978
|
+
const deadLetterTopic = deadLetterTopicFor(policy, topic);
|
|
788
979
|
if (policy === "skip") {
|
|
789
|
-
this.#logger.error(
|
|
980
|
+
this.#settings.logger.error(
|
|
790
981
|
`[KafkaTransport](handleInvalid) Skipping malformed message | ${location} reason=${reason} ${errorFields}`
|
|
791
982
|
);
|
|
792
983
|
} else if (typeof policy === "function") {
|
|
@@ -799,32 +990,15 @@ var KafkaTransport = class {
|
|
|
799
990
|
value: message.value
|
|
800
991
|
});
|
|
801
992
|
} 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
|
-
]
|
|
993
|
+
const record = deadLetterRecord(payload, {
|
|
994
|
+
reason,
|
|
995
|
+
name,
|
|
996
|
+
code,
|
|
997
|
+
deadLetterTopic
|
|
826
998
|
});
|
|
827
|
-
this.#
|
|
999
|
+
const producer = await this.#producer.connected();
|
|
1000
|
+
await this.#producer.send(producer, record);
|
|
1001
|
+
this.#settings.logger.error(
|
|
828
1002
|
`[KafkaTransport](handleInvalid) Dead-lettered malformed message | ${location} deadLetterTopic=${deadLetterTopic} reason=${reason} ${errorFields}`
|
|
829
1003
|
);
|
|
830
1004
|
}
|
|
@@ -835,7 +1009,8 @@ var KafkaTransport = class {
|
|
|
835
1009
|
* redelivers the record; heartbeats between attempts keep the session alive.
|
|
836
1010
|
*/
|
|
837
1011
|
async #withRetries(payload, { method, operation }) {
|
|
838
|
-
const { maxAttempts,
|
|
1012
|
+
const { maxAttempts, processingBackoff } = this.#settings.timings;
|
|
1013
|
+
const { logger } = this.#settings;
|
|
839
1014
|
const location = `topic=${payload.topic} partition=${payload.partition} offset=${payload.message.offset}`;
|
|
840
1015
|
const partitionKey = `${payload.topic}:${payload.partition}`;
|
|
841
1016
|
for (let attempt = 1; ; attempt += 1) {
|
|
@@ -847,51 +1022,28 @@ var KafkaTransport = class {
|
|
|
847
1022
|
const reason = toErrorInfo(error).message;
|
|
848
1023
|
if (attempt >= maxAttempts || this.#state === "closed") {
|
|
849
1024
|
this.#stalledPartitions.add(partitionKey);
|
|
850
|
-
|
|
1025
|
+
logger.error(
|
|
851
1026
|
`[KafkaTransport](withRetries) Giving up for now, the record stays uncommitted and will be redelivered | operation=${method} ${location} attempts=${attempt} error=${reason}`
|
|
852
1027
|
);
|
|
853
1028
|
throw toHandlerFailure(error);
|
|
854
1029
|
}
|
|
855
|
-
|
|
1030
|
+
logger.warn(
|
|
856
1031
|
`[KafkaTransport](withRetries) Attempt failed, retrying | operation=${method} ${location} attempt=${attempt} error=${reason}`
|
|
857
1032
|
);
|
|
858
|
-
await this.#
|
|
859
|
-
|
|
860
|
-
payload
|
|
861
|
-
|
|
1033
|
+
await this.#sleeps.sleepInSlices(processingBackoff.delayFor(attempt), {
|
|
1034
|
+
sliceMs: this.#settings.timings.heartbeatIntervalMs,
|
|
1035
|
+
between: () => payload.heartbeat(),
|
|
1036
|
+
stopped: () => this.#state === "closed"
|
|
1037
|
+
});
|
|
862
1038
|
}
|
|
863
1039
|
}
|
|
864
1040
|
}
|
|
865
|
-
/** Sleeps in slices and heartbeats after each, so a long backoff never outlives the session timeout. */
|
|
866
|
-
async #sleepWithHeartbeats(milliseconds, payload) {
|
|
867
|
-
let remaining = milliseconds;
|
|
868
|
-
do {
|
|
869
|
-
const slice = Math.min(remaining, this.#settings.heartbeatIntervalMs);
|
|
870
|
-
await this.#sleep(slice);
|
|
871
|
-
await payload.heartbeat();
|
|
872
|
-
remaining -= slice;
|
|
873
|
-
} while (remaining > 0 && this.#state !== "closed");
|
|
874
|
-
}
|
|
875
|
-
#sleep(milliseconds) {
|
|
876
|
-
return new Promise((resolve) => {
|
|
877
|
-
const wake = () => {
|
|
878
|
-
cancel();
|
|
879
|
-
this.#sleepers.delete(wake);
|
|
880
|
-
resolve();
|
|
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
1041
|
#observeConsumer(consumer) {
|
|
891
|
-
if (!
|
|
1042
|
+
if (!tracksMembership(consumer)) {
|
|
892
1043
|
this.#groupJoined = true;
|
|
893
1044
|
return;
|
|
894
1045
|
}
|
|
1046
|
+
const { logger, groupId } = this.#settings;
|
|
895
1047
|
const names = consumer.events;
|
|
896
1048
|
const listen = (name, listener) => {
|
|
897
1049
|
if (name !== void 0) {
|
|
@@ -900,29 +1052,28 @@ var KafkaTransport = class {
|
|
|
900
1052
|
};
|
|
901
1053
|
listen(names.GROUP_JOIN, () => {
|
|
902
1054
|
this.#groupJoined = true;
|
|
903
|
-
this.#
|
|
904
|
-
|
|
1055
|
+
this.#groupJoin.joined();
|
|
1056
|
+
logger.log(`[KafkaTransport](observeConsumer) Joined group | group=${groupId}`);
|
|
905
1057
|
});
|
|
906
1058
|
listen(names.REBALANCING, () => {
|
|
907
1059
|
this.#groupJoined = false;
|
|
908
1060
|
this.#stalledPartitions.clear();
|
|
909
|
-
|
|
1061
|
+
logger.warn(`[KafkaTransport](observeConsumer) Rebalancing | group=${groupId}`);
|
|
910
1062
|
});
|
|
911
1063
|
listen(names.CRASH, (event) => this.#handleCrash(event));
|
|
912
|
-
|
|
913
|
-
this.#groupJoined = false;
|
|
914
|
-
});
|
|
915
|
-
listen(names.DISCONNECT, () => {
|
|
1064
|
+
const leftGroup = () => {
|
|
916
1065
|
this.#groupJoined = false;
|
|
917
|
-
}
|
|
1066
|
+
};
|
|
1067
|
+
listen(names.STOP, leftGroup);
|
|
1068
|
+
listen(names.DISCONNECT, leftGroup);
|
|
918
1069
|
}
|
|
919
1070
|
#handleCrash(event) {
|
|
920
1071
|
const { error, restart } = event.payload ?? {};
|
|
921
1072
|
this.#groupJoined = false;
|
|
922
|
-
this.#logger.error(
|
|
923
|
-
`[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}`
|
|
924
1075
|
);
|
|
925
|
-
this.#
|
|
1076
|
+
this.#groupJoin.failed(error);
|
|
926
1077
|
if (restart === false && this.#consuming && this.#restartLoop === void 0) {
|
|
927
1078
|
this.#restartLoop = this.#restartConsumer().finally(() => {
|
|
928
1079
|
this.#restartLoop = void 0;
|
|
@@ -934,125 +1085,28 @@ var KafkaTransport = class {
|
|
|
934
1085
|
* again, with bounded backoff, until it joined or the transport closed.
|
|
935
1086
|
*/
|
|
936
1087
|
async #restartConsumer() {
|
|
937
|
-
const {
|
|
1088
|
+
const { logger, groupId, timings } = this.#settings;
|
|
938
1089
|
const consumer = this.#consumer;
|
|
939
1090
|
for (let attempt = 1; this.#state === "subscribed"; attempt += 1) {
|
|
940
|
-
await this.#sleep(
|
|
941
|
-
Math.min(restartInitialBackoffMs * 2 ** (attempt - 1), restartMaxBackoffMs)
|
|
942
|
-
);
|
|
1091
|
+
await this.#sleeps.sleep(timings.restartBackoff.delayFor(attempt));
|
|
943
1092
|
if (this.#state !== "subscribed") {
|
|
944
1093
|
return;
|
|
945
1094
|
}
|
|
946
1095
|
await consumer.disconnect().catch(() => void 0);
|
|
947
1096
|
try {
|
|
948
1097
|
await this.#startConsuming();
|
|
949
|
-
|
|
950
|
-
`[KafkaTransport](restartConsumer) Consumer restarted | group=${
|
|
1098
|
+
logger.log(
|
|
1099
|
+
`[KafkaTransport](restartConsumer) Consumer restarted | group=${groupId} attempt=${attempt}`
|
|
951
1100
|
);
|
|
952
1101
|
return;
|
|
953
1102
|
} catch (error) {
|
|
954
|
-
|
|
955
|
-
`[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}`
|
|
956
1105
|
);
|
|
957
1106
|
}
|
|
958
1107
|
}
|
|
959
1108
|
}
|
|
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
1109
|
};
|
|
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
1110
|
|
|
1057
1111
|
export { GroupJoinTimeoutError, HandlerFailedError, InvalidKafkaMessageError, KafkaTransport, PublishTimeoutError, TransportClosedError, UnencodableEventError, cloudEventsBinary, cloudEventsStructured, decodeCloudEvent, decodeEnvelope, encodeEnvelope, jsonEnvelope, plainJsonDecoder };
|
|
1058
1112
|
//# sourceMappingURL=index.js.map
|