youeduc-sdk-messaging 0.0.0 → 0.5.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/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/admin.d.ts +61 -0
- package/dist/admin.js +143 -0
- package/dist/admin.js.map +1 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.js +128 -0
- package/dist/config.js.map +1 -0
- package/dist/consumer-client.d.ts +155 -0
- package/dist/consumer-client.js +299 -0
- package/dist/consumer-client.js.map +1 -0
- package/dist/consumer.d.ts +156 -0
- package/dist/consumer.js +1228 -0
- package/dist/consumer.js.map +1 -0
- package/dist/contracts/index.d.ts +16 -0
- package/dist/contracts/index.js +42 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/schemas.d.ts +570 -0
- package/dist/contracts/schemas.js +902 -0
- package/dist/contracts/schemas.js.map +1 -0
- package/dist/contracts/usuarios.d.ts +163 -0
- package/dist/contracts/usuarios.js +5 -0
- package/dist/contracts/usuarios.js.map +1 -0
- package/dist/dedup.d.ts +29 -0
- package/dist/dedup.js +65 -0
- package/dist/dedup.js.map +1 -0
- package/dist/dlq.d.ts +161 -0
- package/dist/dlq.js +341 -0
- package/dist/dlq.js.map +1 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.js +95 -0
- package/dist/errors.js.map +1 -0
- package/dist/headers.d.ts +92 -0
- package/dist/headers.js +144 -0
- package/dist/headers.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/redis.d.ts +118 -0
- package/dist/integrations/redis.js +272 -0
- package/dist/integrations/redis.js.map +1 -0
- package/dist/integrations/runtime.d.ts +232 -0
- package/dist/integrations/runtime.js +554 -0
- package/dist/integrations/runtime.js.map +1 -0
- package/dist/logger.d.ts +37 -0
- package/dist/logger.js +49 -0
- package/dist/logger.js.map +1 -0
- package/dist/messaging.d.ts +145 -0
- package/dist/messaging.js +164 -0
- package/dist/messaging.js.map +1 -0
- package/dist/naming.d.ts +64 -0
- package/dist/naming.js +167 -0
- package/dist/naming.js.map +1 -0
- package/dist/payload.d.ts +8 -0
- package/dist/payload.js +32 -0
- package/dist/payload.js.map +1 -0
- package/dist/publisher.d.ts +50 -0
- package/dist/publisher.js +199 -0
- package/dist/publisher.js.map +1 -0
- package/dist/retry.d.ts +24 -0
- package/dist/retry.js +42 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts +83 -0
- package/dist/schema.js +676 -0
- package/dist/schema.js.map +1 -0
- package/dist/telemetry.d.ts +52 -0
- package/dist/telemetry.js +221 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/transport.d.ts +130 -0
- package/dist/transport.js +283 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.js +35 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +38 -0
- package/dist/util.js +112 -0
- package/dist/util.js.map +1 -0
- package/dist/w3c.d.ts +16 -0
- package/dist/w3c.js +119 -0
- package/dist/w3c.js.map +1 -0
- package/package.json +74 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kafka client contract used by {@link Consumer} (`consumer.ts`) and its default implementation
|
|
3
|
+
* over the callback (librdkafka) API of `@confluentinc/kafka-javascript` (`KafkaConsumer`).
|
|
4
|
+
*
|
|
5
|
+
* Why the callback API and not `KafkaJS.Consumer.run()`: `run()` owns the poll loop and swallows
|
|
6
|
+
* fetch errors (it always restarts, so fatal poll errors could never stop the consumer), applies
|
|
7
|
+
* `seek`/`pause`/`resume` only as deferred "pending operations" at worker barriers (no
|
|
8
|
+
* synchronous HOLD at a record boundary) and defers `unassign` on revoke until its workers stop.
|
|
9
|
+
* The callback API gives the primitives of aiokafka's `getmany` model that the Python SDK uses:
|
|
10
|
+
* `consume(n)` batches, explicit `commit`, `seek`, `pause`/`resume`, `committed` and a
|
|
11
|
+
* `rebalance_cb` whose `assign`/`unassign` the application calls itself.
|
|
12
|
+
*
|
|
13
|
+
* Guarantees of {@link ConfluentConsumerClient} (what makes the at-least-once model hold):
|
|
14
|
+
*
|
|
15
|
+
* - **Assignment never changes during a `consume()`**: librdkafka invokes `rebalance_cb` from
|
|
16
|
+
* inside `consume()` (worker thread) and the binding only enqueues the event to JS; the adapter
|
|
17
|
+
* applies it (listener + `assign`/`unassign`) only after the in-flight `consume()` returned,
|
|
18
|
+
* and `poll()` does not start a new `consume()` while a rebalance is being applied. Every batch
|
|
19
|
+
* therefore comes from a single assignment, contiguous with the fetch positions.
|
|
20
|
+
* - **Revoke**: `listener.onPartitionsRevoked` is awaited **before** `unassign()` (the consumer
|
|
21
|
+
* drains at the next record boundary and commits there, like aiokafka's listener). A revoke
|
|
22
|
+
* served during a `consume()` that already collected records runs right after it returns, so
|
|
23
|
+
* the listener may finish before the consumer takes that batch (and before `unassign()`): the
|
|
24
|
+
* consumer then drops the revoked partitions' records of that batch itself.
|
|
25
|
+
* - **Assign**: `listener.onPartitionsAssigned` is awaited **before** `assign()`; a `seekToEnd`
|
|
26
|
+
* issued for those partitions during the callback becomes their start offset (`OFFSET_END`),
|
|
27
|
+
* so the `latest` reset never races a fetch. Partitions start resumed (aiokafka semantics).
|
|
28
|
+
* - **Eager protocol only** (`partition.assignment.strategy=roundrobin`, set by the consumer):
|
|
29
|
+
* a revoke always covers the whole assignment, so draining every partition is correct.
|
|
30
|
+
* - **Commits**: `commit()` queues an explicit-offset commit (`commitCb`); the broker result is
|
|
31
|
+
* reported asynchronously through `offset_commit_cb` → `hooks.onCommitFailed` (a failed commit
|
|
32
|
+
* only means redelivery). librdkafka's `close()` waits for outstanding commits.
|
|
33
|
+
* - **Close**: during `disconnect()` a revoke from librdkafka's close is applied immediately.
|
|
34
|
+
*/
|
|
35
|
+
import type { ClientConfig } from './config.js';
|
|
36
|
+
import type { RawHeaders } from './headers.js';
|
|
37
|
+
export interface TopicPartition {
|
|
38
|
+
readonly topic: string;
|
|
39
|
+
readonly partition: number;
|
|
40
|
+
}
|
|
41
|
+
export interface TopicPartitionOffset extends TopicPartition {
|
|
42
|
+
readonly offset: number;
|
|
43
|
+
}
|
|
44
|
+
/** A fetched record (key/value bytes untouched, headers as received). */
|
|
45
|
+
export interface ConsumerRecord {
|
|
46
|
+
readonly topic: string;
|
|
47
|
+
readonly partition: number;
|
|
48
|
+
readonly offset: number;
|
|
49
|
+
readonly key: Buffer | null;
|
|
50
|
+
readonly value: Buffer | null;
|
|
51
|
+
readonly headers: RawHeaders;
|
|
52
|
+
}
|
|
53
|
+
/** Awaited by the client around assignment changes (aiokafka's `ConsumerRebalanceListener`). */
|
|
54
|
+
export interface RebalanceListener {
|
|
55
|
+
onPartitionsRevoked(partitions: TopicPartition[]): Promise<void>;
|
|
56
|
+
onPartitionsAssigned(partitions: TopicPartition[]): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export interface ConsumerClientHooks {
|
|
59
|
+
/** A commit accepted by `commit()` failed later at the broker (the record will be redelivered). */
|
|
60
|
+
onCommitFailed(error: unknown): void;
|
|
61
|
+
}
|
|
62
|
+
/** What the consumer needs from a Kafka client (the confluent adapter or a test fake). */
|
|
63
|
+
export interface KafkaConsumerClient {
|
|
64
|
+
connect(): Promise<void>;
|
|
65
|
+
/** After `connect()`. `pattern` is a regex source starting with `^`. */
|
|
66
|
+
subscribe(pattern: string, listener: RebalanceListener): void;
|
|
67
|
+
/**
|
|
68
|
+
* Up to `maxRecords` records, waiting up to `timeoutMs` for the first one. Errors reject.
|
|
69
|
+
* Records belong to the current assignment and are contiguous with the fetch position: a
|
|
70
|
+
* client never returns records fetched under a previous assignment of the partition.
|
|
71
|
+
*/
|
|
72
|
+
poll(options: {
|
|
73
|
+
maxRecords: number;
|
|
74
|
+
timeoutMs: number;
|
|
75
|
+
}): Promise<ConsumerRecord[]>;
|
|
76
|
+
/** Commits the given next offsets (`offset` = next record to read). */
|
|
77
|
+
commit(offsets: TopicPartitionOffset[]): Promise<void>;
|
|
78
|
+
/** Committed offset of the group, or `null` when there is none. */
|
|
79
|
+
committed(partition: TopicPartition): Promise<number | null>;
|
|
80
|
+
/** Next fetch of the partition starts at `offset`. Rejects when the partition is not assigned. */
|
|
81
|
+
seek(partition: TopicPartition, offset: number): Promise<void>;
|
|
82
|
+
seekToEnd(partitions: TopicPartition[]): Promise<void>;
|
|
83
|
+
/** Throws when a partition is not assigned. */
|
|
84
|
+
pause(partitions: TopicPartition[]): void;
|
|
85
|
+
/** Throws when a partition is not assigned. */
|
|
86
|
+
resume(partitions: TopicPartition[]): void;
|
|
87
|
+
assignment(): TopicPartition[];
|
|
88
|
+
/** Leaves the group and releases the client. */
|
|
89
|
+
disconnect(): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
/** Builds a client from the final librdkafka configuration (sync or async). */
|
|
92
|
+
export type ConsumerClientFactory = (config: ClientConfig, hooks: ConsumerClientHooks) => KafkaConsumerClient | Promise<KafkaConsumerClient>;
|
|
93
|
+
/** `topic:partition` (`:` never appears in a Kafka topic name). */
|
|
94
|
+
export declare function partitionKey(tp: TopicPartition): string;
|
|
95
|
+
/** A message as returned by `KafkaConsumer.consume()` of `@confluentinc/kafka-javascript`. */
|
|
96
|
+
export interface RdKafkaMessage {
|
|
97
|
+
topic: string;
|
|
98
|
+
partition: number;
|
|
99
|
+
offset: number;
|
|
100
|
+
key?: Buffer | string | null | undefined;
|
|
101
|
+
value: Buffer | null;
|
|
102
|
+
headers?: Array<Record<string, Buffer | string>> | undefined;
|
|
103
|
+
}
|
|
104
|
+
type ErrorCallback = (error?: unknown) => void;
|
|
105
|
+
/** Subset of the callback-based `KafkaConsumer` used by the adapter. */
|
|
106
|
+
export interface RdKafkaConsumerLike {
|
|
107
|
+
connect(metadataOptions: null, cb: ErrorCallback): unknown;
|
|
108
|
+
disconnect(cb: ErrorCallback): unknown;
|
|
109
|
+
isConnected(): boolean;
|
|
110
|
+
subscribe(topics: string[]): unknown;
|
|
111
|
+
consume(count: number, cb: (error: unknown, messages: RdKafkaMessage[]) => void): void;
|
|
112
|
+
setDefaultConsumeTimeout(timeoutMs: number): void;
|
|
113
|
+
setDefaultIsTimeoutOnlyForFirstMessage?(value: boolean): void;
|
|
114
|
+
commitCb(offsets: TopicPartitionOffset[], cb: ErrorCallback): unknown;
|
|
115
|
+
committed(partitions: TopicPartition[], timeoutMs: number, cb: (error: unknown, offsets: Array<{
|
|
116
|
+
offset?: number;
|
|
117
|
+
error?: unknown;
|
|
118
|
+
}>) => void): unknown;
|
|
119
|
+
seek(partition: TopicPartitionOffset, timeoutMs: number, cb: ErrorCallback): unknown;
|
|
120
|
+
pause(partitions: TopicPartition[]): unknown;
|
|
121
|
+
resume(partitions: TopicPartition[]): unknown;
|
|
122
|
+
assign(partitions: Array<TopicPartition | TopicPartitionOffset>): unknown;
|
|
123
|
+
unassign(): unknown;
|
|
124
|
+
incrementalAssign?(partitions: Array<TopicPartition | TopicPartitionOffset>): unknown;
|
|
125
|
+
incrementalUnassign?(partitions: TopicPartition[]): unknown;
|
|
126
|
+
rebalanceProtocol?(): string;
|
|
127
|
+
assignments(): TopicPartition[];
|
|
128
|
+
}
|
|
129
|
+
/** Creates the raw consumer from the configuration (with the adapter's callbacks added). */
|
|
130
|
+
export type RdKafkaConsumerConstructor = (config: Record<string, unknown>) => RdKafkaConsumerLike;
|
|
131
|
+
/** Adapter of the callback `KafkaConsumer` to {@link KafkaConsumerClient} (guarantees in the header). */
|
|
132
|
+
export declare class ConfluentConsumerClient implements KafkaConsumerClient {
|
|
133
|
+
#private;
|
|
134
|
+
constructor(config: ClientConfig, hooks: ConsumerClientHooks, create: RdKafkaConsumerConstructor);
|
|
135
|
+
connect(): Promise<void>;
|
|
136
|
+
subscribe(pattern: string, listener: RebalanceListener): void;
|
|
137
|
+
poll(options: {
|
|
138
|
+
maxRecords: number;
|
|
139
|
+
timeoutMs: number;
|
|
140
|
+
}): Promise<ConsumerRecord[]>;
|
|
141
|
+
commit(offsets: TopicPartitionOffset[]): Promise<void>;
|
|
142
|
+
committed(partition: TopicPartition): Promise<number | null>;
|
|
143
|
+
seek(partition: TopicPartition, offset: number): Promise<void>;
|
|
144
|
+
seekToEnd(partitions: TopicPartition[]): Promise<void>;
|
|
145
|
+
pause(partitions: TopicPartition[]): void;
|
|
146
|
+
resume(partitions: TopicPartition[]): void;
|
|
147
|
+
assignment(): TopicPartition[];
|
|
148
|
+
disconnect(): Promise<void>;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Default factory: the callback `KafkaConsumer` of `@confluentinc/kafka-javascript` wrapped by
|
|
152
|
+
* {@link ConfluentConsumerClient}. The native module is loaded on first use.
|
|
153
|
+
*/
|
|
154
|
+
export declare const createConfluentConsumer: ConsumerClientFactory;
|
|
155
|
+
export {};
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kafka client contract used by {@link Consumer} (`consumer.ts`) and its default implementation
|
|
3
|
+
* over the callback (librdkafka) API of `@confluentinc/kafka-javascript` (`KafkaConsumer`).
|
|
4
|
+
*
|
|
5
|
+
* Why the callback API and not `KafkaJS.Consumer.run()`: `run()` owns the poll loop and swallows
|
|
6
|
+
* fetch errors (it always restarts, so fatal poll errors could never stop the consumer), applies
|
|
7
|
+
* `seek`/`pause`/`resume` only as deferred "pending operations" at worker barriers (no
|
|
8
|
+
* synchronous HOLD at a record boundary) and defers `unassign` on revoke until its workers stop.
|
|
9
|
+
* The callback API gives the primitives of aiokafka's `getmany` model that the Python SDK uses:
|
|
10
|
+
* `consume(n)` batches, explicit `commit`, `seek`, `pause`/`resume`, `committed` and a
|
|
11
|
+
* `rebalance_cb` whose `assign`/`unassign` the application calls itself.
|
|
12
|
+
*
|
|
13
|
+
* Guarantees of {@link ConfluentConsumerClient} (what makes the at-least-once model hold):
|
|
14
|
+
*
|
|
15
|
+
* - **Assignment never changes during a `consume()`**: librdkafka invokes `rebalance_cb` from
|
|
16
|
+
* inside `consume()` (worker thread) and the binding only enqueues the event to JS; the adapter
|
|
17
|
+
* applies it (listener + `assign`/`unassign`) only after the in-flight `consume()` returned,
|
|
18
|
+
* and `poll()` does not start a new `consume()` while a rebalance is being applied. Every batch
|
|
19
|
+
* therefore comes from a single assignment, contiguous with the fetch positions.
|
|
20
|
+
* - **Revoke**: `listener.onPartitionsRevoked` is awaited **before** `unassign()` (the consumer
|
|
21
|
+
* drains at the next record boundary and commits there, like aiokafka's listener). A revoke
|
|
22
|
+
* served during a `consume()` that already collected records runs right after it returns, so
|
|
23
|
+
* the listener may finish before the consumer takes that batch (and before `unassign()`): the
|
|
24
|
+
* consumer then drops the revoked partitions' records of that batch itself.
|
|
25
|
+
* - **Assign**: `listener.onPartitionsAssigned` is awaited **before** `assign()`; a `seekToEnd`
|
|
26
|
+
* issued for those partitions during the callback becomes their start offset (`OFFSET_END`),
|
|
27
|
+
* so the `latest` reset never races a fetch. Partitions start resumed (aiokafka semantics).
|
|
28
|
+
* - **Eager protocol only** (`partition.assignment.strategy=roundrobin`, set by the consumer):
|
|
29
|
+
* a revoke always covers the whole assignment, so draining every partition is correct.
|
|
30
|
+
* - **Commits**: `commit()` queues an explicit-offset commit (`commitCb`); the broker result is
|
|
31
|
+
* reported asynchronously through `offset_commit_cb` → `hooks.onCommitFailed` (a failed commit
|
|
32
|
+
* only means redelivery). librdkafka's `close()` waits for outstanding commits.
|
|
33
|
+
* - **Close**: during `disconnect()` a revoke from librdkafka's close is applied immediately.
|
|
34
|
+
*/
|
|
35
|
+
/** `topic:partition` (`:` never appears in a Kafka topic name). */
|
|
36
|
+
export function partitionKey(tp) {
|
|
37
|
+
return `${tp.topic}:${tp.partition}`;
|
|
38
|
+
}
|
|
39
|
+
// librdkafka constants (rdkafka.h).
|
|
40
|
+
const ERR__ASSIGN_PARTITIONS = -175;
|
|
41
|
+
const ERR__REVOKE_PARTITIONS = -174;
|
|
42
|
+
const ERR__NO_OFFSET = -168;
|
|
43
|
+
const OFFSET_END = -1;
|
|
44
|
+
const SEEK_TIMEOUT_MS = 5_000;
|
|
45
|
+
const COMMITTED_TIMEOUT_MS = 10_000;
|
|
46
|
+
function plain(tp) {
|
|
47
|
+
return { topic: tp.topic, partition: tp.partition };
|
|
48
|
+
}
|
|
49
|
+
function errorCode(error) {
|
|
50
|
+
const code = error?.code;
|
|
51
|
+
return typeof code === 'number' ? code : undefined;
|
|
52
|
+
}
|
|
53
|
+
function toRecord(message) {
|
|
54
|
+
const key = message.key;
|
|
55
|
+
const headers = [];
|
|
56
|
+
for (const header of message.headers ?? []) {
|
|
57
|
+
for (const [name, value] of Object.entries(header))
|
|
58
|
+
headers.push([name, value]);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
topic: message.topic,
|
|
62
|
+
partition: message.partition,
|
|
63
|
+
offset: message.offset,
|
|
64
|
+
key: key === null || key === undefined ? null : typeof key === 'string' ? Buffer.from(key, 'utf8') : key,
|
|
65
|
+
value: message.value ?? null,
|
|
66
|
+
headers,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Adapter of the callback `KafkaConsumer` to {@link KafkaConsumerClient} (guarantees in the header). */
|
|
70
|
+
export class ConfluentConsumerClient {
|
|
71
|
+
#raw;
|
|
72
|
+
#hooks;
|
|
73
|
+
#listener = null;
|
|
74
|
+
/** Settles when the in-flight `consume()` returned (never rejects). */
|
|
75
|
+
#consuming = null;
|
|
76
|
+
/** Serialized rebalance applications. */
|
|
77
|
+
#rebalanceTail = Promise.resolve();
|
|
78
|
+
#closing = false;
|
|
79
|
+
/** Partitions being assigned (inside `onPartitionsAssigned`) and their requested start offsets. */
|
|
80
|
+
#assigning = null;
|
|
81
|
+
constructor(config, hooks, create) {
|
|
82
|
+
this.#hooks = hooks;
|
|
83
|
+
this.#raw = create({
|
|
84
|
+
...config,
|
|
85
|
+
rebalance_cb: (error, assignment) => this.#onRebalance(error, assignment ?? []),
|
|
86
|
+
offset_commit_cb: (error) => this.#onOffsetCommit(error),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
connect() {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
this.#raw.connect(null, (error) => (error ? reject(error) : resolve()));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
subscribe(pattern, listener) {
|
|
95
|
+
this.#listener = listener;
|
|
96
|
+
this.#raw.subscribe([pattern]);
|
|
97
|
+
}
|
|
98
|
+
async poll(options) {
|
|
99
|
+
// Never consume while a rebalance is pending or being applied.
|
|
100
|
+
for (;;) {
|
|
101
|
+
const tail = this.#rebalanceTail;
|
|
102
|
+
await tail;
|
|
103
|
+
if (tail === this.#rebalanceTail)
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
// A poll abandoned by a cancelled loop must not consume on a closing client.
|
|
107
|
+
if (this.#closing)
|
|
108
|
+
return [];
|
|
109
|
+
let done = () => undefined;
|
|
110
|
+
this.#consuming = new Promise((resolve) => {
|
|
111
|
+
done = resolve;
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
const messages = await new Promise((resolve, reject) => {
|
|
115
|
+
try {
|
|
116
|
+
this.#raw.setDefaultConsumeTimeout(options.timeoutMs);
|
|
117
|
+
this.#raw.setDefaultIsTimeoutOnlyForFirstMessage?.(true);
|
|
118
|
+
this.#raw.consume(options.maxRecords, (error, batch) => error ? reject(error) : resolve(batch ?? []));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
reject(error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return messages.map(toRecord);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
this.#consuming = null;
|
|
128
|
+
done();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
commit(offsets) {
|
|
132
|
+
if (offsets.length === 0)
|
|
133
|
+
return Promise.resolve();
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
try {
|
|
136
|
+
this.#raw.commitCb(offsets.map((o) => ({ topic: o.topic, partition: o.partition, offset: o.offset })), (error) => (error ? reject(error) : resolve()));
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
reject(error);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
committed(partition) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
try {
|
|
146
|
+
this.#raw.committed([plain(partition)], COMMITTED_TIMEOUT_MS, (error, offsets) => {
|
|
147
|
+
const entry = offsets?.[0];
|
|
148
|
+
if (error)
|
|
149
|
+
reject(error);
|
|
150
|
+
else if (entry?.error)
|
|
151
|
+
reject(entry.error);
|
|
152
|
+
else
|
|
153
|
+
resolve(typeof entry?.offset === 'number' && entry.offset >= 0 ? entry.offset : null);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
reject(error);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
seek(partition, offset) {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
try {
|
|
164
|
+
this.#raw.seek({ ...plain(partition), offset }, SEEK_TIMEOUT_MS, (error) => error ? reject(error) : resolve());
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
reject(error);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async seekToEnd(partitions) {
|
|
172
|
+
const assigning = this.#assigning;
|
|
173
|
+
const direct = [];
|
|
174
|
+
for (const tp of partitions) {
|
|
175
|
+
const key = partitionKey(tp);
|
|
176
|
+
// Not assigned yet: becomes the start offset passed to assign() (no fetch race).
|
|
177
|
+
if (assigning?.keys.has(key))
|
|
178
|
+
assigning.starts.set(key, OFFSET_END);
|
|
179
|
+
else
|
|
180
|
+
direct.push(tp);
|
|
181
|
+
}
|
|
182
|
+
await Promise.all(direct.map((tp) => this.seek(tp, OFFSET_END)));
|
|
183
|
+
}
|
|
184
|
+
pause(partitions) {
|
|
185
|
+
this.#raw.pause(partitions.map(plain));
|
|
186
|
+
}
|
|
187
|
+
resume(partitions) {
|
|
188
|
+
this.#raw.resume(partitions.map(plain));
|
|
189
|
+
}
|
|
190
|
+
assignment() {
|
|
191
|
+
try {
|
|
192
|
+
return this.#raw.assignments().map(plain);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return []; // not connected
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async disconnect() {
|
|
199
|
+
this.#closing = true;
|
|
200
|
+
await this.#rebalanceTail;
|
|
201
|
+
if (!this.#raw.isConnected())
|
|
202
|
+
return;
|
|
203
|
+
await new Promise((resolve) => {
|
|
204
|
+
this.#raw.disconnect(() => resolve());
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------- rebalance
|
|
208
|
+
#onRebalance(error, assignment) {
|
|
209
|
+
const code = errorCode(error);
|
|
210
|
+
if (code !== ERR__ASSIGN_PARTITIONS && code !== ERR__REVOKE_PARTITIONS)
|
|
211
|
+
return;
|
|
212
|
+
const partitions = assignment.map(plain);
|
|
213
|
+
if (this.#closing) {
|
|
214
|
+
// librdkafka's close() waits for this (un)assign: apply it now.
|
|
215
|
+
this.#applyAssignment(code, partitions, undefined);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.#rebalanceTail = this.#rebalanceTail
|
|
219
|
+
.then(async () => {
|
|
220
|
+
while (this.#consuming !== null)
|
|
221
|
+
await this.#consuming;
|
|
222
|
+
if (this.#closing)
|
|
223
|
+
this.#applyAssignment(code, partitions, undefined);
|
|
224
|
+
else
|
|
225
|
+
await this.#handleRebalance(code, partitions);
|
|
226
|
+
})
|
|
227
|
+
.catch(() => undefined);
|
|
228
|
+
}
|
|
229
|
+
async #handleRebalance(code, partitions) {
|
|
230
|
+
const listener = this.#listener;
|
|
231
|
+
if (code === ERR__REVOKE_PARTITIONS) {
|
|
232
|
+
try {
|
|
233
|
+
await listener?.onPartitionsRevoked(partitions);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// The listener is the consumer's own code and never throws; unassign regardless.
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
this.#applyAssignment(code, partitions, undefined);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const starts = new Map();
|
|
244
|
+
this.#assigning = { keys: new Set(partitions.map(partitionKey)), starts };
|
|
245
|
+
try {
|
|
246
|
+
await listener?.onPartitionsAssigned(partitions);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Same as above: always complete the assignment.
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
this.#assigning = null;
|
|
253
|
+
this.#applyAssignment(code, partitions, starts);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
#applyAssignment(code, partitions, starts) {
|
|
257
|
+
try {
|
|
258
|
+
const cooperative = this.#raw.rebalanceProtocol?.() === 'COOPERATIVE';
|
|
259
|
+
if (code === ERR__ASSIGN_PARTITIONS) {
|
|
260
|
+
const list = partitions.map((tp) => {
|
|
261
|
+
const offset = starts?.get(partitionKey(tp));
|
|
262
|
+
return offset === undefined ? plain(tp) : { ...plain(tp), offset };
|
|
263
|
+
});
|
|
264
|
+
if (cooperative && this.#raw.incrementalAssign)
|
|
265
|
+
this.#raw.incrementalAssign(list);
|
|
266
|
+
else
|
|
267
|
+
this.#raw.assign(list);
|
|
268
|
+
// Newly assigned partitions start resumed (a pause from a previous ownership must not stick).
|
|
269
|
+
if (partitions.length > 0)
|
|
270
|
+
this.#raw.resume(partitions);
|
|
271
|
+
}
|
|
272
|
+
else if (cooperative && this.#raw.incrementalUnassign) {
|
|
273
|
+
this.#raw.incrementalUnassign(partitions);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
this.#raw.unassign();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Not connected anymore: nothing to (un)assign.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
#onOffsetCommit(error) {
|
|
284
|
+
if (error === null || error === undefined)
|
|
285
|
+
return;
|
|
286
|
+
if (errorCode(error) === ERR__NO_OFFSET)
|
|
287
|
+
return;
|
|
288
|
+
this.#hooks.onCommitFailed(error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Default factory: the callback `KafkaConsumer` of `@confluentinc/kafka-javascript` wrapped by
|
|
293
|
+
* {@link ConfluentConsumerClient}. The native module is loaded on first use.
|
|
294
|
+
*/
|
|
295
|
+
export const createConfluentConsumer = async (config, hooks) => {
|
|
296
|
+
const kafka = (await import('@confluentinc/kafka-javascript')).default;
|
|
297
|
+
return new ConfluentConsumerClient(config, hooks, (conf) => new kafka.KafkaConsumer(conf));
|
|
298
|
+
};
|
|
299
|
+
//# sourceMappingURL=consumer-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumer-client.js","sourceRoot":"","sources":["../src/consumer-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kafka consumer with retry through the group's retry topics, DLQ, deduplication and manual
|
|
3
|
+
* commits (spec §5). Mirrors Python's `consumer.py` step by step.
|
|
4
|
+
*
|
|
5
|
+
* Topology (per consumer group):
|
|
6
|
+
*
|
|
7
|
+
* {domain}.events.{version} main topic (shared)
|
|
8
|
+
* {main}.{group}.retry.{30s,5m,1h,12h} group retry topics (read back by the group)
|
|
9
|
+
* {main}.{group}.dlq group DLQ
|
|
10
|
+
*
|
|
11
|
+
* With `resourcePrefix` (spec §2.7) the main topic becomes `{prefix}.{domain}.events.{version}` and
|
|
12
|
+
* the group id `{prefix}.{group}`; retry and DLQ derive from the prefixed main topic and the bare
|
|
13
|
+
* group. Telemetry, logs and the getters (`group`, `mainTopic`...) show the full names.
|
|
14
|
+
*
|
|
15
|
+
* The group subscribes by regex (`naming.retryTopicPattern`) to the main topic and to **every**
|
|
16
|
+
* retry topic of the group, including delays that left the `RetryPolicy`. A retry is processed
|
|
17
|
+
* straight from its retry topic when due (`x-retry-due-at`); it is never republished to the main
|
|
18
|
+
* topic. The next attempt always follows the CURRENT policy, by `x-retry-count`.
|
|
19
|
+
*
|
|
20
|
+
* Kafka client: the callback (librdkafka) API of `@confluentinc/kafka-javascript`, wrapped by
|
|
21
|
+
* `ConfluentConsumerClient` (`consumer-client.ts`, which documents why `KafkaJS.Consumer.run()`
|
|
22
|
+
* cannot implement §5 and how rebalances are serialized with `consume()`). Mapping to the spec:
|
|
23
|
+
*
|
|
24
|
+
* - §5.1 config: `enable.auto.commit=false`, `enable.auto.offset.store=false`,
|
|
25
|
+
* `auto.offset.reset=earliest` (always; the user's `latest` is applied on assignment, §5.7),
|
|
26
|
+
* `max.poll.interval.ms` (also librdkafka's rebalance timeout), eager `roundrobin` assignor.
|
|
27
|
+
* - Poll loop = Python's `getmany` loop: `client.poll({maxRecords, timeoutMs})` → batch grouped
|
|
28
|
+
* by partition → processed under the batch lock: partitions concurrently
|
|
29
|
+
* (`Promise.allSettled`), records of a partition sequentially by offset (§5.4).
|
|
30
|
+
* - Each record ends in ADVANCE (final outcome, or retry/DLQ send acknowledged) or HOLD. HOLD =
|
|
31
|
+
* `pause` + `seek(offset)` + resume timer (§5.4); the rest of the partition's batch is skipped.
|
|
32
|
+
* A seek that fails while the partition is still ours is retried before resuming, so a
|
|
33
|
+
* partition never resumes from an unknown position.
|
|
34
|
+
* - End of batch: commit `last contiguous ADVANCE + 1` per partition, only for partitions still
|
|
35
|
+
* assigned (`offset_commit_skipped_unassigned`); failure → `offset_commit_failed` (§5.4).
|
|
36
|
+
* - Not-due retry (§5.5): HOLD for `min(due − now, topic delay)` (10 ms floor), nothing recorded.
|
|
37
|
+
* - Routing failure (§5.6): retriable → pending route cache (target + headers) + HOLD for
|
|
38
|
+
* `routingFailureBackoffMs`, resent without re-running the handler; non-retriable retry send →
|
|
39
|
+
* DLQ `routing_failed` with the handler's error; non-retriable DLQ send → one shrunk attempt
|
|
40
|
+
* (`x-last-error` ≤ 256 code points, no trace context, nothing injected) → then fatal.
|
|
41
|
+
* - Dedup (§8): checked before the payload is decoded, marked only after handler success.
|
|
42
|
+
* - Processing timeout (§7): the handler's `AbortSignal` is aborted at `processingTimeoutMs`,
|
|
43
|
+
* then it gets the same time again; still running → `message_handler_abandoned`, detached.
|
|
44
|
+
* - Lifecycle (§5.7): `stop()` drains at the record boundary, commits and closes; after
|
|
45
|
+
* `drainTimeoutMs` the loop is cancelled (every await of the pipeline is raced against the
|
|
46
|
+
* cancellation; handlers are aborted) and the unfinished offset is not committed. Revoke asks
|
|
47
|
+
* the partitions to stop at the next record boundary and waits for the batch lock (the batch
|
|
48
|
+
* commits what it finished), then drops timers/pending routes of the revoked partitions.
|
|
49
|
+
* `latest`: main-topic partitions without a committed offset move to the end when assigned (the
|
|
50
|
+
* confluent client turns it into the assign start offset); records fetched before that reset
|
|
51
|
+
* are dropped. The reset serial is read right after each poll returns (as in Python, after
|
|
52
|
+
* `getmany`), so a record fetched after the reset is never dropped (§5.7).
|
|
53
|
+
* Fatal poll errors (codes 29, 30, 31, 35, 58, authentication, fatal/stopped client, or any
|
|
54
|
+
* non-Kafka error) stop the consumer and `wait()` rethrows; others log `consumer_poll_failed`
|
|
55
|
+
* and wait `routingFailureBackoffMs`.
|
|
56
|
+
*
|
|
57
|
+
* A handler may run more than once for the same message (crash before commit, failed commit,
|
|
58
|
+
* rebalance): handlers must be idempotent or use a `DuplicateChecker`. A record may take up to
|
|
59
|
+
* `2 × processingTimeoutMs`.
|
|
60
|
+
*/
|
|
61
|
+
import type { ClientConfig, KafkaConfig } from './config.js';
|
|
62
|
+
import { type ConsumerClientFactory, type TopicPartition } from './consumer-client.js';
|
|
63
|
+
import { type Logger } from './logger.js';
|
|
64
|
+
import { RetryPolicy } from './retry.js';
|
|
65
|
+
import { type DuplicateChecker, type MessageHandler, type MessageSender, type PayloadValidator, type Telemetry } from './types.js';
|
|
66
|
+
export type AutoOffsetReset = 'earliest' | 'latest';
|
|
67
|
+
/** `event_type` → async handler (plain object or `Map`). */
|
|
68
|
+
export type ConsumerHandlers = Readonly<Record<string, MessageHandler>> | ReadonlyMap<string, MessageHandler>;
|
|
69
|
+
export interface ConsumerOptions {
|
|
70
|
+
/** Consumer group (kebab-case); also the Kafka `group.id`. */
|
|
71
|
+
group: string;
|
|
72
|
+
domain: string;
|
|
73
|
+
version: string;
|
|
74
|
+
/** Non-empty map `event_type` → `async` handler. Event types without a handler are skipped. */
|
|
75
|
+
handlers: ConsumerHandlers;
|
|
76
|
+
validator: PayloadValidator;
|
|
77
|
+
/** Producer used to route retries/DLQ (the facade injects the shared one). */
|
|
78
|
+
sender: MessageSender;
|
|
79
|
+
/** Default `new RetryPolicy()`. */
|
|
80
|
+
retryPolicy?: RetryPolicy | undefined;
|
|
81
|
+
/** Default: no deduplication. */
|
|
82
|
+
duplicateChecker?: DuplicateChecker | null | undefined;
|
|
83
|
+
/** Default {@link NoopTelemetry}. */
|
|
84
|
+
telemetry?: Telemetry | undefined;
|
|
85
|
+
/** Finite `> 0` and `2 × processingTimeoutMs < maxPollIntervalMs`. Default: no timeout. */
|
|
86
|
+
processingTimeoutMs?: number | null | undefined;
|
|
87
|
+
/** Default 100. */
|
|
88
|
+
maxPollRecords?: number | undefined;
|
|
89
|
+
/** Default 300 000 (also the rebalance timeout). */
|
|
90
|
+
maxPollIntervalMs?: number | undefined;
|
|
91
|
+
/** Default 1000. */
|
|
92
|
+
pollTimeoutMs?: number | undefined;
|
|
93
|
+
/** HOLD after a routing failure, and wait after a non-fatal poll error. Default 5000. */
|
|
94
|
+
routingFailureBackoffMs?: number | undefined;
|
|
95
|
+
/** Default `earliest`; `latest` applies only to main-topic partitions without a committed offset. */
|
|
96
|
+
autoOffsetReset?: AutoOffsetReset | undefined;
|
|
97
|
+
/** Kafka client factory (tests inject fakes). Default {@link createConfluentConsumer}. */
|
|
98
|
+
consumerFactory?: ConsumerClientFactory | undefined;
|
|
99
|
+
/** Clock in epoch milliseconds (`x-retry-due-at`, `x-first-failed-at`). Default `Date.now`. */
|
|
100
|
+
clock?: (() => number) | undefined;
|
|
101
|
+
/** Default: the SDK default logger. */
|
|
102
|
+
logger?: Logger | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Resource prefix (spec §2.7): main topic `{prefix}.{domain}.events.{version}`, group id
|
|
105
|
+
* `{prefix}.{group}`; retry/DLQ derive from the prefixed main topic and the bare group.
|
|
106
|
+
* Default: none.
|
|
107
|
+
*/
|
|
108
|
+
resourcePrefix?: string | null | undefined;
|
|
109
|
+
}
|
|
110
|
+
/** Whether a poll error stops the consumer: any non-Kafka error, a fatal code or `isFatal`. */
|
|
111
|
+
export declare function isFatalPollError(error: unknown): boolean;
|
|
112
|
+
/** @internal Test seam (not exported by the package index): mirrors the private state Python tests read. */
|
|
113
|
+
export interface ConsumerInternals {
|
|
114
|
+
resumeTimers(): ReadonlyMap<string, {
|
|
115
|
+
readonly delayMs: number;
|
|
116
|
+
readonly cancelled: boolean;
|
|
117
|
+
}>;
|
|
118
|
+
pendingRoutes(): ReadonlyMap<string, unknown>;
|
|
119
|
+
resumePartition(tp: TopicPartition): Promise<void>;
|
|
120
|
+
setHandler(eventType: string, handler: unknown): void;
|
|
121
|
+
}
|
|
122
|
+
/** @internal */
|
|
123
|
+
export declare function consumerInternals(consumer: Consumer): ConsumerInternals;
|
|
124
|
+
/**
|
|
125
|
+
* Consumes the domain's main topic and the group's retry topics.
|
|
126
|
+
*
|
|
127
|
+
* Handler contract: resolving = success; rejecting with `PermanentProcessingError` = DLQ; any
|
|
128
|
+
* other rejection (including the processing timeout) = retry, until `retryPolicy` is exhausted.
|
|
129
|
+
*/
|
|
130
|
+
export declare class Consumer {
|
|
131
|
+
#private;
|
|
132
|
+
constructor(config: KafkaConfig, options: ConsumerOptions);
|
|
133
|
+
get mainTopic(): string;
|
|
134
|
+
/** Retry topics of the current policy (targets of new retries). */
|
|
135
|
+
get retryTopics(): string[];
|
|
136
|
+
/** Subscribed regex (source): main topic + every retry topic of the group. */
|
|
137
|
+
get subscriptionPattern(): string;
|
|
138
|
+
get dlqTopic(): string;
|
|
139
|
+
/** Effective Kafka group id (with the resource prefix, if any). */
|
|
140
|
+
get group(): string;
|
|
141
|
+
get resourcePrefix(): string | null;
|
|
142
|
+
get running(): boolean;
|
|
143
|
+
/** Final librdkafka configuration handed to the client factory. */
|
|
144
|
+
consumerConfig(): ClientConfig;
|
|
145
|
+
start(): Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* Graceful shutdown: the in-flight record of each partition finishes and the progress is
|
|
148
|
+
* committed. After `drainTimeoutMs` the loop is cancelled (the in-flight handler is aborted)
|
|
149
|
+
* and the unfinished offset is not committed: it will be redelivered. Idempotent.
|
|
150
|
+
*/
|
|
151
|
+
stop(options?: {
|
|
152
|
+
drainTimeoutMs?: number;
|
|
153
|
+
}): Promise<void>;
|
|
154
|
+
/** Waits for the loop to end; rethrows the fatal error that stopped it, if any. */
|
|
155
|
+
wait(): Promise<void>;
|
|
156
|
+
}
|