@drarzter/kafka-client 0.9.4 → 0.10.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 +625 -8
- package/dist/chunk-CMO7SMVK.mjs +4814 -0
- package/dist/chunk-CMO7SMVK.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} +964 -264
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +355 -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 +149 -0
- package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/ops.d.ts +51 -0
- package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/pipeline.d.ts +167 -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 +65 -0
- package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -0
- package/dist/client/kafka.client/consumer/setup.d.ts +63 -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 +72 -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 +70 -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 +105 -0
- package/dist/client/message/schema-registry.d.ts.map +1 -0
- package/dist/client/message/topic.d.ts +138 -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 +216 -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 +150 -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 +10 -314
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1325 -73
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +39 -3
- package/dist/index.d.ts +7 -128
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1342 -73
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +56 -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/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 +21 -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,105 @@
|
|
|
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 and compatibility checks — the pieces
|
|
32
|
+
* needed to keep locally-defined schemas in lockstep with a central registry.
|
|
33
|
+
* Payload (de)serialisation stays JSON as everywhere in this library; wire-format
|
|
34
|
+
* framing with magic bytes (Avro/Protobuf binary) is intentionally out of scope.
|
|
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
|
+
constructor(options: SchemaRegistryClientOptions);
|
|
52
|
+
private headers;
|
|
53
|
+
private request;
|
|
54
|
+
/** Fetch the latest schema registered under `subject`. Cached for `cacheTtlMs`. */
|
|
55
|
+
getLatestSchema(subject: string): Promise<RegisteredSchema>;
|
|
56
|
+
/** Fetch a specific schema version of a subject. */
|
|
57
|
+
getSchemaVersion(subject: string, version: number): Promise<RegisteredSchema>;
|
|
58
|
+
/**
|
|
59
|
+
* Register a schema under `subject` (idempotent — re-registering the same
|
|
60
|
+
* schema returns the existing id). Returns the registry-assigned schema id.
|
|
61
|
+
*/
|
|
62
|
+
registerSchema(subject: string, schema: string, schemaType?: RegistrySchemaType): Promise<{
|
|
63
|
+
id: number;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Test `schema` against the subject's compatibility policy without registering.
|
|
67
|
+
* Returns `true` when the registry reports the schema as compatible.
|
|
68
|
+
*/
|
|
69
|
+
checkCompatibility(subject: string, schema: string, schemaType?: RegistrySchemaType): Promise<boolean>;
|
|
70
|
+
}
|
|
71
|
+
/** Options for `registrySchema()`. */
|
|
72
|
+
export interface RegistrySchemaOptions<T> {
|
|
73
|
+
/**
|
|
74
|
+
* Local structural validator (Zod/Valibot/…) applied to every message.
|
|
75
|
+
* The registry governs schema *evolution*; this governs runtime *shape*.
|
|
76
|
+
*/
|
|
77
|
+
validator?: SchemaLike<T>;
|
|
78
|
+
/**
|
|
79
|
+
* When `true` (default), the message's `x-schema-version` must not be newer
|
|
80
|
+
* than the latest version registered for the subject — a producer publishing
|
|
81
|
+
* an unregistered version fails loudly instead of drifting silently.
|
|
82
|
+
*/
|
|
83
|
+
enforceVersion?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Bridge a Schema Registry subject to this library's `SchemaLike` seam.
|
|
87
|
+
*
|
|
88
|
+
* On each `parse` the adapter resolves the subject's latest registered version
|
|
89
|
+
* (cached), optionally verifies the message's schema version does not exceed
|
|
90
|
+
* it, and delegates structural validation to the provided local validator.
|
|
91
|
+
* Attach the result to a `TopicDescriptor` like any other schema:
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
|
|
96
|
+
*
|
|
97
|
+
* const OrderCreated = topic('order.created').schema(
|
|
98
|
+
* registrySchema(registry, 'order.created-value', {
|
|
99
|
+
* validator: z.object({ orderId: z.string() }),
|
|
100
|
+
* }),
|
|
101
|
+
* );
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export declare function registrySchema<T = any>(client: SchemaRegistryClient, subject: string, options?: RegistrySchemaOptions<T>): SchemaLike<T>;
|
|
105
|
+
//# 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;IAQnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAGxB;gBAEyB,OAAO,EAAE,2BAA2B;IAQjE,OAAO,CAAC,OAAO;YAYD,OAAO;IAoBrB,mFAAmF;IAC7E,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoBjE,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,138 @@
|
|
|
1
|
+
import type { MessageHeaders } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Context passed as the second argument to `SchemaLike.parse()`.
|
|
4
|
+
* Enables schema-registry adapters, version-aware migration, and
|
|
5
|
+
* header-driven parsing without coupling validators to Kafka internals.
|
|
6
|
+
*
|
|
7
|
+
* All fields are optional-friendly — validators that don't need the context
|
|
8
|
+
* can simply ignore the second argument.
|
|
9
|
+
*/
|
|
10
|
+
export interface SchemaParseContext {
|
|
11
|
+
/** Topic the message was produced to / consumed from. */
|
|
12
|
+
topic: string;
|
|
13
|
+
/** Decoded message headers (envelope headers included). */
|
|
14
|
+
headers: MessageHeaders;
|
|
15
|
+
/** Value of the `x-schema-version` header, defaults to `1`. */
|
|
16
|
+
version: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Any validation library with a `.parse()` method.
|
|
20
|
+
* Works with Zod, Valibot, ArkType, or any custom validator.
|
|
21
|
+
*
|
|
22
|
+
* The optional `ctx` argument carries topic/header/version metadata so
|
|
23
|
+
* validators can perform schema-registry lookups or version-aware migrations.
|
|
24
|
+
* Existing validators that only use the first argument continue to work
|
|
25
|
+
* unchanged — the second argument is silently ignored.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { z } from 'zod';
|
|
30
|
+
* const schema: SchemaLike<{ id: string }> = z.object({ id: z.string() });
|
|
31
|
+
*
|
|
32
|
+
* // Context-aware validator:
|
|
33
|
+
* const schema: SchemaLike<MyType> = {
|
|
34
|
+
* parse(data, ctx) {
|
|
35
|
+
* const version = ctx?.version ?? 1;
|
|
36
|
+
* return version >= 2 ? migrateV1toV2(data) : validateV1(data);
|
|
37
|
+
* }
|
|
38
|
+
* };
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export interface SchemaLike<T = any> {
|
|
42
|
+
parse(data: unknown, ctx?: SchemaParseContext): T | Promise<T>;
|
|
43
|
+
}
|
|
44
|
+
/** Infer the output type from a SchemaLike. */
|
|
45
|
+
export type InferSchema<S extends SchemaLike> = S extends SchemaLike<infer T> ? T : never;
|
|
46
|
+
/**
|
|
47
|
+
* A typed topic descriptor that pairs a topic name with its message type.
|
|
48
|
+
* Created via the `topic()` factory function.
|
|
49
|
+
*
|
|
50
|
+
* @typeParam N - The literal topic name string.
|
|
51
|
+
* @typeParam M - The message payload type for this topic.
|
|
52
|
+
*/
|
|
53
|
+
export interface TopicDescriptor<N extends string = string, M extends Record<string, any> = Record<string, any>> {
|
|
54
|
+
readonly __topic: N;
|
|
55
|
+
/** @internal Phantom type — never has a real value at runtime. */
|
|
56
|
+
readonly __type: M;
|
|
57
|
+
/** Runtime schema validator. Present only when created via `topic().schema()`. */
|
|
58
|
+
readonly __schema?: SchemaLike<M>;
|
|
59
|
+
/**
|
|
60
|
+
* Partition-key extractor. Present only when created via `.key()`.
|
|
61
|
+
* Applied on every send through this descriptor unless an explicit
|
|
62
|
+
* `key` is passed in `SendOptions` / the batch item.
|
|
63
|
+
*
|
|
64
|
+
* Declared with method syntax (not a function property) so `M` stays
|
|
65
|
+
* bivariant — otherwise narrow descriptors would stop being assignable
|
|
66
|
+
* to `TopicDescriptor<string, Record<string, any>>` parameters.
|
|
67
|
+
*/
|
|
68
|
+
__key?(message: M): string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A `TopicDescriptor` that can still be extended with a `.key()` extractor.
|
|
72
|
+
* Returned by `topic().type()` and `topic().schema()` — usable directly as a
|
|
73
|
+
* descriptor, or chained once more to declare partition affinity.
|
|
74
|
+
*/
|
|
75
|
+
export type KeyableTopicDescriptor<N extends string, M extends Record<string, any>> = TopicDescriptor<N, M> & {
|
|
76
|
+
/**
|
|
77
|
+
* Declare a partition-key extractor for this topic. The extractor runs on
|
|
78
|
+
* the ORIGINAL (pre-validation) payload of every message sent through this
|
|
79
|
+
* descriptor, so messages with the same logical key always land on the same
|
|
80
|
+
* partition without passing `key` at each call site.
|
|
81
|
+
*
|
|
82
|
+
* An explicit `SendOptions.key` / batch-item `key` always wins.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const OrderCreated = topic('order.created')
|
|
87
|
+
* .type<{ orderId: string; amount: number }>()
|
|
88
|
+
* .key((m) => m.orderId);
|
|
89
|
+
*
|
|
90
|
+
* await kafka.sendMessage(OrderCreated, { orderId: '42', amount: 100 });
|
|
91
|
+
* // → produced with key '42'
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
key(extractor: (message: M) => string): TopicDescriptor<N, M>;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Define a typed topic descriptor.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* // Without schema — explicit type via .type<T>():
|
|
102
|
+
* const OrderCreated = topic('order.created').type<{ orderId: string; amount: number }>();
|
|
103
|
+
*
|
|
104
|
+
* // With schema — type inferred from schema:
|
|
105
|
+
* const OrderCreated = topic('order.created').schema(z.object({
|
|
106
|
+
* orderId: z.string(),
|
|
107
|
+
* amount: z.number(),
|
|
108
|
+
* }));
|
|
109
|
+
*
|
|
110
|
+
* // Use with KafkaClient:
|
|
111
|
+
* await kafka.sendMessage(OrderCreated, { orderId: '123', amount: 100 });
|
|
112
|
+
*
|
|
113
|
+
* // Use with @SubscribeTo:
|
|
114
|
+
* @SubscribeTo(OrderCreated)
|
|
115
|
+
* async handleOrder(msg) { ... }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export declare function topic<N extends string>(name: N): {
|
|
119
|
+
/** Provide an explicit message type without a runtime schema. */
|
|
120
|
+
type: <M extends Record<string, any>>() => KeyableTopicDescriptor<N, M>;
|
|
121
|
+
schema: <S extends SchemaLike<Record<string, any>>>(schema: S) => KeyableTopicDescriptor<N, InferSchema<S>>;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Build a topic-message map type from a union of TopicDescriptors.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* const OrderCreated = topic('order.created').type<{ orderId: string }>();
|
|
129
|
+
* const OrderCompleted = topic('order.completed').type<{ completedAt: string }>();
|
|
130
|
+
*
|
|
131
|
+
* type MyTopics = TopicsFrom<typeof OrderCreated | typeof OrderCompleted>;
|
|
132
|
+
* // { 'order.created': { orderId: string }; 'order.completed': { completedAt: string } }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export type TopicsFrom<D extends TopicDescriptor<any, any>> = {
|
|
136
|
+
[K in D as K["__topic"]]: K["__type"];
|
|
137
|
+
};
|
|
138
|
+
//# 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;AAE/C;;;;;;;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;;;;;;;;OAQG;IACH,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC;CAC5B;AAED;;;;GAIG;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,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC/D,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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OutboxMessage, OutboxStore } from "./outbox.types";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory reference implementation of {@link OutboxStore}.
|
|
4
|
+
*
|
|
5
|
+
* Intended for tests and as executable documentation of the expected
|
|
6
|
+
* {@link OutboxStore} semantics — **not** for production use: it provides no
|
|
7
|
+
* durability, so a process restart loses all pending rows and the "same DB
|
|
8
|
+
* transaction as the business write" guarantee (the whole point of the outbox
|
|
9
|
+
* pattern) does not hold. Back a real relay with a persistent store (Postgres,
|
|
10
|
+
* MySQL, …) implementing the same interface.
|
|
11
|
+
*
|
|
12
|
+
* Semantics mirrored:
|
|
13
|
+
* - {@link fetchUnpublished} returns unpublished rows **oldest first** (insertion
|
|
14
|
+
* order), capped at `limit`.
|
|
15
|
+
* - {@link markPublished} is idempotent — unknown ids are ignored.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const store = new InMemoryOutboxStore();
|
|
20
|
+
* store.add({ id: '1', topic: 'orders.created', payload: { orderId: 'o1' } });
|
|
21
|
+
*
|
|
22
|
+
* const handle = startOutboxRelay(kafka, store);
|
|
23
|
+
* // ... later
|
|
24
|
+
* await handle.stop();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class InMemoryOutboxStore implements OutboxStore {
|
|
28
|
+
/** Insertion-ordered rows. `published` flips to true after `markPublished`. */
|
|
29
|
+
private readonly rows;
|
|
30
|
+
/**
|
|
31
|
+
* Append a message to the outbox. In a real store this INSERT would run inside
|
|
32
|
+
* the same DB transaction as the corresponding business write.
|
|
33
|
+
*/
|
|
34
|
+
add(message: OutboxMessage): void;
|
|
35
|
+
fetchUnpublished(limit: number): Promise<OutboxMessage[]>;
|
|
36
|
+
markPublished(ids: string[]): Promise<void>;
|
|
37
|
+
/** Test helper: count of rows not yet marked published. */
|
|
38
|
+
get pendingCount(): number;
|
|
39
|
+
/** Test helper: count of rows marked published. */
|
|
40
|
+
get publishedCount(): number;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=outbox.store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox.store.d.ts","sourceRoot":"","sources":["../../../src/client/outbox/outbox.store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,mBAAoB,YAAW,WAAW;IACrD,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,IAAI,CAChB;IAEL;;;OAGG;IACH,GAAG,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAI3B,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAUzD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjD,2DAA2D;IAC3D,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,mDAAmD;IACnD,IAAI,cAAc,IAAI,MAAM,CAE3B;CACF"}
|