@relaybus/kafka 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,21 +184,24 @@ 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 milliseconds = Number(recordTimestamp);
153
- const time = new Date(milliseconds);
154
- if (recordTimestamp === void 0 || recordTimestamp === "" || Number.isNaN(time.getTime())) {
191
+ const time = readRecordTimestamp(recordTimestamp2);
192
+ if (time === void 0) {
155
193
  throw invalid(`${HEADER.time} is missing and the record has no usable timestamp`);
156
194
  }
157
195
  return time;
158
196
  }
159
- function parseTime(text) {
197
+ function readRecordTimestamp(recordTimestamp2) {
198
+ const time = new Date(Number(recordTimestamp2));
199
+ return recordTimestamp2 === void 0 || recordTimestamp2 === "" || Number.isNaN(time.getTime()) ? void 0 : time;
200
+ }
201
+ function parseTime(text, label = HEADER.time) {
160
202
  const time = new Date(text);
161
203
  if (!RFC3339_PATTERN.test(text) || Number.isNaN(time.getTime())) {
162
- throw invalid(`${HEADER.time} is not an RFC 3339 timestamp: '${text}'`);
204
+ throw invalid(`${label} is not an RFC 3339 timestamp: '${text}'`);
163
205
  }
164
206
  return time;
165
207
  }
@@ -167,7 +209,7 @@ function parsePayload(value) {
167
209
  if (value === null) {
168
210
  throw invalid("Message has no value");
169
211
  }
170
- const text = decodeText(value, true, "Message value");
212
+ const text = decodeText(value, { strict: true, label: "Message value" });
171
213
  try {
172
214
  return JSON.parse(text);
173
215
  } catch {
@@ -175,12 +217,15 @@ function parsePayload(value) {
175
217
  }
176
218
  }
177
219
  function resolveKey(subject, recordKey) {
178
- const keyText = recordKey === null ? null : decodeText(recordKey, true, "Message key");
220
+ const keyText = readRecordKey(recordKey);
179
221
  if (subject !== void 0 && keyText !== null && subject !== keyText) {
180
222
  throw invalid(`${HEADER.subject} does not match the message key`);
181
223
  }
182
224
  return subject ?? keyText;
183
225
  }
226
+ function readRecordKey(recordKey) {
227
+ return recordKey === null ? null : decodeText(recordKey, { strict: true, label: "Message key" });
228
+ }
184
229
  function collectUserHeaders(headers) {
185
230
  const entries = [];
186
231
  for (const [name, value] of headers) {
@@ -208,12 +253,250 @@ function toOutgoingHeaders(incoming) {
208
253
  function toBufferOrString(value) {
209
254
  return typeof value === "string" ? value : Buffer.from(value);
210
255
  }
211
- var PublishTimeoutError = class extends core.RelayBusError {
212
- name = "PublishTimeoutError";
213
- };
214
- var HandlerFailedError = class extends core.RelayBusError {
215
- name = "HandlerFailedError";
216
- };
256
+ var { optionalAttribute, requiredAttribute, assertValidType, assertJsonDataContentType } = internal.createCloudEventReaders(invalid);
257
+ var STRUCTURED_MEDIA_TYPE2 = "application/cloudevents+json";
258
+ var exactUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
259
+ var SPEC_VERSION2 = "1.0";
260
+ var SPEC_VERSION_HEADER = "ce_specversion";
261
+ var STRUCTURED_ATTRIBUTES = /* @__PURE__ */ new Set([
262
+ "specversion",
263
+ "id",
264
+ "type",
265
+ "source",
266
+ "time",
267
+ "subject",
268
+ "datacontenttype",
269
+ "dataschema",
270
+ "data",
271
+ "data_base64",
272
+ "correlationid",
273
+ "causationid"
274
+ ]);
275
+ function decodeCloudEvent(record) {
276
+ const headers = readHeaders(record.headers);
277
+ if (internal.mediaTypeOf(headers.get(CONTENT_TYPE)) !== STRUCTURED_MEDIA_TYPE2) {
278
+ return decodeEnvelope(record);
279
+ }
280
+ const body = parsePayload(record.value);
281
+ if (!internal.isJsonObject(body)) {
282
+ throw invalid("A structured CloudEvent must be a JSON object");
283
+ }
284
+ if (body["specversion"] !== SPEC_VERSION2) {
285
+ throw invalid(`specversion must be '${SPEC_VERSION2}'`);
286
+ }
287
+ const type = requiredAttribute(body, "type");
288
+ assertValidType(type, "type");
289
+ assertJsonDataContentType(optionalAttribute(body, "datacontenttype"));
290
+ if (Object.hasOwn(body, "data_base64")) {
291
+ throw invalid("data_base64 is not supported: relaybus payloads are JSON, send them as data");
292
+ }
293
+ const time = optionalAttribute(body, "time");
294
+ return {
295
+ id: requiredAttribute(body, "id"),
296
+ type,
297
+ source: requiredAttribute(body, "source"),
298
+ key: optionalAttribute(body, "subject") ?? readRecordKey(record.key),
299
+ payload: Object.hasOwn(body, "data") ? body["data"] : null,
300
+ headers: { ...collectUserHeaders(headers), ...extensionAttributes(body) },
301
+ correlationId: optionalAttribute(body, "correlationid") ?? null,
302
+ causationId: optionalAttribute(body, "causationid") ?? null,
303
+ occurredAt: time === void 0 ? requireRecordTimestamp(record) : parseTime(time, "time")
304
+ };
305
+ }
306
+ function plainJsonDecoder(options) {
307
+ if (typeof options.type === "string") {
308
+ core.assertEventType(options.type);
309
+ }
310
+ return (record) => {
311
+ const headers = readHeadersLeniently(record.headers);
312
+ if (record.headers?.[SPEC_VERSION_HEADER] !== void 0 || internal.mediaTypeOf(headers[CONTENT_TYPE]) === STRUCTURED_MEDIA_TYPE2) {
313
+ return decodeCloudEvent(record);
314
+ }
315
+ const plain = {
316
+ topic: record.topic,
317
+ partition: record.partition,
318
+ offset: record.offset,
319
+ key: readKeyLeniently(record.key),
320
+ headers,
321
+ payload: parsePayload(record.value),
322
+ timestamp: record.timestamp
323
+ };
324
+ const type = resolveText(options.type, { record: plain, name: "type" });
325
+ if (type === void 0) {
326
+ throw invalid("Record has no event type: the type option found none");
327
+ }
328
+ assertValidType(type, "The event type");
329
+ return {
330
+ id: resolveText(options.id, { record: plain, name: "id" }) ?? `kafka:${record.topic}:${record.partition}:${record.offset}`,
331
+ type,
332
+ source: resolveText(options.source, { record: plain, name: "source" }) ?? record.topic,
333
+ key: options.key === void 0 ? plain.key : resolveText(options.key, { record: plain, name: "key" }) ?? null,
334
+ payload: plain.payload,
335
+ headers,
336
+ correlationId: null,
337
+ causationId: null,
338
+ occurredAt: resolveOccurredAt(options.occurredAt, { plain, record })
339
+ };
340
+ };
341
+ }
342
+ function completeDecodedEvent(decoded, record) {
343
+ if (typeof decoded !== "object" || decoded === null) {
344
+ throw invalid("The decoder must return an event object synchronously");
345
+ }
346
+ if ("then" in decoded) {
347
+ observeRejection(decoded);
348
+ throw invalid("The decoder must return an event object synchronously");
349
+ }
350
+ const event = decoded;
351
+ return {
352
+ id: event.id,
353
+ type: event.type,
354
+ source: event.source ?? record.topic,
355
+ key: event.key === void 0 ? readRecordKey(record.key) : event.key,
356
+ payload: event.payload,
357
+ headers: event.headers ?? {},
358
+ correlationId: event.correlationId ?? null,
359
+ causationId: event.causationId ?? null,
360
+ occurredAt: event.occurredAt ?? requireRecordTimestamp(record)
361
+ };
362
+ }
363
+ function extensionAttributes(body) {
364
+ return Object.fromEntries(
365
+ Object.entries(body).filter(
366
+ (entry) => !STRUCTURED_ATTRIBUTES.has(entry[0]) && typeof entry[1] === "string"
367
+ )
368
+ );
369
+ }
370
+ function requireRecordTimestamp(record) {
371
+ const time = readRecordTimestamp(record.timestamp);
372
+ if (time === void 0) {
373
+ throw invalid("The event has no time and the record has no usable timestamp");
374
+ }
375
+ return time;
376
+ }
377
+ function readHeadersLeniently(incoming) {
378
+ const entries = [];
379
+ for (const [name, raw] of Object.entries(incoming ?? {})) {
380
+ const value = Array.isArray(raw) ? raw.at(-1) : raw;
381
+ const text = value === void 0 ? void 0 : decodeUtf8OrUndefined(value);
382
+ if (text !== void 0) {
383
+ entries.push([name, text]);
384
+ }
385
+ }
386
+ return Object.fromEntries(entries);
387
+ }
388
+ function decodeUtf8OrUndefined(value) {
389
+ if (typeof value === "string") {
390
+ return value;
391
+ }
392
+ try {
393
+ return exactUtf8.decode(value);
394
+ } catch {
395
+ return void 0;
396
+ }
397
+ }
398
+ function readKeyLeniently(key) {
399
+ if (key === null) {
400
+ return null;
401
+ }
402
+ try {
403
+ return readRecordKey(key);
404
+ } catch {
405
+ return Buffer.from(key).toString("base64");
406
+ }
407
+ }
408
+ function resolveText(option, { record, name }) {
409
+ const value = typeof option === "function" ? option(record) : option;
410
+ if (value === void 0 || value === null) {
411
+ return void 0;
412
+ }
413
+ if (typeof value !== "string") {
414
+ observeRejection(value);
415
+ throw invalid(`The ${name} option returned ${typeof value}, expected a string`);
416
+ }
417
+ return value.length === 0 ? void 0 : value;
418
+ }
419
+ function resolveOccurredAt(option, { plain, record }) {
420
+ const value = option?.(plain);
421
+ if (value === void 0) {
422
+ return requireRecordTimestamp(record);
423
+ }
424
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
425
+ observeRejection(value);
426
+ throw invalid("The occurredAt option returned no valid Date");
427
+ }
428
+ return value;
429
+ }
430
+ function observeRejection(value) {
431
+ if (typeof value === "object" && value !== null && "then" in value) {
432
+ Promise.resolve(value).catch(() => void 0);
433
+ }
434
+ }
435
+
436
+ // src/codecs.ts
437
+ var cloudEventsBinary = Object.freeze({
438
+ encode: encodeEnvelope,
439
+ decode: decodeEnvelope
440
+ });
441
+ var cloudEventsStructured = Object.freeze({
442
+ encode: encodeStructuredEnvelope,
443
+ decode: decodeCloudEvent
444
+ });
445
+ var RECORD_POSITION = "record-position";
446
+ var EVENT_ID_HEADER = "relaybus-event-id";
447
+ var DEFAULT_CONTENT_TYPE = "application/json";
448
+ function jsonEnvelope(mapping) {
449
+ const format = internal.createJsonEnvelopeFormat(mapping, {
450
+ transportIds: [RECORD_POSITION],
451
+ failures: { invalid, unencodable: (reason) => new UnencodableEventError(reason) }
452
+ });
453
+ const contentType = mapping.contentType ?? DEFAULT_CONTENT_TYPE;
454
+ const stampsTime = mapping.occurredAt === void 0;
455
+ const ownHeaders = (envelope) => mapping.id === RECORD_POSITION ? { [CONTENT_TYPE]: contentType, [EVENT_ID_HEADER]: envelope.id } : { [CONTENT_TYPE]: contentType };
456
+ return {
457
+ encode: (envelope) => {
458
+ assertRepresentable(envelope);
459
+ const { body, headers } = format.write(envelope);
460
+ const taken = [CONTENT_TYPE, EVENT_ID_HEADER].find((name) => Object.hasOwn(headers, name));
461
+ if (taken !== void 0) {
462
+ throw new UnencodableEventError(
463
+ `Event ${envelope.id} has a header '${taken}', which the codec writes itself`
464
+ );
465
+ }
466
+ return {
467
+ key: envelope.key,
468
+ value: serializeJson(envelope, body),
469
+ headers: { ...headers, ...ownHeaders(envelope) },
470
+ ...stampsTime ? { timestamp: recordTimestamp(envelope) } : {}
471
+ };
472
+ },
473
+ decode: (record) => {
474
+ const { [EVENT_ID_HEADER]: eventId, ...headers } = withoutContentType(
475
+ readHeadersLeniently(record.headers)
476
+ );
477
+ const fields = format.read({ body: parsePayload(record.value), headers });
478
+ return {
479
+ ...fields,
480
+ id: fields.id ?? (eventId === void 0 || eventId === "" ? `kafka:${record.topic}:${record.partition}:${record.offset}` : eventId)
481
+ };
482
+ }
483
+ };
484
+ }
485
+ function withoutContentType(headers) {
486
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => name !== CONTENT_TYPE));
487
+ }
488
+ function assertRepresentable(envelope) {
489
+ const kind = typeof envelope.payload;
490
+ if (kind === "undefined" || kind === "function" || kind === "symbol") {
491
+ throw new UnencodableEventError(
492
+ `Event ${envelope.id} has a payload that JSON cannot represent (undefined, a function or a symbol)`
493
+ );
494
+ }
495
+ }
496
+ function recordTimestamp(envelope) {
497
+ formatTime(envelope);
498
+ return String(envelope.occurredAt.getTime());
499
+ }
217
500
  var DEFAULTS = {
218
501
  name: "kafka",
219
502
  maxInFlightRequests: 1,
@@ -228,6 +511,7 @@ var DEFAULTS = {
228
511
  };
229
512
  var KafkaTransport = class {
230
513
  name;
514
+ system = "kafka";
231
515
  #kafka;
232
516
  #groupId;
233
517
  #fromBeginning;
@@ -235,6 +519,7 @@ var KafkaTransport = class {
235
519
  #producerConfig;
236
520
  #consumerConfig;
237
521
  #invalidPolicy;
522
+ #codec;
238
523
  #logger;
239
524
  #settings;
240
525
  #sleepers = /* @__PURE__ */ new Set();
@@ -253,15 +538,13 @@ var KafkaTransport = class {
253
538
  this.#kafka = options.kafka;
254
539
  this.#groupId = options.groupId;
255
540
  this.#fromBeginning = options.fromBeginning ?? true;
256
- this.#maxInFlightRequests = options.maxInFlightRequests ?? DEFAULTS.maxInFlightRequests;
257
- if (!Number.isInteger(this.#maxInFlightRequests) || this.#maxInFlightRequests < 1 || this.#maxInFlightRequests > 5) {
258
- throw new core.ConfigurationError(
259
- `maxInFlightRequests must be an integer from 1 to 5, received ${this.#maxInFlightRequests}`
260
- );
261
- }
541
+ this.#maxInFlightRequests = assertMaxInFlightRequests(
542
+ options.maxInFlightRequests ?? DEFAULTS.maxInFlightRequests
543
+ );
262
544
  this.#producerConfig = options.producerConfig ?? {};
263
545
  this.#consumerConfig = options.consumerConfig ?? {};
264
546
  this.#invalidPolicy = options.onInvalidMessage ?? "dead-letter";
547
+ this.#codec = resolveCodec(options);
265
548
  this.#logger = options.logger ?? console;
266
549
  this.#settings = resolveSettings(options);
267
550
  }
@@ -291,11 +574,10 @@ var KafkaTransport = class {
291
574
  }
292
575
  async publish(envelope, destination) {
293
576
  const producer = await this.#connectedProducer();
294
- const { key, value, headers } = encodeEnvelope(envelope);
295
577
  await this.#send(producer, {
296
578
  topic: destination,
297
579
  acks: -1,
298
- messages: [{ key, value, headers }]
580
+ messages: [this.#codec.encode(envelope)]
299
581
  });
300
582
  }
301
583
  /**
@@ -305,13 +587,14 @@ var KafkaTransport = class {
305
587
  * @throws ConfigurationError when `groupId` is missing, `destinations` is empty
306
588
  * or the transport already subscribed (create one transport per subscription).
307
589
  */
308
- async subscribe(destinations, onMessage) {
590
+ // eslint-disable-next-line relaybus/max-two-params -- the Transport.subscribe signature
591
+ async subscribe(destinations, onMessage, hooks) {
309
592
  this.#assertCanSubscribe(destinations);
310
593
  this.#state = "subscribed";
311
594
  const consumer = this.#kafka.consumer({ ...this.#consumerConfig, groupId: this.#groupId });
312
595
  this.#consumer = consumer;
313
596
  this.#observeConsumer(consumer);
314
- const start = () => this.#startConsumer(consumer, [...destinations], onMessage);
597
+ const start = () => this.#startConsumer(consumer, { destinations: [...destinations], onMessage, hooks });
315
598
  this.#startConsuming = start;
316
599
  try {
317
600
  await start();
@@ -327,12 +610,18 @@ var KafkaTransport = class {
327
610
  `[KafkaTransport](subscribe) Consuming | transport=${this.name} group=${this.#groupId} topics=${destinations.join(",")}`
328
611
  );
329
612
  }
330
- async #startConsumer(consumer, destinations, onMessage) {
613
+ async #startConsumer(consumer, {
614
+ destinations,
615
+ onMessage,
616
+ hooks
617
+ }) {
331
618
  await consumer.connect();
332
619
  await consumer.subscribe({ topics: destinations, fromBeginning: this.#fromBeginning });
333
620
  const joined = this.#waitForGroupJoin(consumer);
334
621
  try {
335
- await consumer.run({ eachMessage: (payload) => this.#handleMessage(payload, onMessage) });
622
+ await consumer.run({
623
+ eachMessage: (payload) => this.#handleMessage(payload, { onMessage, hooks })
624
+ });
336
625
  await joined.promise;
337
626
  } finally {
338
627
  joined.dispose();
@@ -350,7 +639,7 @@ var KafkaTransport = class {
350
639
  for (const wake of this.#sleepers) {
351
640
  wake();
352
641
  }
353
- this.#joinWaiter?.reject(new core.RelayBusError(`Transport '${this.name}' is closed`));
642
+ this.#joinWaiter?.reject(new TransportClosedError(`Transport '${this.name}' is closed`));
354
643
  await this.#restartLoop;
355
644
  const producer = await pendingProducer?.catch(() => void 0);
356
645
  const results = await Promise.allSettled([
@@ -364,7 +653,7 @@ var KafkaTransport = class {
364
653
  }
365
654
  #assertCanSubscribe(destinations) {
366
655
  if (this.#state === "closed") {
367
- throw new core.RelayBusError(`Transport '${this.name}' is closed`);
656
+ throw new TransportClosedError(`Transport '${this.name}' is closed`);
368
657
  }
369
658
  if (this.#state === "subscribed") {
370
659
  throw new core.ConfigurationError(
@@ -400,27 +689,30 @@ var KafkaTransport = class {
400
689
  }
401
690
  /** Sends with the publish timeout; the late outcome of a timed-out send is ignored. */
402
691
  async #send(producer, record) {
403
- let timer;
692
+ let cancel;
404
693
  const timeout = new Promise((_resolve, reject) => {
405
- timer = setTimeout(() => {
406
- reject(
407
- new PublishTimeoutError(
408
- `Transport '${this.name}' got no broker ack for topic '${record.topic}' within ${this.#settings.publishTimeoutMs}ms`
409
- )
410
- );
411
- }, this.#settings.publishTimeoutMs);
694
+ cancel = internal.schedule(core.systemClock, {
695
+ run: () => {
696
+ reject(
697
+ new PublishTimeoutError(
698
+ `Transport '${this.name}' got no broker ack for topic '${record.topic}' within ${this.#settings.publishTimeoutMs}ms`
699
+ )
700
+ );
701
+ },
702
+ delayMs: this.#settings.publishTimeoutMs
703
+ });
412
704
  });
413
705
  const sending = producer.send(record);
414
706
  sending.catch(() => void 0);
415
707
  try {
416
708
  await Promise.race([sending, timeout]);
417
709
  } finally {
418
- clearTimeout(timer);
710
+ cancel?.();
419
711
  }
420
712
  }
421
713
  #connectedProducer() {
422
714
  if (this.#state === "closed") {
423
- return Promise.reject(new core.RelayBusError(`Transport '${this.name}' is closed`));
715
+ return Promise.reject(new TransportClosedError(`Transport '${this.name}' is closed`));
424
716
  }
425
717
  this.#producer ??= this.#openProducer();
426
718
  return this.#producer;
@@ -440,40 +732,63 @@ var KafkaTransport = class {
440
732
  this.#producerConnected = this.#state !== "closed";
441
733
  return producer;
442
734
  }
443
- async #handleMessage(payload, onMessage) {
735
+ async #handleMessage(payload, {
736
+ onMessage,
737
+ hooks
738
+ }) {
444
739
  let envelope;
445
740
  try {
446
- envelope = decodeEnvelope(payload.message);
741
+ envelope = this.#decodeRecord(payload);
447
742
  } catch (error) {
448
- if (!(error instanceof InvalidKafkaMessageError)) {
743
+ if (this.#codec === cloudEventsBinary && !(error instanceof InvalidKafkaMessageError)) {
449
744
  throw toHandlerFailure(error);
450
745
  }
451
- await this.#withRetries(
452
- payload,
453
- "handleInvalid",
454
- () => this.#handleInvalid(payload, error.message)
455
- );
746
+ hooks?.onInvalidMessage({ destination: payload.topic, error });
747
+ await this.#withRetries(payload, {
748
+ method: "handleInvalid",
749
+ operation: () => this.#handleInvalid(payload, error)
750
+ });
456
751
  return;
457
752
  }
458
- await this.#withRetries(payload, "deliver", async () => {
459
- try {
460
- await onMessage(envelope);
461
- } catch (error) {
462
- if (!(error instanceof core.NonRetryableError)) {
463
- throw error;
753
+ await this.#withRetries(payload, {
754
+ method: "deliver",
755
+ operation: async () => {
756
+ try {
757
+ await onMessage(envelope, { destination: payload.topic });
758
+ } catch (error) {
759
+ if (!(error instanceof core.NonRetryableError)) {
760
+ throw error;
761
+ }
762
+ await this.#handleInvalid(payload, error);
464
763
  }
465
- await this.#handleInvalid(payload, error.message);
466
764
  }
467
765
  });
468
766
  }
469
- async #handleInvalid(payload, reason) {
767
+ #decodeRecord({ topic, partition, message }) {
768
+ if (this.#codec === cloudEventsBinary) {
769
+ return decodeEnvelope(message);
770
+ }
771
+ const record = {
772
+ topic,
773
+ partition,
774
+ offset: message.offset,
775
+ key: message.key,
776
+ value: message.value,
777
+ headers: message.headers,
778
+ timestamp: message.timestamp
779
+ };
780
+ return completeDecodedEvent(this.#codec.decode(record), record);
781
+ }
782
+ async #handleInvalid(payload, error) {
470
783
  const { topic, partition, message } = payload;
784
+ const { message: reason, name, code } = core.toErrorInfo(error);
471
785
  const location = `topic=${topic} partition=${partition} offset=${message.offset}`;
786
+ const errorFields = `errorName=${name}${code === void 0 ? "" : ` code=${code}`}`;
472
787
  const policy = this.#invalidPolicy;
473
788
  const deadLetterTopic = this.#deadLetterTopicFor(topic);
474
789
  if (policy === "skip") {
475
790
  this.#logger.error(
476
- `[KafkaTransport](handleInvalid) Skipping malformed message | ${location} reason=${reason}`
791
+ `[KafkaTransport](handleInvalid) Skipping malformed message | ${location} reason=${reason} ${errorFields}`
477
792
  );
478
793
  } else if (typeof policy === "function") {
479
794
  await policy({
@@ -485,6 +800,19 @@ var KafkaTransport = class {
485
800
  value: message.value
486
801
  });
487
802
  } else {
803
+ const headers = {
804
+ ...toOutgoingHeaders(message.headers),
805
+ "relaybus-invalid-reason": reason,
806
+ "relaybus-source-topic": topic,
807
+ "relaybus-source-partition": String(partition),
808
+ "relaybus-source-offset": message.offset,
809
+ "relaybus-error-name": name
810
+ };
811
+ if (code === void 0) {
812
+ delete headers["relaybus-error-code"];
813
+ } else {
814
+ headers["relaybus-error-code"] = code;
815
+ }
488
816
  const producer = await this.#connectedProducer();
489
817
  await this.#send(producer, {
490
818
  topic: deadLetterTopic,
@@ -493,18 +821,12 @@ var KafkaTransport = class {
493
821
  {
494
822
  key: message.key === null ? null : Buffer.from(message.key),
495
823
  value: message.value === null ? null : Buffer.from(message.value),
496
- headers: {
497
- ...toOutgoingHeaders(message.headers),
498
- "relaybus-invalid-reason": reason,
499
- "relaybus-source-topic": topic,
500
- "relaybus-source-partition": String(partition),
501
- "relaybus-source-offset": message.offset
502
- }
824
+ headers
503
825
  }
504
826
  ]
505
827
  });
506
828
  this.#logger.error(
507
- `[KafkaTransport](handleInvalid) Dead-lettered malformed message | ${location} deadLetterTopic=${deadLetterTopic} reason=${reason}`
829
+ `[KafkaTransport](handleInvalid) Dead-lettered malformed message | ${location} deadLetterTopic=${deadLetterTopic} reason=${reason} ${errorFields}`
508
830
  );
509
831
  }
510
832
  }
@@ -513,7 +835,7 @@ var KafkaTransport = class {
513
835
  * last error is rethrown so the client leaves the offset uncommitted and
514
836
  * redelivers the record; heartbeats between attempts keep the session alive.
515
837
  */
516
- async #withRetries(payload, method, operation) {
838
+ async #withRetries(payload, { method, operation }) {
517
839
  const { maxAttempts, initialBackoffMs, maxBackoffMs } = this.#settings;
518
840
  const location = `topic=${payload.topic} partition=${payload.partition} offset=${payload.message.offset}`;
519
841
  const partitionKey = `${payload.topic}:${payload.partition}`;
@@ -527,12 +849,12 @@ var KafkaTransport = class {
527
849
  if (attempt >= maxAttempts || this.#state === "closed") {
528
850
  this.#stalledPartitions.add(partitionKey);
529
851
  this.#logger.error(
530
- `[KafkaTransport](${method}) Giving up for now, the record stays uncommitted and will be redelivered | ${location} attempts=${attempt} error=${reason}`
852
+ `[KafkaTransport](withRetries) Giving up for now, the record stays uncommitted and will be redelivered | operation=${method} ${location} attempts=${attempt} error=${reason}`
531
853
  );
532
854
  throw toHandlerFailure(error);
533
855
  }
534
856
  this.#logger.warn(
535
- `[KafkaTransport](${method}) Attempt failed, retrying | ${location} attempt=${attempt} error=${reason}`
857
+ `[KafkaTransport](withRetries) Attempt failed, retrying | operation=${method} ${location} attempt=${attempt} error=${reason}`
536
858
  );
537
859
  await this.#sleepWithHeartbeats(
538
860
  Math.min(initialBackoffMs * 2 ** (attempt - 1), maxBackoffMs),
@@ -554,11 +876,11 @@ var KafkaTransport = class {
554
876
  #sleep(milliseconds) {
555
877
  return new Promise((resolve) => {
556
878
  const wake = () => {
557
- clearTimeout(timer);
879
+ cancel();
558
880
  this.#sleepers.delete(wake);
559
881
  resolve();
560
882
  };
561
- const timer = setTimeout(wake, milliseconds);
883
+ const cancel = internal.schedule(core.systemClock, { run: wake, delayMs: milliseconds });
562
884
  this.#sleepers.add(wake);
563
885
  });
564
886
  }
@@ -640,42 +962,53 @@ var KafkaTransport = class {
640
962
  if (!this.#tracksMembership(consumer)) {
641
963
  return { promise: Promise.resolve(), dispose: () => void 0 };
642
964
  }
643
- let timer;
965
+ let cancel;
644
966
  const promise = new Promise((resolve, reject) => {
645
967
  this.#joinWaiter = { resolve, reject };
646
- timer = setTimeout(() => {
647
- reject(
648
- new core.RelayBusError(
649
- `Consumer of transport '${this.name}' did not join group '${this.#groupId}' within ${this.#settings.startupTimeoutMs}ms`
650
- )
651
- );
652
- }, this.#settings.startupTimeoutMs);
968
+ cancel = internal.schedule(core.systemClock, {
969
+ run: () => {
970
+ reject(
971
+ new GroupJoinTimeoutError(
972
+ `Consumer of transport '${this.name}' did not join group '${this.#groupId}' within ${this.#settings.startupTimeoutMs}ms`
973
+ )
974
+ );
975
+ },
976
+ delayMs: this.#settings.startupTimeoutMs
977
+ });
653
978
  });
654
979
  promise.catch(() => void 0);
655
980
  return {
656
981
  promise,
657
982
  dispose: () => {
658
- clearTimeout(timer);
983
+ cancel?.();
659
984
  this.#joinWaiter = void 0;
660
985
  }
661
986
  };
662
987
  }
663
988
  };
664
- function resolveSettings(options) {
665
- const maxAttempts = options.processing?.maxAttempts ?? DEFAULTS.maxAttempts;
666
- if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
667
- throw new core.ConfigurationError(
668
- `processing.maxAttempts must be a positive integer, received ${maxAttempts}`
669
- );
989
+ function resolveCodec({ codec, decode }) {
990
+ if (decode === void 0) {
991
+ return codec ?? cloudEventsBinary;
670
992
  }
993
+ if (codec !== void 0) {
994
+ throw new core.ConfigurationError("Give 'codec' or the deprecated 'decode', not both");
995
+ }
996
+ return { encode: encodeEnvelope, decode };
997
+ }
998
+ function resolveSettings(options) {
999
+ const processing = options.processing ?? {};
1000
+ const maxAttempts = internal.assertPositiveInteger(
1001
+ processing.maxAttempts ?? DEFAULTS.maxAttempts,
1002
+ "processing.maxAttempts"
1003
+ );
671
1004
  return {
672
1005
  maxAttempts,
673
1006
  initialBackoffMs: core.toMilliseconds(
674
- options.processing?.initialBackoff ?? DEFAULTS.initialBackoff,
1007
+ processing.initialBackoff ?? DEFAULTS.initialBackoff,
675
1008
  "processing.initialBackoff"
676
1009
  ),
677
1010
  maxBackoffMs: core.toMilliseconds(
678
- options.processing?.maxBackoff ?? DEFAULTS.maxBackoff,
1011
+ processing.maxBackoff ?? DEFAULTS.maxBackoff,
679
1012
  "processing.maxBackoff"
680
1013
  ),
681
1014
  startupTimeoutMs: core.toMilliseconds(
@@ -689,31 +1022,60 @@ function resolveSettings(options) {
689
1022
  heartbeatIntervalMs: Math.max(
690
1023
  1,
691
1024
  core.toMilliseconds(
692
- options.processing?.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
1025
+ processing.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
693
1026
  "processing.heartbeatInterval"
694
1027
  )
695
1028
  ),
1029
+ ...resolveRestartSettings(options.restart ?? {})
1030
+ };
1031
+ }
1032
+ function resolveRestartSettings(restart) {
1033
+ return {
696
1034
  restartInitialBackoffMs: core.toMilliseconds(
697
- options.restart?.initialBackoff ?? DEFAULTS.restartInitialBackoff,
1035
+ restart.initialBackoff ?? DEFAULTS.restartInitialBackoff,
698
1036
  "restart.initialBackoff"
699
1037
  ),
700
1038
  restartMaxBackoffMs: core.toMilliseconds(
701
- options.restart?.maxBackoff ?? DEFAULTS.restartMaxBackoff,
1039
+ restart.maxBackoff ?? DEFAULTS.restartMaxBackoff,
702
1040
  "restart.maxBackoff"
703
1041
  )
704
1042
  };
705
1043
  }
1044
+ var MAXIMUM_IN_FLIGHT_REQUESTS = 5;
1045
+ function assertMaxInFlightRequests(value) {
1046
+ if (!Number.isInteger(value) || value < 1 || value > MAXIMUM_IN_FLIGHT_REQUESTS) {
1047
+ throw new core.ConfigurationError(
1048
+ `maxInFlightRequests must be an integer from 1 to 5, received ${value}`
1049
+ );
1050
+ }
1051
+ return value;
1052
+ }
706
1053
  function toHandlerFailure(error) {
707
1054
  const { name, message } = core.toErrorInfo(error);
708
1055
  return new HandlerFailedError(`${name}: ${message}`);
709
1056
  }
710
1057
 
1058
+ Object.defineProperty(exports, "fromHeader", {
1059
+ enumerable: true,
1060
+ get: function () { return internal.fromHeader; }
1061
+ });
1062
+ Object.defineProperty(exports, "fromPayloadField", {
1063
+ enumerable: true,
1064
+ get: function () { return internal.fromPayloadField; }
1065
+ });
1066
+ exports.GroupJoinTimeoutError = GroupJoinTimeoutError;
711
1067
  exports.HandlerFailedError = HandlerFailedError;
712
1068
  exports.InvalidKafkaMessageError = InvalidKafkaMessageError;
713
1069
  exports.KafkaTransport = KafkaTransport;
714
1070
  exports.PublishTimeoutError = PublishTimeoutError;
1071
+ exports.TransportClosedError = TransportClosedError;
715
1072
  exports.UnencodableEventError = UnencodableEventError;
1073
+ exports.cloudEventsBinary = cloudEventsBinary;
1074
+ exports.cloudEventsStructured = cloudEventsStructured;
1075
+ exports.decodeCloudEvent = decodeCloudEvent;
716
1076
  exports.decodeEnvelope = decodeEnvelope;
717
1077
  exports.encodeEnvelope = encodeEnvelope;
1078
+ exports.jsonEnvelope = jsonEnvelope;
1079
+ exports.plainJsonDecoder = plainJsonDecoder;
718
1080
  //# sourceMappingURL=index.cjs.map
719
1081
  //# sourceMappingURL=index.cjs.map