@drarzter/kafka-client 0.9.4 → 0.11.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 +693 -8
- package/dist/chunk-OR7TPAAE.mjs +4760 -0
- package/dist/chunk-OR7TPAAE.mjs.map +1 -0
- package/dist/chunk-PQVBRDNV.mjs +149 -0
- package/dist/chunk-PQVBRDNV.mjs.map +1 -0
- package/dist/cli/dlq.d.ts +119 -0
- package/dist/cli/dlq.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/{chunk-SM4FZKAZ.mjs → cli/index.js} +1073 -309
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +356 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/config/from-env.d.ts +188 -0
- package/dist/client/config/from-env.d.ts.map +1 -0
- package/dist/client/config/index.d.ts +2 -0
- package/dist/client/config/index.d.ts.map +1 -0
- package/dist/client/errors.d.ts +67 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/kafka.client/admin/ops.d.ts +114 -0
- package/dist/client/kafka.client/admin/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/delayed.d.ts +24 -0
- package/dist/client/kafka.client/consumer/features/delayed.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts +52 -0
- package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/routed.d.ts +4 -0
- package/dist/client/kafka.client/consumer/features/routed.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/snapshot.d.ts +10 -0
- package/dist/client/kafka.client/consumer/features/snapshot.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/features/window.d.ts +5 -0
- package/dist/client/kafka.client/consumer/features/window.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/handler.d.ts +163 -0
- package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/ops.d.ts +64 -0
- package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/pipeline.d.ts +168 -0
- package/dist/client/kafka.client/consumer/pipeline.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/queue.d.ts +37 -0
- package/dist/client/kafka.client/consumer/queue.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/retry-topic.d.ts +68 -0
- package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/setup.d.ts +66 -0
- package/dist/client/kafka.client/consumer/setup.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/start.d.ts +7 -0
- package/dist/client/kafka.client/consumer/start.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/stop.d.ts +19 -0
- package/dist/client/kafka.client/consumer/stop.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/subscribe-retry.d.ts +4 -0
- package/dist/client/kafka.client/consumer/subscribe-retry.d.ts.map +1 -0
- package/dist/client/kafka.client/context.d.ts +75 -0
- package/dist/client/kafka.client/context.d.ts.map +1 -0
- package/dist/client/kafka.client/index.d.ts +155 -0
- package/dist/client/kafka.client/index.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts +61 -0
- package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/dedup.store.d.ts +28 -0
- package/dist/client/kafka.client/infra/dedup.store.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/inflight.tracker.d.ts +22 -0
- package/dist/client/kafka.client/infra/inflight.tracker.d.ts.map +1 -0
- package/dist/client/kafka.client/infra/metrics.manager.d.ts +67 -0
- package/dist/client/kafka.client/infra/metrics.manager.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/lifecycle.d.ts +41 -0
- package/dist/client/kafka.client/producer/lifecycle.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/ops.d.ts +79 -0
- package/dist/client/kafka.client/producer/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/producer/send.d.ts +21 -0
- package/dist/client/kafka.client/producer/send.d.ts.map +1 -0
- package/dist/client/kafka.client/validate-options.d.ts +11 -0
- package/dist/client/kafka.client/validate-options.d.ts.map +1 -0
- package/dist/client/message/envelope.d.ts +105 -0
- package/dist/client/message/envelope.d.ts.map +1 -0
- package/dist/client/message/schema-registry.d.ts +124 -0
- package/dist/client/message/schema-registry.d.ts.map +1 -0
- package/dist/client/message/serde.d.ts +68 -0
- package/dist/client/message/serde.d.ts.map +1 -0
- package/dist/client/message/topic.d.ts +159 -0
- package/dist/client/message/topic.d.ts.map +1 -0
- package/dist/client/message/versioned-schema.d.ts +53 -0
- package/dist/client/message/versioned-schema.d.ts.map +1 -0
- package/dist/client/outbox/index.d.ts +4 -0
- package/dist/client/outbox/index.d.ts.map +1 -0
- package/dist/client/outbox/outbox.relay.d.ts +90 -0
- package/dist/client/outbox/outbox.relay.d.ts.map +1 -0
- package/dist/client/outbox/outbox.store.d.ts +42 -0
- package/dist/client/outbox/outbox.store.d.ts.map +1 -0
- package/dist/client/outbox/outbox.types.d.ts +144 -0
- package/dist/client/outbox/outbox.types.d.ts.map +1 -0
- package/dist/client/security/acl.d.ts +108 -0
- package/dist/client/security/acl.d.ts.map +1 -0
- package/dist/client/security/index.d.ts +5 -0
- package/dist/client/security/index.d.ts.map +1 -0
- package/dist/client/security/providers.d.ts +88 -0
- package/dist/client/security/providers.d.ts.map +1 -0
- package/dist/client/security/resolve-security.d.ts +19 -0
- package/dist/client/security/resolve-security.d.ts.map +1 -0
- package/dist/client/security/security.types.d.ts +76 -0
- package/dist/client/security/security.types.d.ts.map +1 -0
- package/dist/client/transport/confluent.transport.d.ts +32 -0
- package/dist/client/transport/confluent.transport.d.ts.map +1 -0
- package/dist/client/transport/transport.interface.d.ts +221 -0
- package/dist/client/transport/transport.interface.d.ts.map +1 -0
- package/dist/client/types/admin.interface.d.ts +174 -0
- package/dist/client/types/admin.interface.d.ts.map +1 -0
- package/dist/client/types/admin.types.d.ts +140 -0
- package/dist/client/types/admin.types.d.ts.map +1 -0
- package/dist/client/types/client.d.ts +21 -0
- package/dist/client/types/client.d.ts.map +1 -0
- package/dist/client/types/common.d.ts +84 -0
- package/dist/client/types/common.d.ts.map +1 -0
- package/dist/client/types/config.types.d.ts +167 -0
- package/dist/client/types/config.types.d.ts.map +1 -0
- package/dist/client/types/consumer.interface.d.ts +115 -0
- package/dist/client/types/consumer.interface.d.ts.map +1 -0
- package/dist/{consumer.types-fFCag3VJ.d.mts → client/types/consumer.types.d.ts} +62 -383
- package/dist/client/types/consumer.types.d.ts.map +1 -0
- package/dist/client/types/dedup.types.d.ts +50 -0
- package/dist/client/types/dedup.types.d.ts.map +1 -0
- package/dist/client/types/lifecycle.interface.d.ts +72 -0
- package/dist/client/types/lifecycle.interface.d.ts.map +1 -0
- package/dist/client/types/producer.interface.d.ts +52 -0
- package/dist/client/types/producer.interface.d.ts.map +1 -0
- package/dist/client/types/producer.types.d.ts +90 -0
- package/dist/client/types/producer.types.d.ts.map +1 -0
- package/dist/client/types.d.ts +8 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/core.d.ts +13 -314
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1466 -123
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +45 -3
- package/dist/index.d.ts +7 -128
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1483 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -3
- package/dist/index.mjs.map +1 -1
- package/dist/nest/kafka.constants.d.ts +5 -0
- package/dist/nest/kafka.constants.d.ts.map +1 -0
- package/dist/nest/kafka.decorator.d.ts +49 -0
- package/dist/nest/kafka.decorator.d.ts.map +1 -0
- package/dist/nest/kafka.explorer.d.ts +17 -0
- package/dist/nest/kafka.explorer.d.ts.map +1 -0
- package/dist/nest/kafka.health.d.ts +7 -0
- package/dist/nest/kafka.health.d.ts.map +1 -0
- package/dist/nest/kafka.module.d.ts +61 -0
- package/dist/nest/kafka.module.d.ts.map +1 -0
- package/dist/otel.d.ts +83 -5
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +100 -6
- package/dist/otel.js.map +1 -1
- package/dist/otel.mjs +98 -5
- package/dist/otel.mjs.map +1 -1
- package/dist/serde.d.ts +157 -0
- package/dist/serde.d.ts.map +1 -0
- package/dist/serde.js +308 -0
- package/dist/serde.js.map +1 -0
- package/dist/serde.mjs +158 -0
- package/dist/serde.mjs.map +1 -0
- package/dist/testing/client.mock.d.ts +47 -0
- package/dist/testing/client.mock.d.ts.map +1 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/test.container.d.ts +63 -0
- package/dist/testing/test.container.d.ts.map +1 -0
- package/dist/{testing.d.mts → testing/transport.fake.d.ts} +7 -111
- package/dist/testing/transport.fake.d.ts.map +1 -0
- package/dist/testing.d.ts +2 -318
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +26 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +26 -0
- package/dist/testing.mjs.map +1 -1
- package/package.json +40 -8
- package/dist/chunk-SM4FZKAZ.mjs.map +0 -1
- package/dist/client-1irhGEu0.d.mts +0 -751
- package/dist/client-BpFjkHhr.d.ts +0 -751
- package/dist/consumer.types-fFCag3VJ.d.ts +0 -958
- package/dist/core.d.mts +0 -314
- package/dist/index.d.mts +0 -128
- package/dist/otel.d.mts +0 -27
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { MessageHeaders } from "../types";
|
|
2
|
+
export declare const HEADER_EVENT_ID = "x-event-id";
|
|
3
|
+
export declare const HEADER_CORRELATION_ID = "x-correlation-id";
|
|
4
|
+
export declare const HEADER_TIMESTAMP = "x-timestamp";
|
|
5
|
+
export declare const HEADER_SCHEMA_VERSION = "x-schema-version";
|
|
6
|
+
export declare const HEADER_TRACEPARENT = "traceparent";
|
|
7
|
+
/** Monotonically increasing logical clock stamped by the producer for deduplication. */
|
|
8
|
+
export declare const HEADER_LAMPORT_CLOCK = "x-lamport-clock";
|
|
9
|
+
/** Absolute epoch-ms deadline before which a delayed message must not be delivered. */
|
|
10
|
+
export declare const HEADER_DELAYED_UNTIL = "x-delayed-until";
|
|
11
|
+
/** Target topic a delayed message is forwarded to once its deadline passes. */
|
|
12
|
+
export declare const HEADER_DELAYED_TARGET = "x-delayed-target";
|
|
13
|
+
/**
|
|
14
|
+
* Typed wrapper combining a parsed message payload with Kafka metadata
|
|
15
|
+
* and envelope headers.
|
|
16
|
+
*
|
|
17
|
+
* On **send**, the library auto-generates envelope headers
|
|
18
|
+
* (`x-event-id`, `x-correlation-id`, `x-timestamp`, `x-schema-version`).
|
|
19
|
+
*
|
|
20
|
+
* On **consume**, the library extracts those headers and assembles
|
|
21
|
+
* an `EventEnvelope` that is passed to the handler.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* await kafka.startConsumer(['orders'], async (envelope: EventEnvelope<Order>) => {
|
|
26
|
+
* console.log(envelope.payload.orderId); // typed payload
|
|
27
|
+
* console.log(envelope.correlationId); // auto-propagated
|
|
28
|
+
* console.log(envelope.eventId); // unique message ID
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export interface EventEnvelope<T> {
|
|
33
|
+
/** Deserialized + validated message body. */
|
|
34
|
+
payload: T;
|
|
35
|
+
/** Topic the message was produced to / consumed from. */
|
|
36
|
+
topic: string;
|
|
37
|
+
/** Kafka partition (consume-side only, `-1` on send). */
|
|
38
|
+
partition: number;
|
|
39
|
+
/** Kafka offset (consume-side only, empty string on send). */
|
|
40
|
+
offset: string;
|
|
41
|
+
/** ISO-8601 timestamp set by the producer. */
|
|
42
|
+
timestamp: string;
|
|
43
|
+
/** Unique ID for this event (UUID v4). */
|
|
44
|
+
eventId: string;
|
|
45
|
+
/** Correlation ID — auto-propagated via AsyncLocalStorage. */
|
|
46
|
+
correlationId: string;
|
|
47
|
+
/** Schema version of the payload. */
|
|
48
|
+
schemaVersion: number;
|
|
49
|
+
/** W3C Trace Context `traceparent` header (set by OTel instrumentation). */
|
|
50
|
+
traceparent?: string;
|
|
51
|
+
/** All decoded Kafka headers for extensibility. */
|
|
52
|
+
headers: MessageHeaders;
|
|
53
|
+
}
|
|
54
|
+
interface EnvelopeCtx {
|
|
55
|
+
correlationId: string;
|
|
56
|
+
traceparent?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read the current envelope context (correlationId / traceparent) from ALS.
|
|
60
|
+
* Returns `undefined` outside of a Kafka consumer handler.
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const ctx = getEnvelopeContext();
|
|
64
|
+
* if (ctx) console.log('correlationId:', ctx.correlationId);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function getEnvelopeContext(): EnvelopeCtx | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Execute `fn` inside an envelope context so nested sends inherit correlationId.
|
|
70
|
+
* Automatically called by the consumer pipeline — use this in tests or manual flows.
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* await runWithEnvelopeContext({ correlationId: 'abc-123' }, async () => {
|
|
74
|
+
* await kafka.sendMessage('orders.created', payload); // inherits correlationId
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function runWithEnvelopeContext<R>(ctx: EnvelopeCtx, fn: () => R): R;
|
|
79
|
+
/** Options accepted by `buildEnvelopeHeaders`. */
|
|
80
|
+
export interface EnvelopeHeaderOptions {
|
|
81
|
+
correlationId?: string;
|
|
82
|
+
schemaVersion?: number;
|
|
83
|
+
eventId?: string;
|
|
84
|
+
headers?: MessageHeaders;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Generate envelope headers for the send path.
|
|
88
|
+
*
|
|
89
|
+
* Priority for `correlationId`:
|
|
90
|
+
* explicit option → ALS context → new UUID.
|
|
91
|
+
*/
|
|
92
|
+
export declare function buildEnvelopeHeaders(options?: EnvelopeHeaderOptions): MessageHeaders;
|
|
93
|
+
/**
|
|
94
|
+
* Decode kafkajs headers (`Record<string, Buffer | string | undefined>`)
|
|
95
|
+
* into plain `Record<string, string>`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function decodeHeaders(raw: Record<string, Buffer | string | (Buffer | string)[] | undefined> | undefined): MessageHeaders;
|
|
98
|
+
/**
|
|
99
|
+
* Build an `EventEnvelope` from a consumed kafkajs message.
|
|
100
|
+
* Tolerates missing envelope headers — generates defaults so messages
|
|
101
|
+
* from non-envelope producers still work.
|
|
102
|
+
*/
|
|
103
|
+
export declare function extractEnvelope<T>(payload: T, headers: MessageHeaders, topic: string, partition: number, offset: string): EventEnvelope<T>;
|
|
104
|
+
export {};
|
|
105
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../../../src/client/message/envelope.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAI/C,eAAO,MAAM,eAAe,eAAe,CAAC;AAC5C,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AACxD,eAAO,MAAM,gBAAgB,gBAAgB,CAAC;AAC9C,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AACxD,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAChD,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,uFAAuF;AACvF,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAIxD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,6CAA6C;IAC7C,OAAO,EAAE,CAAC,CAAC;IACX,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,OAAO,EAAE,cAAc,CAAC;CACzB;AAID,UAAU,WAAW;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,IAAI,WAAW,GAAG,SAAS,CAE5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE1E;AAID,kDAAkD;AAClD,MAAM,WAAW,qBAAqB;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAuBhB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EACC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,SAAS,CAAC,GACjE,SAAS,GACZ,cAAc,CAehB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,CAAC,EACV,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,aAAa,CAAC,CAAC,CAAC,CAalB"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { SchemaLike } from "./topic";
|
|
2
|
+
/** A schema registered in a Confluent-compatible Schema Registry. */
|
|
3
|
+
export interface RegisteredSchema {
|
|
4
|
+
/** Globally unique schema id assigned by the registry. */
|
|
5
|
+
id: number;
|
|
6
|
+
/** Version of the schema within its subject. */
|
|
7
|
+
version: number;
|
|
8
|
+
/** The schema definition string (JSON Schema / Avro / Protobuf source). */
|
|
9
|
+
schema: string;
|
|
10
|
+
}
|
|
11
|
+
/** Options for `SchemaRegistryClient`. */
|
|
12
|
+
export interface SchemaRegistryClientOptions {
|
|
13
|
+
/** Registry base URL, e.g. `http://localhost:8081` or a Confluent Cloud SR endpoint. */
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
/** HTTP Basic credentials (Confluent Cloud SR API key/secret). */
|
|
16
|
+
auth?: {
|
|
17
|
+
username: string;
|
|
18
|
+
password: string;
|
|
19
|
+
};
|
|
20
|
+
/** Cache TTL for subject lookups in ms. Default: `300_000` (5 min). */
|
|
21
|
+
cacheTtlMs?: number;
|
|
22
|
+
/** Injectable fetch implementation (tests). Default: global `fetch`. */
|
|
23
|
+
fetchFn?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
/** Schema type accepted by Confluent-compatible registries. */
|
|
26
|
+
export type RegistrySchemaType = "JSON" | "AVRO" | "PROTOBUF";
|
|
27
|
+
/**
|
|
28
|
+
* Minimal, dependency-free client for the Confluent Schema Registry REST API
|
|
29
|
+
* (works with Confluent Platform/Cloud, Redpanda, Karapace, AWS Glue SR proxy).
|
|
30
|
+
*
|
|
31
|
+
* Scope: subject/version management, compatibility checks, and id->schema
|
|
32
|
+
* lookups. Used to keep locally-defined schemas in lockstep with a central
|
|
33
|
+
* registry, and as the backing lookup for the Avro/Protobuf serdes in
|
|
34
|
+
* `@drarzter/kafka-client/serde` (which handle the wire-format framing).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
|
|
39
|
+
* const { id } = await registry.registerSchema(
|
|
40
|
+
* 'order.created-value',
|
|
41
|
+
* JSON.stringify(orderJsonSchema),
|
|
42
|
+
* 'JSON',
|
|
43
|
+
* );
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare class SchemaRegistryClient {
|
|
47
|
+
private readonly options;
|
|
48
|
+
private readonly fetchFn;
|
|
49
|
+
private readonly cacheTtlMs;
|
|
50
|
+
private readonly latestCache;
|
|
51
|
+
/**
|
|
52
|
+
* `id → schema` cache. Schema ids are immutable in a Confluent-compatible
|
|
53
|
+
* registry (a given id always maps to the same schema string), so entries
|
|
54
|
+
* are cached for the lifetime of the client with no TTL.
|
|
55
|
+
*/
|
|
56
|
+
private readonly byIdCache;
|
|
57
|
+
constructor(options: SchemaRegistryClientOptions);
|
|
58
|
+
private headers;
|
|
59
|
+
private request;
|
|
60
|
+
/** Fetch the latest schema registered under `subject`. Cached for `cacheTtlMs`. */
|
|
61
|
+
getLatestSchema(subject: string): Promise<RegisteredSchema>;
|
|
62
|
+
/**
|
|
63
|
+
* Fetch a schema by its globally unique registry id (`GET /schemas/ids/{id}`).
|
|
64
|
+
*
|
|
65
|
+
* Used by the Avro/Protobuf serdes on the deserialize path: the writer schema
|
|
66
|
+
* id is read from the Confluent wire-format prefix, then resolved here. Results
|
|
67
|
+
* are cached forever (schema ids are immutable), so a given id triggers exactly
|
|
68
|
+
* one registry round-trip regardless of how many messages reference it.
|
|
69
|
+
*/
|
|
70
|
+
getSchemaById(id: number): Promise<{
|
|
71
|
+
id: number;
|
|
72
|
+
schema: string;
|
|
73
|
+
schemaType?: string;
|
|
74
|
+
}>;
|
|
75
|
+
/** Fetch a specific schema version of a subject. */
|
|
76
|
+
getSchemaVersion(subject: string, version: number): Promise<RegisteredSchema>;
|
|
77
|
+
/**
|
|
78
|
+
* Register a schema under `subject` (idempotent — re-registering the same
|
|
79
|
+
* schema returns the existing id). Returns the registry-assigned schema id.
|
|
80
|
+
*/
|
|
81
|
+
registerSchema(subject: string, schema: string, schemaType?: RegistrySchemaType): Promise<{
|
|
82
|
+
id: number;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Test `schema` against the subject's compatibility policy without registering.
|
|
86
|
+
* Returns `true` when the registry reports the schema as compatible.
|
|
87
|
+
*/
|
|
88
|
+
checkCompatibility(subject: string, schema: string, schemaType?: RegistrySchemaType): Promise<boolean>;
|
|
89
|
+
}
|
|
90
|
+
/** Options for `registrySchema()`. */
|
|
91
|
+
export interface RegistrySchemaOptions<T> {
|
|
92
|
+
/**
|
|
93
|
+
* Local structural validator (Zod/Valibot/…) applied to every message.
|
|
94
|
+
* The registry governs schema *evolution*; this governs runtime *shape*.
|
|
95
|
+
*/
|
|
96
|
+
validator?: SchemaLike<T>;
|
|
97
|
+
/**
|
|
98
|
+
* When `true` (default), the message's `x-schema-version` must not be newer
|
|
99
|
+
* than the latest version registered for the subject — a producer publishing
|
|
100
|
+
* an unregistered version fails loudly instead of drifting silently.
|
|
101
|
+
*/
|
|
102
|
+
enforceVersion?: boolean;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Bridge a Schema Registry subject to this library's `SchemaLike` seam.
|
|
106
|
+
*
|
|
107
|
+
* On each `parse` the adapter resolves the subject's latest registered version
|
|
108
|
+
* (cached), optionally verifies the message's schema version does not exceed
|
|
109
|
+
* it, and delegates structural validation to the provided local validator.
|
|
110
|
+
* Attach the result to a `TopicDescriptor` like any other schema:
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
|
|
115
|
+
*
|
|
116
|
+
* const OrderCreated = topic('order.created').schema(
|
|
117
|
+
* registrySchema(registry, 'order.created-value', {
|
|
118
|
+
* validator: z.object({ orderId: z.string() }),
|
|
119
|
+
* }),
|
|
120
|
+
* );
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export declare function registrySchema<T = any>(client: SchemaRegistryClient, subject: string, options?: RegistrySchemaOptions<T>): SchemaLike<T>;
|
|
124
|
+
//# sourceMappingURL=schema-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-registry.d.ts","sourceRoot":"","sources":["../../../src/client/message/schema-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAsB,MAAM,SAAS,CAAC;AAE9D,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,2BAA2B;IAC1C,wFAAwF;IACxF,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,IAAI,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED,+DAA+D;AAC/D,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE9D;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,oBAAoB;IAiBnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAhBpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAGxB;IACJ;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAGtB;gBAEyB,OAAO,EAAE,2BAA2B;IAQjE,OAAO,CAAC,OAAO;YAYD,OAAO;IAoBrB,mFAAmF;IAC7E,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoBjE;;;;;;;OAOG;IACG,aAAa,CACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAY/D,oDAAoD;IAC9C,gBAAgB,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,CAAC;IAY5B;;;OAGG;IACG,cAAc,CAClB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,kBAA2B,GACtC,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAS1B;;;OAGG;IACG,kBAAkB,CACtB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,kBAA2B,GACtC,OAAO,CAAC,OAAO,CAAC;CAQpB;AAED,sCAAsC;AACtC,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,GAAG,EACpC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC,CAAC,GACjC,UAAU,CAAC,CAAC,CAAC,CAkBf"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { MessageHeaders } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Context passed to `MessageSerde.serialize` / `deserialize`.
|
|
4
|
+
*
|
|
5
|
+
* Carries the topic name, decoded message headers, and which side of the
|
|
6
|
+
* record is being (de)serialized. A Confluent Schema Registry serde uses
|
|
7
|
+
* `topic` + `isKey` to derive the subject name (`<topic>-value` / `<topic>-key`)
|
|
8
|
+
* and reads the schema id from the header/magic-byte prefix on `data`.
|
|
9
|
+
*/
|
|
10
|
+
export interface SerdeContext {
|
|
11
|
+
/** Topic the message is produced to / consumed from. */
|
|
12
|
+
topic: string;
|
|
13
|
+
/** Decoded message headers (envelope headers included). */
|
|
14
|
+
headers: MessageHeaders;
|
|
15
|
+
/**
|
|
16
|
+
* Which side of the Kafka record this call is (de)serializing.
|
|
17
|
+
* `false` / omitted → the value (default); `true` → the key.
|
|
18
|
+
* Used by schema-registry serdes to pick the `value` vs `key` subject.
|
|
19
|
+
*/
|
|
20
|
+
isKey?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pluggable serialization layer for message payloads.
|
|
24
|
+
*
|
|
25
|
+
* A `MessageSerde` converts a validated payload object to the wire form
|
|
26
|
+
* (`Buffer` or `string`) on produce, and back to an object on consume.
|
|
27
|
+
* The default is {@link JsonSerde}, which reproduces the client's historical
|
|
28
|
+
* `JSON.stringify` / `JSON.parse` behaviour exactly.
|
|
29
|
+
*
|
|
30
|
+
* Serde only touches the message VALUE. Envelope metadata
|
|
31
|
+
* (`x-event-id`, `x-correlation-id`, `x-lamport-clock`, `traceparent`, …)
|
|
32
|
+
* always travels in headers and is never serialized through this layer.
|
|
33
|
+
*
|
|
34
|
+
* Set a client-wide serde via `KafkaClientOptions.serde`, or a per-topic
|
|
35
|
+
* override via `topic(...).serde(mySerde)` — the per-topic serde wins for
|
|
36
|
+
* that topic.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const kafka = new KafkaClient(id, group, brokers, { serde: new JsonSerde() });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export interface MessageSerde {
|
|
44
|
+
/**
|
|
45
|
+
* Serialize a validated payload object to wire bytes (`Buffer`) or a
|
|
46
|
+
* `string`. Validation has already run on `value` before this is called.
|
|
47
|
+
*/
|
|
48
|
+
serialize(value: unknown, ctx: SerdeContext): Buffer | string | Promise<Buffer | string>;
|
|
49
|
+
/**
|
|
50
|
+
* Deserialize raw wire bytes into a payload object. Schema validation
|
|
51
|
+
* (if any) runs on the returned object afterwards.
|
|
52
|
+
*/
|
|
53
|
+
deserialize(data: Buffer, ctx: SerdeContext): unknown | Promise<unknown>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Default {@link MessageSerde}: JSON via `JSON.stringify` / `JSON.parse`.
|
|
57
|
+
*
|
|
58
|
+
* Byte-for-byte identical to the client's historical serialization, so it is
|
|
59
|
+
* a zero-behaviour-change default. Produces a UTF-8 `string` on serialize and
|
|
60
|
+
* decodes UTF-8 bytes on deserialize.
|
|
61
|
+
*/
|
|
62
|
+
export declare class JsonSerde implements MessageSerde {
|
|
63
|
+
/** JSON-stringify the validated payload. Returns a UTF-8 string. */
|
|
64
|
+
serialize(value: unknown): string;
|
|
65
|
+
/** JSON-parse UTF-8 wire bytes into an object. */
|
|
66
|
+
deserialize(data: Buffer): unknown;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=serde.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serde.d.ts","sourceRoot":"","sources":["../../../src/client/message/serde.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,OAAO,EAAE,cAAc,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,CACP,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,YAAY,GAChB,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAC9C;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1E;AAED;;;;;;GAMG;AACH,qBAAa,SAAU,YAAW,YAAY;IAC5C,oEAAoE;IACpE,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM;IAIjC,kDAAkD;IAClD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAGnC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { MessageHeaders } from "../types";
|
|
2
|
+
import type { MessageSerde } from "./serde";
|
|
3
|
+
/**
|
|
4
|
+
* Context passed as the second argument to `SchemaLike.parse()`.
|
|
5
|
+
* Enables schema-registry adapters, version-aware migration, and
|
|
6
|
+
* header-driven parsing without coupling validators to Kafka internals.
|
|
7
|
+
*
|
|
8
|
+
* All fields are optional-friendly — validators that don't need the context
|
|
9
|
+
* can simply ignore the second argument.
|
|
10
|
+
*/
|
|
11
|
+
export interface SchemaParseContext {
|
|
12
|
+
/** Topic the message was produced to / consumed from. */
|
|
13
|
+
topic: string;
|
|
14
|
+
/** Decoded message headers (envelope headers included). */
|
|
15
|
+
headers: MessageHeaders;
|
|
16
|
+
/** Value of the `x-schema-version` header, defaults to `1`. */
|
|
17
|
+
version: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Any validation library with a `.parse()` method.
|
|
21
|
+
* Works with Zod, Valibot, ArkType, or any custom validator.
|
|
22
|
+
*
|
|
23
|
+
* The optional `ctx` argument carries topic/header/version metadata so
|
|
24
|
+
* validators can perform schema-registry lookups or version-aware migrations.
|
|
25
|
+
* Existing validators that only use the first argument continue to work
|
|
26
|
+
* unchanged — the second argument is silently ignored.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { z } from 'zod';
|
|
31
|
+
* const schema: SchemaLike<{ id: string }> = z.object({ id: z.string() });
|
|
32
|
+
*
|
|
33
|
+
* // Context-aware validator:
|
|
34
|
+
* const schema: SchemaLike<MyType> = {
|
|
35
|
+
* parse(data, ctx) {
|
|
36
|
+
* const version = ctx?.version ?? 1;
|
|
37
|
+
* return version >= 2 ? migrateV1toV2(data) : validateV1(data);
|
|
38
|
+
* }
|
|
39
|
+
* };
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export interface SchemaLike<T = any> {
|
|
43
|
+
parse(data: unknown, ctx?: SchemaParseContext): T | Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
/** Infer the output type from a SchemaLike. */
|
|
46
|
+
export type InferSchema<S extends SchemaLike> = S extends SchemaLike<infer T> ? T : never;
|
|
47
|
+
/**
|
|
48
|
+
* A typed topic descriptor that pairs a topic name with its message type.
|
|
49
|
+
* Created via the `topic()` factory function.
|
|
50
|
+
*
|
|
51
|
+
* @typeParam N - The literal topic name string.
|
|
52
|
+
* @typeParam M - The message payload type for this topic.
|
|
53
|
+
*/
|
|
54
|
+
export interface TopicDescriptor<N extends string = string, M extends Record<string, any> = Record<string, any>> {
|
|
55
|
+
readonly __topic: N;
|
|
56
|
+
/** @internal Phantom type — never has a real value at runtime. */
|
|
57
|
+
readonly __type: M;
|
|
58
|
+
/** Runtime schema validator. Present only when created via `topic().schema()`. */
|
|
59
|
+
readonly __schema?: SchemaLike<M>;
|
|
60
|
+
/**
|
|
61
|
+
* Per-topic serialization override. Present only when created via `.serde()`.
|
|
62
|
+
* When set, this serde is used for produce/consume on this topic instead of
|
|
63
|
+
* the client-wide `KafkaClientOptions.serde`.
|
|
64
|
+
*/
|
|
65
|
+
readonly __serde?: MessageSerde;
|
|
66
|
+
/**
|
|
67
|
+
* Partition-key extractor. Present only when created via `.key()`.
|
|
68
|
+
* Applied on every send through this descriptor unless an explicit
|
|
69
|
+
* `key` is passed in `SendOptions` / the batch item.
|
|
70
|
+
*
|
|
71
|
+
* Declared with method syntax (not a function property) so `M` stays
|
|
72
|
+
* bivariant — otherwise narrow descriptors would stop being assignable
|
|
73
|
+
* to `TopicDescriptor<string, Record<string, any>>` parameters.
|
|
74
|
+
*/
|
|
75
|
+
__key?(message: M): string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A `TopicDescriptor` that can still be extended with a `.key()` extractor or
|
|
79
|
+
* a `.serde()` override. Returned by `topic().type()` and `topic().schema()` —
|
|
80
|
+
* usable directly as a descriptor, or chained further to declare partition
|
|
81
|
+
* affinity and/or a custom serializer.
|
|
82
|
+
*/
|
|
83
|
+
export type KeyableTopicDescriptor<N extends string, M extends Record<string, any>> = TopicDescriptor<N, M> & {
|
|
84
|
+
/**
|
|
85
|
+
* Declare a partition-key extractor for this topic. The extractor runs on
|
|
86
|
+
* the ORIGINAL (pre-validation) payload of every message sent through this
|
|
87
|
+
* descriptor, so messages with the same logical key always land on the same
|
|
88
|
+
* partition without passing `key` at each call site.
|
|
89
|
+
*
|
|
90
|
+
* An explicit `SendOptions.key` / batch-item `key` always wins.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const OrderCreated = topic('order.created')
|
|
95
|
+
* .type<{ orderId: string; amount: number }>()
|
|
96
|
+
* .key((m) => m.orderId);
|
|
97
|
+
*
|
|
98
|
+
* await kafka.sendMessage(OrderCreated, { orderId: '42', amount: 100 });
|
|
99
|
+
* // → produced with key '42'
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
key(extractor: (message: M) => string): KeyableTopicDescriptor<N, M>;
|
|
103
|
+
/**
|
|
104
|
+
* Declare a per-topic serialization override. The given {@link MessageSerde}
|
|
105
|
+
* is used for produce/consume on this topic instead of the client-wide
|
|
106
|
+
* `KafkaClientOptions.serde`. Chainable with `.key()`.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const OrderCreated = topic('order.created')
|
|
111
|
+
* .schema(OrderSchema)
|
|
112
|
+
* .serde(new AvroSerde(OrderAvroSchema));
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
serde(serde: MessageSerde): KeyableTopicDescriptor<N, M>;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Define a typed topic descriptor.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* // Without schema — explicit type via .type<T>():
|
|
123
|
+
* const OrderCreated = topic('order.created').type<{ orderId: string; amount: number }>();
|
|
124
|
+
*
|
|
125
|
+
* // With schema — type inferred from schema:
|
|
126
|
+
* const OrderCreated = topic('order.created').schema(z.object({
|
|
127
|
+
* orderId: z.string(),
|
|
128
|
+
* amount: z.number(),
|
|
129
|
+
* }));
|
|
130
|
+
*
|
|
131
|
+
* // Use with KafkaClient:
|
|
132
|
+
* await kafka.sendMessage(OrderCreated, { orderId: '123', amount: 100 });
|
|
133
|
+
*
|
|
134
|
+
* // Use with @SubscribeTo:
|
|
135
|
+
* @SubscribeTo(OrderCreated)
|
|
136
|
+
* async handleOrder(msg) { ... }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export declare function topic<N extends string>(name: N): {
|
|
140
|
+
/** Provide an explicit message type without a runtime schema. */
|
|
141
|
+
type: <M extends Record<string, any>>() => KeyableTopicDescriptor<N, M>;
|
|
142
|
+
schema: <S extends SchemaLike<Record<string, any>>>(schema: S) => KeyableTopicDescriptor<N, InferSchema<S>>;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Build a topic-message map type from a union of TopicDescriptors.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const OrderCreated = topic('order.created').type<{ orderId: string }>();
|
|
150
|
+
* const OrderCompleted = topic('order.completed').type<{ completedAt: string }>();
|
|
151
|
+
*
|
|
152
|
+
* type MyTopics = TopicsFrom<typeof OrderCreated | typeof OrderCompleted>;
|
|
153
|
+
* // { 'order.created': { orderId: string }; 'order.completed': { completedAt: string } }
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export type TopicsFrom<D extends TopicDescriptor<any, any>> = {
|
|
157
|
+
[K in D as K["__topic"]]: K["__type"];
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=topic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topic.d.ts","sourceRoot":"","sources":["../../../src/client/message/topic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,OAAO,EAAE,cAAc,CAAC;IACxB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,GAAG;IACjC,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,kBAAkB,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAChE;AAED,+CAA+C;AAC/C,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,UAAU,IAC1C,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,WAAW,eAAe,CAC9B,CAAC,SAAS,MAAM,GAAG,MAAM,EACzB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAEnD,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC;IAChC;;;;;;;;OAQG;IACH,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,CAChC,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAC3B,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAC1B;;;;;;;;;;;;;;;;;OAiBG;IACH,GAAG,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,GAAG,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACrE;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC1D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,CAAC;IAE3C,iEAAiE;WAC1D,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,OAAK,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC;aAM5D,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,UACxC,CAAC,KACR,sBAAsB,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;EAO/C;AAeD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI;KAC3D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;CACtC,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SchemaLike } from "./topic";
|
|
2
|
+
/**
|
|
3
|
+
* Options for `versionedSchema()`.
|
|
4
|
+
* @typeParam T - The (latest) output type produced after parsing and migration.
|
|
5
|
+
*/
|
|
6
|
+
export interface VersionedSchemaOptions<T> {
|
|
7
|
+
/**
|
|
8
|
+
* Called after a message parsed with a non-latest schema version.
|
|
9
|
+
* Receives the parsed data, the version it was parsed with, and the latest
|
|
10
|
+
* registered version. Must return the data in its latest shape.
|
|
11
|
+
*
|
|
12
|
+
* When omitted, older versions are returned as parsed — callers must handle
|
|
13
|
+
* shape differences themselves.
|
|
14
|
+
*/
|
|
15
|
+
migrate?: (data: any, fromVersion: number, latestVersion: number) => T | Promise<T>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compose per-version validators into a single `SchemaLike` that dispatches on
|
|
19
|
+
* the message's `x-schema-version` header (via `SchemaParseContext.version`).
|
|
20
|
+
*
|
|
21
|
+
* - Consume path: the version comes from the `x-schema-version` header
|
|
22
|
+
* (defaults to `1` when absent).
|
|
23
|
+
* - Send path: the version comes from `SendOptions.schemaVersion`
|
|
24
|
+
* (defaults to `1`).
|
|
25
|
+
* - No parse context at all (direct `.parse(data)` call): the latest
|
|
26
|
+
* registered version is assumed.
|
|
27
|
+
*
|
|
28
|
+
* Throws when a message carries a version with no registered schema — a
|
|
29
|
+
* misconfigured producer fails loudly instead of validating against the
|
|
30
|
+
* wrong shape.
|
|
31
|
+
*
|
|
32
|
+
* @typeParam T - The latest message shape (post-migration).
|
|
33
|
+
* @param versions Map of version number → validator for that version.
|
|
34
|
+
* @param options Optional migration hook to upgrade old shapes to the latest.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const OrderSchema = versionedSchema<{ orderId: string; amountMinor: number }>(
|
|
39
|
+
* {
|
|
40
|
+
* 1: z.object({ orderId: z.string(), amount: z.number() }),
|
|
41
|
+
* 2: z.object({ orderId: z.string(), amountMinor: z.number().int() }),
|
|
42
|
+
* },
|
|
43
|
+
* {
|
|
44
|
+
* migrate: (data, from) =>
|
|
45
|
+
* from === 1 ? { orderId: data.orderId, amountMinor: Math.round(data.amount * 100) } : data,
|
|
46
|
+
* },
|
|
47
|
+
* );
|
|
48
|
+
*
|
|
49
|
+
* const OrderCreated = topic('order.created').schema(OrderSchema);
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function versionedSchema<T = any>(versions: Record<number, SchemaLike<any>>, options?: VersionedSchemaOptions<T>): SchemaLike<T>;
|
|
53
|
+
//# sourceMappingURL=versioned-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versioned-schema.d.ts","sourceRoot":"","sources":["../../../src/client/message/versioned-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAsB,MAAM,SAAS,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,GAAG,EACT,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,KAClB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,eAAe,CAAC,CAAC,GAAG,GAAG,EACrC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,EACzC,OAAO,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAClC,UAAU,CAAC,CAAC,CAAC,CA6Bf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/outbox/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { TopicMapConstraint } from "../types";
|
|
2
|
+
import type { OutboxProducer, OutboxRelayHandle, OutboxRelayOptions, OutboxStore } from "./outbox.types";
|
|
3
|
+
/**
|
|
4
|
+
* Start a transactional-outbox relay.
|
|
5
|
+
*
|
|
6
|
+
* The outbox pattern decouples "write my business state" from "publish an event"
|
|
7
|
+
* so the two can never diverge: application code writes an event row into an
|
|
8
|
+
* outbox table **in the same DB transaction** as its business writes; this relay
|
|
9
|
+
* polls that table and publishes the rows to Kafka, marking them published only
|
|
10
|
+
* after Kafka has acked them.
|
|
11
|
+
*
|
|
12
|
+
* ## Poll loop
|
|
13
|
+
* Every `pollIntervalMs` the relay calls {@link OutboxStore.fetchUnpublished}
|
|
14
|
+
* with `batchSize`. An empty result means there is nothing to do — it waits for
|
|
15
|
+
* the next tick. A non-empty batch is published **entirely inside one Kafka
|
|
16
|
+
* transaction** (`kafka.transaction`, one `tx.send` per message), and only after
|
|
17
|
+
* that transaction commits does it call {@link OutboxStore.markPublished} with
|
|
18
|
+
* the row ids.
|
|
19
|
+
*
|
|
20
|
+
* ## Delivery guarantee: at-least-once
|
|
21
|
+
* If the process crashes **after** the Kafka transaction commits but **before**
|
|
22
|
+
* `markPublished` runs, those rows are still unpublished in the store and will be
|
|
23
|
+
* re-published on the next poll. This produces **duplicates**, but each duplicate
|
|
24
|
+
* carries the **same** `x-event-id` (when {@link OutboxMessage.eventId} is set on
|
|
25
|
+
* the row), so consumers can deduplicate — either via this library's Lamport-clock
|
|
26
|
+
* deduplication or an application-level idempotency check keyed on the event id.
|
|
27
|
+
* Persist a stable `eventId` on each outbox row to make this work.
|
|
28
|
+
*
|
|
29
|
+
* ## Failure handling
|
|
30
|
+
* The loop never dies and never leaks an unhandled rejection:
|
|
31
|
+
* - If the publishing transaction throws, `onError` fires, the batch is **not**
|
|
32
|
+
* marked published, and it is retried on the next tick.
|
|
33
|
+
* - If the store throws (either `fetchUnpublished` or `markPublished`), same
|
|
34
|
+
* behaviour: `onError`, no marking, retried next tick.
|
|
35
|
+
*
|
|
36
|
+
* Note the one asymmetry: if `markPublished` throws *after* a successful Kafka
|
|
37
|
+
* commit, the batch is re-published next tick — an intentional consequence of
|
|
38
|
+
* the at-least-once guarantee above.
|
|
39
|
+
*
|
|
40
|
+
* ## Concurrency
|
|
41
|
+
* Iterations never overlap. If an iteration runs longer than `pollIntervalMs`,
|
|
42
|
+
* intervening ticks are skipped (a guard flag) rather than stacked.
|
|
43
|
+
*
|
|
44
|
+
* ## Shutdown
|
|
45
|
+
* `handle.stop()` clears the timer and awaits any in-flight iteration before
|
|
46
|
+
* resolving, so no publish is left half-done. It is idempotent.
|
|
47
|
+
*
|
|
48
|
+
* @param kafka Any producer exposing `transaction()` — a `KafkaClient<T>` or
|
|
49
|
+
* `IKafkaProducer<T>`. Its producer must already be connected.
|
|
50
|
+
* @param store Your DB-backed {@link OutboxStore} implementation.
|
|
51
|
+
* @param options Tuning and observability — see {@link OutboxRelayOptions}.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* // Pseudo-Postgres outbox store.
|
|
56
|
+
* const store: OutboxStore = {
|
|
57
|
+
* async fetchUnpublished(limit) {
|
|
58
|
+
* const { rows } = await pool.query(
|
|
59
|
+
* `SELECT id, topic, payload, key, correlation_id AS "correlationId",
|
|
60
|
+
* event_id AS "eventId", headers
|
|
61
|
+
* FROM outbox
|
|
62
|
+
* WHERE published_at IS NULL
|
|
63
|
+
* ORDER BY created_at ASC
|
|
64
|
+
* LIMIT $1`,
|
|
65
|
+
* [limit],
|
|
66
|
+
* );
|
|
67
|
+
* return rows;
|
|
68
|
+
* },
|
|
69
|
+
* async markPublished(ids) {
|
|
70
|
+
* await pool.query(
|
|
71
|
+
* `UPDATE outbox SET published_at = now() WHERE id = ANY($1)`,
|
|
72
|
+
* [ids],
|
|
73
|
+
* );
|
|
74
|
+
* },
|
|
75
|
+
* };
|
|
76
|
+
*
|
|
77
|
+
* await kafka.connectProducer();
|
|
78
|
+
* const relay = startOutboxRelay(kafka, store, {
|
|
79
|
+
* pollIntervalMs: 500,
|
|
80
|
+
* batchSize: 200,
|
|
81
|
+
* onPublished: (n) => metrics.increment('outbox.published', n),
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* // On shutdown:
|
|
85
|
+
* await relay.stop();
|
|
86
|
+
* await kafka.disconnect();
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare function startOutboxRelay<T extends TopicMapConstraint<T>>(kafka: OutboxProducer<T>, store: OutboxStore, options?: OutboxRelayOptions): OutboxRelayHandle;
|
|
90
|
+
//# sourceMappingURL=outbox.relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox.relay.d.ts","sourceRoot":"","sources":["../../../src/client/outbox/outbox.relay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,KAAK,EAEV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAOxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,kBAAkB,CAAC,CAAC,CAAC,EAC9D,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EACxB,KAAK,EAAE,WAAW,EAClB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAwEnB"}
|