@relaybus/kafka 0.2.0 → 0.3.1

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