@relaybus/kafka 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -19
- package/dist/index.cjs +277 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -2
- package/dist/index.d.ts +163 -2
- package/dist/index.js +274 -14
- package/dist/index.js.map +1 -1
- package/package.json +22 -9
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RelayBusError, NonRetryableError, EventEnvelope, Duration, Transport, RelayBusLogger, TransportMessageHandler } from '@relaybus/core';
|
|
1
|
+
import { RelayBusError, NonRetryableError, EventEnvelope, EventHeaders, Duration, Transport, RelayBusLogger, TransportMessageHandler } from '@relaybus/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Structural views of the Kafka client API used by this package.
|
|
@@ -143,6 +143,125 @@ declare function encodeEnvelope(envelope: EventEnvelope): EncodedMessage;
|
|
|
143
143
|
*/
|
|
144
144
|
declare function decodeEnvelope(message: DecodableMessage): EventEnvelope;
|
|
145
145
|
|
|
146
|
+
/** A consumed record as handed to a {@link KafkaMessageDecoder}. */
|
|
147
|
+
interface KafkaInboundRecord extends DecodableMessage {
|
|
148
|
+
readonly topic: string;
|
|
149
|
+
readonly partition: number;
|
|
150
|
+
readonly offset: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The event a decoder extracted from a record. `id`, `type` and `payload` are
|
|
154
|
+
* required; every other field may be left out and is then filled by the
|
|
155
|
+
* transport: `source` with the topic, `key` with the record key (UTF-8),
|
|
156
|
+
* `headers` with none, `correlationId` and `causationId` with null, and
|
|
157
|
+
* `occurredAt` with the record timestamp.
|
|
158
|
+
*/
|
|
159
|
+
interface DecodedEvent {
|
|
160
|
+
readonly id: string;
|
|
161
|
+
readonly type: string;
|
|
162
|
+
readonly payload: unknown;
|
|
163
|
+
readonly source?: string;
|
|
164
|
+
readonly key?: string | null;
|
|
165
|
+
readonly headers?: EventHeaders;
|
|
166
|
+
readonly correlationId?: string | null;
|
|
167
|
+
readonly causationId?: string | null;
|
|
168
|
+
readonly occurredAt?: Date;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Turns a consumed record into an event, for topics written by producers that
|
|
172
|
+
* do not use relaybus. Must be synchronous and free of side effects: it runs
|
|
173
|
+
* again on every redelivery, so the same record must always give the same
|
|
174
|
+
* event, above all the same `id`, which is what the inbox deduplicates on.
|
|
175
|
+
*
|
|
176
|
+
* Anything it throws marks the record as malformed: it goes to the transport's
|
|
177
|
+
* `onInvalidMessage` policy (dead-letter by default) and the consumer moves on.
|
|
178
|
+
* An event the bus then rejects (empty id, invalid type or time) is
|
|
179
|
+
* dead-lettered the same way.
|
|
180
|
+
*/
|
|
181
|
+
type KafkaMessageDecoder = (record: KafkaInboundRecord) => DecodedEvent;
|
|
182
|
+
/** A plain JSON record, decoded for the extractor options of {@link plainJsonDecoder}. */
|
|
183
|
+
interface PlainJsonRecord {
|
|
184
|
+
readonly topic: string;
|
|
185
|
+
readonly partition: number;
|
|
186
|
+
readonly offset: string;
|
|
187
|
+
/**
|
|
188
|
+
* The record key as UTF-8 text, base64 when its bytes are not valid UTF-8
|
|
189
|
+
* (for example a key written by Java's `LongSerializer`), null when absent.
|
|
190
|
+
*/
|
|
191
|
+
readonly key: string | null;
|
|
192
|
+
/**
|
|
193
|
+
* Every record header as text: the last value of a repeated header; a value
|
|
194
|
+
* that is not valid UTF-8 is left out.
|
|
195
|
+
*/
|
|
196
|
+
readonly headers: EventHeaders;
|
|
197
|
+
/** The record value parsed as JSON. */
|
|
198
|
+
readonly payload: unknown;
|
|
199
|
+
/** Record timestamp in epoch milliseconds. */
|
|
200
|
+
readonly timestamp: string | undefined;
|
|
201
|
+
}
|
|
202
|
+
/** Options of {@link plainJsonDecoder}. Extractors returning undefined or `''` select the default. */
|
|
203
|
+
interface PlainJsonDecoderOptions {
|
|
204
|
+
/**
|
|
205
|
+
* Event type handlers subscribe to: a fixed name for a topic that carries one
|
|
206
|
+
* kind of event, or an extractor such as `fromHeader('event-type')` or
|
|
207
|
+
* `fromPayloadField('type')`. Required; a record without a type is invalid.
|
|
208
|
+
*/
|
|
209
|
+
readonly type: string | ((record: PlainJsonRecord) => string | undefined);
|
|
210
|
+
/**
|
|
211
|
+
* Event id the inbox deduplicates on, for example `fromHeader('message-id')`.
|
|
212
|
+
* Default, and fallback when the extractor finds none:
|
|
213
|
+
* `kafka:<topic>:<partition>:<offset>`, which is stable across redeliveries
|
|
214
|
+
* of the record. Prefer an id set by the producer when it may send the same
|
|
215
|
+
* event twice: two sends are two offsets, so only a producer id dedupes them.
|
|
216
|
+
*/
|
|
217
|
+
readonly id?: (record: PlainJsonRecord) => string | undefined;
|
|
218
|
+
/** Ordering key. Default: the record key. When given, its result is final (undefined means no key). */
|
|
219
|
+
readonly key?: (record: PlainJsonRecord) => string | null | undefined;
|
|
220
|
+
/** Logical name of the producer. Default: the topic. */
|
|
221
|
+
readonly source?: string | ((record: PlainJsonRecord) => string | undefined);
|
|
222
|
+
/** When the event happened. Default: the record timestamp. */
|
|
223
|
+
readonly occurredAt?: (record: PlainJsonRecord) => Date | undefined;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Decodes a CloudEvents 1.0 record in either content mode of the Kafka
|
|
227
|
+
* protocol binding:
|
|
228
|
+
* - structured (`content-type: application/cloudevents+json`): the value is
|
|
229
|
+
* a JSON object with `specversion`, `id`, `type`, `source` and optionally
|
|
230
|
+
* `time`, `subject` (the key, else the record key), `data` (the payload,
|
|
231
|
+
* null when absent) and a JSON `datacontenttype`. `data_base64` is refused,
|
|
232
|
+
* because relaybus payloads are JSON. Extension attributes with string
|
|
233
|
+
* values (`traceparent`, ...) become event headers, as do the record
|
|
234
|
+
* headers; `correlationid` and `causationid` fill their envelope fields.
|
|
235
|
+
* - binary (anything else): exactly {@link decodeEnvelope}, the default decoder.
|
|
236
|
+
*
|
|
237
|
+
* @throws InvalidKafkaMessageError for a record that is neither.
|
|
238
|
+
*/
|
|
239
|
+
declare function decodeCloudEvent(record: DecodableMessage): EventEnvelope;
|
|
240
|
+
/**
|
|
241
|
+
* Creates a decoder for records whose value is plain JSON, as written by a
|
|
242
|
+
* service that does not use relaybus (a plain kafkajs producer, Spring Kafka's
|
|
243
|
+
* `JsonSerializer`, a Go service): the value becomes the payload, the record
|
|
244
|
+
* headers become event headers, and the options say where the event type and
|
|
245
|
+
* id come from.
|
|
246
|
+
*
|
|
247
|
+
* A record that is a CloudEvent anyway (a `ce_specversion` header, or a
|
|
248
|
+
* structured `application/cloudevents+json` value) is decoded with
|
|
249
|
+
* {@link decodeCloudEvent} instead, so relaybus and foreign producers can share
|
|
250
|
+
* a topic. A missing or non-JSON value, a missing or invalid type, and an
|
|
251
|
+
* extractor returning anything but a string are invalid.
|
|
252
|
+
*
|
|
253
|
+
* ```ts
|
|
254
|
+
* new KafkaTransport({
|
|
255
|
+
* kafka,
|
|
256
|
+
* groupId: 'billing',
|
|
257
|
+
* decode: plainJsonDecoder({ type: fromHeader('event-type'), id: fromHeader('message-id') }),
|
|
258
|
+
* });
|
|
259
|
+
* ```
|
|
260
|
+
*
|
|
261
|
+
* @throws ConfigurationError when a fixed `type` is not a valid event type.
|
|
262
|
+
*/
|
|
263
|
+
declare function plainJsonDecoder(options: PlainJsonDecoderOptions): KafkaMessageDecoder;
|
|
264
|
+
|
|
146
265
|
/** A record the transport refused to hand to the bus because it is malformed. */
|
|
147
266
|
interface InvalidKafkaMessage {
|
|
148
267
|
readonly topic: string;
|
|
@@ -224,12 +343,25 @@ interface KafkaTransportOptions {
|
|
|
224
343
|
readonly restart?: KafkaRestartOptions;
|
|
225
344
|
/** Handling of records that cannot be processed for good. Default `'dead-letter'`. */
|
|
226
345
|
readonly onInvalidMessage?: InvalidMessagePolicy;
|
|
346
|
+
/**
|
|
347
|
+
* How consumed records become events. Default: strict CloudEvents binary
|
|
348
|
+
* mode ({@link decodeEnvelope}), which is what relaybus producers write; any
|
|
349
|
+
* other record is malformed and goes to `onInvalidMessage`. Set it to read
|
|
350
|
+
* topics written by services that do not use relaybus:
|
|
351
|
+
* {@link decodeCloudEvent} also accepts structured mode, {@link plainJsonDecoder}
|
|
352
|
+
* maps plain JSON records, and a function of your own can do anything else
|
|
353
|
+
* (switch on `record.topic` to decode topics differently). Whatever a custom
|
|
354
|
+
* decoder throws marks the record as malformed, never crashes the consumer.
|
|
355
|
+
*/
|
|
356
|
+
readonly decode?: KafkaMessageDecoder;
|
|
227
357
|
/** Default `console`. */
|
|
228
358
|
readonly logger?: RelayBusLogger;
|
|
229
359
|
}
|
|
230
360
|
/**
|
|
231
361
|
* Kafka {@link Transport}. Events travel as CloudEvents binary-mode records
|
|
232
362
|
* (`ce_*` headers, JSON value), so any CloudEvents-aware consumer can read them.
|
|
363
|
+
* Records from producers that do not use relaybus are read through the
|
|
364
|
+
* `decode` option.
|
|
233
365
|
*
|
|
234
366
|
* Publishing: idempotent producer, `acks=-1`, resolves after the broker ack and
|
|
235
367
|
* rejects with {@link PublishTimeoutError} after `publishTimeout`. A timed-out
|
|
@@ -280,4 +412,33 @@ declare class KafkaTransport implements Transport {
|
|
|
280
412
|
close(): Promise<void>;
|
|
281
413
|
}
|
|
282
414
|
|
|
283
|
-
|
|
415
|
+
/**
|
|
416
|
+
* Reads one header of an incoming message, for the `type`, `id`, `key` or
|
|
417
|
+
* `source` option of {@link plainJsonDecoder}. Header names are compared
|
|
418
|
+
* exactly (Kafka header names are case-sensitive). Returns undefined when the
|
|
419
|
+
* header is absent, so the decoder falls back to its default.
|
|
420
|
+
*
|
|
421
|
+
* ```ts
|
|
422
|
+
* plainJsonDecoder({ type: fromHeader('event-type'), id: fromHeader('message-id') })
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
declare function fromHeader(name: string): (message: {
|
|
426
|
+
readonly headers: EventHeaders;
|
|
427
|
+
}) => string | undefined;
|
|
428
|
+
/**
|
|
429
|
+
* Reads a field of the JSON payload by a dotted path (`'meta.eventType'`), for
|
|
430
|
+
* the `type`, `id`, `key` or `source` option of {@link plainJsonDecoder}. A
|
|
431
|
+
* string is returned as is and a safe integer as its decimal text (numeric ids
|
|
432
|
+
* are common). Anything else returns undefined so the decoder falls back to its
|
|
433
|
+
* default: a missing field, and also a fraction or an integer beyond 2^53, which
|
|
434
|
+
* JSON parsing may already have rounded, so two different ids could collide.
|
|
435
|
+
*
|
|
436
|
+
* ```ts
|
|
437
|
+
* plainJsonDecoder({ type: fromPayloadField('eventType'), key: fromPayloadField('order.id') })
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
declare function fromPayloadField(path: string): (message: {
|
|
441
|
+
readonly payload: unknown;
|
|
442
|
+
}) => string | undefined;
|
|
443
|
+
|
|
444
|
+
export { type DecodableMessage, type DecodedEvent, type EncodedMessage, HandlerFailedError, type InvalidKafkaMessage, InvalidKafkaMessageError, type InvalidMessagePolicy, type KafkaBytes, type KafkaClientLike, type KafkaConsumerConfig, type KafkaConsumerEvent, type KafkaConsumerEventNames, type KafkaConsumerLike, type KafkaEachMessagePayload, type KafkaInboundRecord, type KafkaIncomingHeaders, type KafkaIncomingMessage, type KafkaMessageDecoder, type KafkaOutgoingHeaders, type KafkaOutgoingMessage, type KafkaProcessingOptions, type KafkaProducerConfig, type KafkaProducerLike, type KafkaProducerRecord, type KafkaRestartOptions, type KafkaRunConfig, KafkaTransport, type KafkaTransportOptions, type PlainJsonDecoderOptions, type PlainJsonRecord, PublishTimeoutError, UnencodableEventError, decodeCloudEvent, decodeEnvelope, encodeEnvelope, fromHeader, fromPayloadField, plainJsonDecoder };
|
package/dist/index.js
CHANGED
|
@@ -147,17 +147,20 @@ function resolveTime(text, recordTimestamp) {
|
|
|
147
147
|
if (text !== void 0 && text.length > 0) {
|
|
148
148
|
return parseTime(text);
|
|
149
149
|
}
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
if (recordTimestamp === void 0 || recordTimestamp === "" || Number.isNaN(time.getTime())) {
|
|
150
|
+
const time = readRecordTimestamp(recordTimestamp);
|
|
151
|
+
if (time === void 0) {
|
|
153
152
|
throw invalid(`${HEADER.time} is missing and the record has no usable timestamp`);
|
|
154
153
|
}
|
|
155
154
|
return time;
|
|
156
155
|
}
|
|
157
|
-
function
|
|
156
|
+
function readRecordTimestamp(recordTimestamp) {
|
|
157
|
+
const time = new Date(Number(recordTimestamp));
|
|
158
|
+
return recordTimestamp === void 0 || recordTimestamp === "" || Number.isNaN(time.getTime()) ? void 0 : time;
|
|
159
|
+
}
|
|
160
|
+
function parseTime(text, label = HEADER.time) {
|
|
158
161
|
const time = new Date(text);
|
|
159
162
|
if (!RFC3339_PATTERN.test(text) || Number.isNaN(time.getTime())) {
|
|
160
|
-
throw invalid(`${
|
|
163
|
+
throw invalid(`${label} is not an RFC 3339 timestamp: '${text}'`);
|
|
161
164
|
}
|
|
162
165
|
return time;
|
|
163
166
|
}
|
|
@@ -173,12 +176,15 @@ function parsePayload(value) {
|
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
function resolveKey(subject, recordKey) {
|
|
176
|
-
const keyText =
|
|
179
|
+
const keyText = readRecordKey(recordKey);
|
|
177
180
|
if (subject !== void 0 && keyText !== null && subject !== keyText) {
|
|
178
181
|
throw invalid(`${HEADER.subject} does not match the message key`);
|
|
179
182
|
}
|
|
180
183
|
return subject ?? keyText;
|
|
181
184
|
}
|
|
185
|
+
function readRecordKey(recordKey) {
|
|
186
|
+
return recordKey === null ? null : decodeText(recordKey, true, "Message key");
|
|
187
|
+
}
|
|
182
188
|
function collectUserHeaders(headers) {
|
|
183
189
|
const entries = [];
|
|
184
190
|
for (const [name, value] of headers) {
|
|
@@ -206,6 +212,219 @@ function toOutgoingHeaders(incoming) {
|
|
|
206
212
|
function toBufferOrString(value) {
|
|
207
213
|
return typeof value === "string" ? value : Buffer.from(value);
|
|
208
214
|
}
|
|
215
|
+
var STRUCTURED_MEDIA_TYPE = "application/cloudevents+json";
|
|
216
|
+
var exactUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
217
|
+
var SPEC_VERSION2 = "1.0";
|
|
218
|
+
var SPEC_VERSION_HEADER = "ce_specversion";
|
|
219
|
+
var STRUCTURED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
220
|
+
"specversion",
|
|
221
|
+
"id",
|
|
222
|
+
"type",
|
|
223
|
+
"source",
|
|
224
|
+
"time",
|
|
225
|
+
"subject",
|
|
226
|
+
"datacontenttype",
|
|
227
|
+
"dataschema",
|
|
228
|
+
"data",
|
|
229
|
+
"data_base64",
|
|
230
|
+
"correlationid",
|
|
231
|
+
"causationid"
|
|
232
|
+
]);
|
|
233
|
+
function decodeCloudEvent(record) {
|
|
234
|
+
const headers = readHeaders(record.headers);
|
|
235
|
+
if (mediaTypeOf(headers.get(CONTENT_TYPE)) !== STRUCTURED_MEDIA_TYPE) {
|
|
236
|
+
return decodeEnvelope(record);
|
|
237
|
+
}
|
|
238
|
+
const body = parsePayload(record.value);
|
|
239
|
+
if (!isJsonObject(body)) {
|
|
240
|
+
throw invalid("A structured CloudEvent must be a JSON object");
|
|
241
|
+
}
|
|
242
|
+
if (body["specversion"] !== SPEC_VERSION2) {
|
|
243
|
+
throw invalid(`specversion must be '${SPEC_VERSION2}'`);
|
|
244
|
+
}
|
|
245
|
+
const type = requiredAttribute(body, "type");
|
|
246
|
+
assertValidType(type, "type");
|
|
247
|
+
assertJsonDataContentType(optionalAttribute(body, "datacontenttype"));
|
|
248
|
+
if (Object.hasOwn(body, "data_base64")) {
|
|
249
|
+
throw invalid("data_base64 is not supported: relaybus payloads are JSON, send them as data");
|
|
250
|
+
}
|
|
251
|
+
const time = optionalAttribute(body, "time");
|
|
252
|
+
return {
|
|
253
|
+
id: requiredAttribute(body, "id"),
|
|
254
|
+
type,
|
|
255
|
+
source: requiredAttribute(body, "source"),
|
|
256
|
+
key: optionalAttribute(body, "subject") ?? readRecordKey(record.key),
|
|
257
|
+
payload: Object.hasOwn(body, "data") ? body["data"] : null,
|
|
258
|
+
headers: { ...collectUserHeaders(headers), ...extensionAttributes(body) },
|
|
259
|
+
correlationId: optionalAttribute(body, "correlationid") ?? null,
|
|
260
|
+
causationId: optionalAttribute(body, "causationid") ?? null,
|
|
261
|
+
occurredAt: time === void 0 ? requireRecordTimestamp(record) : parseTime(time, "time")
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function plainJsonDecoder(options) {
|
|
265
|
+
if (typeof options.type === "string") {
|
|
266
|
+
assertEventType(options.type);
|
|
267
|
+
}
|
|
268
|
+
return (record) => {
|
|
269
|
+
const headers = readHeadersLeniently(record.headers);
|
|
270
|
+
if (record.headers?.[SPEC_VERSION_HEADER] !== void 0 || mediaTypeOf(headers[CONTENT_TYPE]) === STRUCTURED_MEDIA_TYPE) {
|
|
271
|
+
return decodeCloudEvent(record);
|
|
272
|
+
}
|
|
273
|
+
const plain = {
|
|
274
|
+
topic: record.topic,
|
|
275
|
+
partition: record.partition,
|
|
276
|
+
offset: record.offset,
|
|
277
|
+
key: readKeyLeniently(record.key),
|
|
278
|
+
headers,
|
|
279
|
+
payload: parsePayload(record.value),
|
|
280
|
+
timestamp: record.timestamp
|
|
281
|
+
};
|
|
282
|
+
const type = resolveText(options.type, plain, "type");
|
|
283
|
+
if (type === void 0) {
|
|
284
|
+
throw invalid("Record has no event type: the type option found none");
|
|
285
|
+
}
|
|
286
|
+
assertValidType(type, "The event type");
|
|
287
|
+
return {
|
|
288
|
+
id: resolveText(options.id, plain, "id") ?? `kafka:${record.topic}:${record.partition}:${record.offset}`,
|
|
289
|
+
type,
|
|
290
|
+
source: resolveText(options.source, plain, "source") ?? record.topic,
|
|
291
|
+
key: options.key === void 0 ? plain.key : resolveText(options.key, plain, "key") ?? null,
|
|
292
|
+
payload: plain.payload,
|
|
293
|
+
headers,
|
|
294
|
+
correlationId: null,
|
|
295
|
+
causationId: null,
|
|
296
|
+
occurredAt: resolveOccurredAt(options.occurredAt, plain, record)
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function completeDecodedEvent(decoded, record) {
|
|
301
|
+
if (typeof decoded !== "object" || decoded === null) {
|
|
302
|
+
throw invalid("The decoder must return an event object synchronously");
|
|
303
|
+
}
|
|
304
|
+
if ("then" in decoded) {
|
|
305
|
+
observeRejection(decoded);
|
|
306
|
+
throw invalid("The decoder must return an event object synchronously");
|
|
307
|
+
}
|
|
308
|
+
const event = decoded;
|
|
309
|
+
return {
|
|
310
|
+
id: event.id,
|
|
311
|
+
type: event.type,
|
|
312
|
+
source: event.source ?? record.topic,
|
|
313
|
+
key: event.key === void 0 ? readRecordKey(record.key) : event.key,
|
|
314
|
+
payload: event.payload,
|
|
315
|
+
headers: event.headers ?? {},
|
|
316
|
+
correlationId: event.correlationId ?? null,
|
|
317
|
+
causationId: event.causationId ?? null,
|
|
318
|
+
occurredAt: event.occurredAt ?? requireRecordTimestamp(record)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function mediaTypeOf(contentType) {
|
|
322
|
+
return contentType?.split(";")[0]?.trim().toLowerCase();
|
|
323
|
+
}
|
|
324
|
+
function isJsonObject(value) {
|
|
325
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
326
|
+
}
|
|
327
|
+
function optionalAttribute(body, name) {
|
|
328
|
+
const value = body[name];
|
|
329
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
330
|
+
throw invalid(`Attribute '${name}' must be a string`);
|
|
331
|
+
}
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
function requiredAttribute(body, name) {
|
|
335
|
+
const value = optionalAttribute(body, name);
|
|
336
|
+
if (value === void 0 || value.length === 0) {
|
|
337
|
+
throw invalid(`Attribute '${name}' is missing or empty`);
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
function assertValidType(type, label) {
|
|
342
|
+
try {
|
|
343
|
+
assertEventType(type);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
throw invalid(`${label} is not a valid event type: ${error.message}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function assertJsonDataContentType(dataContentType) {
|
|
349
|
+
const mediaType = mediaTypeOf(dataContentType);
|
|
350
|
+
if (mediaType !== void 0 && mediaType !== "application/json" && mediaType !== "text/json" && !mediaType.endsWith("+json")) {
|
|
351
|
+
throw invalid(`datacontenttype must be JSON, received '${dataContentType}'`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function extensionAttributes(body) {
|
|
355
|
+
return Object.fromEntries(
|
|
356
|
+
Object.entries(body).filter(
|
|
357
|
+
(entry) => !STRUCTURED_ATTRIBUTES.has(entry[0]) && typeof entry[1] === "string"
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
function requireRecordTimestamp(record) {
|
|
362
|
+
const time = readRecordTimestamp(record.timestamp);
|
|
363
|
+
if (time === void 0) {
|
|
364
|
+
throw invalid("The event has no time and the record has no usable timestamp");
|
|
365
|
+
}
|
|
366
|
+
return time;
|
|
367
|
+
}
|
|
368
|
+
function readHeadersLeniently(incoming) {
|
|
369
|
+
const entries = [];
|
|
370
|
+
for (const [name, raw] of Object.entries(incoming ?? {})) {
|
|
371
|
+
const value = Array.isArray(raw) ? raw.at(-1) : raw;
|
|
372
|
+
const text = value === void 0 ? void 0 : decodeUtf8OrUndefined(value);
|
|
373
|
+
if (text !== void 0) {
|
|
374
|
+
entries.push([name, text]);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return Object.fromEntries(entries);
|
|
378
|
+
}
|
|
379
|
+
function decodeUtf8OrUndefined(value) {
|
|
380
|
+
if (typeof value === "string") {
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
return exactUtf8.decode(value);
|
|
385
|
+
} catch {
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function readKeyLeniently(key) {
|
|
390
|
+
if (key === null) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
return readRecordKey(key);
|
|
395
|
+
} catch {
|
|
396
|
+
return Buffer.from(key).toString("base64");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function resolveText(option, record, name) {
|
|
400
|
+
const value = typeof option === "function" ? option(record) : option;
|
|
401
|
+
if (value === void 0 || value === null) {
|
|
402
|
+
return void 0;
|
|
403
|
+
}
|
|
404
|
+
if (typeof value !== "string") {
|
|
405
|
+
observeRejection(value);
|
|
406
|
+
throw invalid(`The ${name} option returned ${typeof value}, expected a string`);
|
|
407
|
+
}
|
|
408
|
+
return value.length === 0 ? void 0 : value;
|
|
409
|
+
}
|
|
410
|
+
function resolveOccurredAt(option, plain, record) {
|
|
411
|
+
const value = option?.(plain);
|
|
412
|
+
if (value === void 0) {
|
|
413
|
+
return requireRecordTimestamp(record);
|
|
414
|
+
}
|
|
415
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
416
|
+
observeRejection(value);
|
|
417
|
+
throw invalid("The occurredAt option returned no valid Date");
|
|
418
|
+
}
|
|
419
|
+
return value;
|
|
420
|
+
}
|
|
421
|
+
function observeRejection(value) {
|
|
422
|
+
if (typeof value === "object" && value !== null && "then" in value) {
|
|
423
|
+
Promise.resolve(value).catch(() => void 0);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/kafka-transport.ts
|
|
209
428
|
var PublishTimeoutError = class extends RelayBusError {
|
|
210
429
|
name = "PublishTimeoutError";
|
|
211
430
|
};
|
|
@@ -233,6 +452,7 @@ var KafkaTransport = class {
|
|
|
233
452
|
#producerConfig;
|
|
234
453
|
#consumerConfig;
|
|
235
454
|
#invalidPolicy;
|
|
455
|
+
#decode;
|
|
236
456
|
#logger;
|
|
237
457
|
#settings;
|
|
238
458
|
#sleepers = /* @__PURE__ */ new Set();
|
|
@@ -260,6 +480,7 @@ var KafkaTransport = class {
|
|
|
260
480
|
this.#producerConfig = options.producerConfig ?? {};
|
|
261
481
|
this.#consumerConfig = options.consumerConfig ?? {};
|
|
262
482
|
this.#invalidPolicy = options.onInvalidMessage ?? "dead-letter";
|
|
483
|
+
this.#decode = options.decode;
|
|
263
484
|
this.#logger = options.logger ?? console;
|
|
264
485
|
this.#settings = resolveSettings(options);
|
|
265
486
|
}
|
|
@@ -441,16 +662,13 @@ var KafkaTransport = class {
|
|
|
441
662
|
async #handleMessage(payload, onMessage) {
|
|
442
663
|
let envelope;
|
|
443
664
|
try {
|
|
444
|
-
envelope =
|
|
665
|
+
envelope = this.#decodeRecord(payload);
|
|
445
666
|
} catch (error) {
|
|
446
|
-
if (!(error instanceof InvalidKafkaMessageError)) {
|
|
667
|
+
if (this.#decode === void 0 && !(error instanceof InvalidKafkaMessageError)) {
|
|
447
668
|
throw toHandlerFailure(error);
|
|
448
669
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
"handleInvalid",
|
|
452
|
-
() => this.#handleInvalid(payload, error.message)
|
|
453
|
-
);
|
|
670
|
+
const reason = toErrorInfo(error).message;
|
|
671
|
+
await this.#withRetries(payload, "handleInvalid", () => this.#handleInvalid(payload, reason));
|
|
454
672
|
return;
|
|
455
673
|
}
|
|
456
674
|
await this.#withRetries(payload, "deliver", async () => {
|
|
@@ -464,6 +682,21 @@ var KafkaTransport = class {
|
|
|
464
682
|
}
|
|
465
683
|
});
|
|
466
684
|
}
|
|
685
|
+
#decodeRecord({ topic, partition, message }) {
|
|
686
|
+
if (this.#decode === void 0) {
|
|
687
|
+
return decodeEnvelope(message);
|
|
688
|
+
}
|
|
689
|
+
const record = {
|
|
690
|
+
topic,
|
|
691
|
+
partition,
|
|
692
|
+
offset: message.offset,
|
|
693
|
+
key: message.key,
|
|
694
|
+
value: message.value,
|
|
695
|
+
headers: message.headers,
|
|
696
|
+
timestamp: message.timestamp
|
|
697
|
+
};
|
|
698
|
+
return completeDecodedEvent(this.#decode(record), record);
|
|
699
|
+
}
|
|
467
700
|
async #handleInvalid(payload, reason) {
|
|
468
701
|
const { topic, partition, message } = payload;
|
|
469
702
|
const location = `topic=${topic} partition=${partition} offset=${message.offset}`;
|
|
@@ -705,7 +938,34 @@ function toHandlerFailure(error) {
|
|
|
705
938
|
const { name, message } = toErrorInfo(error);
|
|
706
939
|
return new HandlerFailedError(`${name}: ${message}`);
|
|
707
940
|
}
|
|
941
|
+
function fromHeader(name) {
|
|
942
|
+
if (name.length === 0) {
|
|
943
|
+
throw new ConfigurationError("fromHeader needs a header name");
|
|
944
|
+
}
|
|
945
|
+
return (message) => Object.hasOwn(message.headers, name) ? message.headers[name] : void 0;
|
|
946
|
+
}
|
|
947
|
+
function fromPayloadField(path) {
|
|
948
|
+
const segments = path.split(".");
|
|
949
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
950
|
+
throw new ConfigurationError(
|
|
951
|
+
`fromPayloadField needs a dotted path of non-empty names, received '${path}'`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
return (message) => {
|
|
955
|
+
let value = message.payload;
|
|
956
|
+
for (const segment of segments) {
|
|
957
|
+
if (typeof value !== "object" || value === null || !Object.hasOwn(value, segment)) {
|
|
958
|
+
return void 0;
|
|
959
|
+
}
|
|
960
|
+
value = value[segment];
|
|
961
|
+
}
|
|
962
|
+
if (typeof value === "string") {
|
|
963
|
+
return value;
|
|
964
|
+
}
|
|
965
|
+
return Number.isSafeInteger(value) ? String(value) : void 0;
|
|
966
|
+
};
|
|
967
|
+
}
|
|
708
968
|
|
|
709
|
-
export { HandlerFailedError, InvalidKafkaMessageError, KafkaTransport, PublishTimeoutError, UnencodableEventError, decodeEnvelope, encodeEnvelope };
|
|
969
|
+
export { HandlerFailedError, InvalidKafkaMessageError, KafkaTransport, PublishTimeoutError, UnencodableEventError, decodeCloudEvent, decodeEnvelope, encodeEnvelope, fromHeader, fromPayloadField, plainJsonDecoder };
|
|
710
970
|
//# sourceMappingURL=index.js.map
|
|
711
971
|
//# sourceMappingURL=index.js.map
|