@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.
Files changed (180) hide show
  1. package/README.md +693 -8
  2. package/dist/chunk-OR7TPAAE.mjs +4760 -0
  3. package/dist/chunk-OR7TPAAE.mjs.map +1 -0
  4. package/dist/chunk-PQVBRDNV.mjs +149 -0
  5. package/dist/chunk-PQVBRDNV.mjs.map +1 -0
  6. package/dist/cli/dlq.d.ts +119 -0
  7. package/dist/cli/dlq.d.ts.map +1 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/{chunk-SM4FZKAZ.mjs → cli/index.js} +1073 -309
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/index.mjs +356 -0
  13. package/dist/cli/index.mjs.map +1 -0
  14. package/dist/client/config/from-env.d.ts +188 -0
  15. package/dist/client/config/from-env.d.ts.map +1 -0
  16. package/dist/client/config/index.d.ts +2 -0
  17. package/dist/client/config/index.d.ts.map +1 -0
  18. package/dist/client/errors.d.ts +67 -0
  19. package/dist/client/errors.d.ts.map +1 -0
  20. package/dist/client/kafka.client/admin/ops.d.ts +114 -0
  21. package/dist/client/kafka.client/admin/ops.d.ts.map +1 -0
  22. package/dist/client/kafka.client/consumer/features/delayed.d.ts +24 -0
  23. package/dist/client/kafka.client/consumer/features/delayed.d.ts.map +1 -0
  24. package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts +52 -0
  25. package/dist/client/kafka.client/consumer/features/dlq-replay.d.ts.map +1 -0
  26. package/dist/client/kafka.client/consumer/features/routed.d.ts +4 -0
  27. package/dist/client/kafka.client/consumer/features/routed.d.ts.map +1 -0
  28. package/dist/client/kafka.client/consumer/features/snapshot.d.ts +10 -0
  29. package/dist/client/kafka.client/consumer/features/snapshot.d.ts.map +1 -0
  30. package/dist/client/kafka.client/consumer/features/window.d.ts +5 -0
  31. package/dist/client/kafka.client/consumer/features/window.d.ts.map +1 -0
  32. package/dist/client/kafka.client/consumer/handler.d.ts +163 -0
  33. package/dist/client/kafka.client/consumer/handler.d.ts.map +1 -0
  34. package/dist/client/kafka.client/consumer/ops.d.ts +64 -0
  35. package/dist/client/kafka.client/consumer/ops.d.ts.map +1 -0
  36. package/dist/client/kafka.client/consumer/pipeline.d.ts +168 -0
  37. package/dist/client/kafka.client/consumer/pipeline.d.ts.map +1 -0
  38. package/dist/client/kafka.client/consumer/queue.d.ts +37 -0
  39. package/dist/client/kafka.client/consumer/queue.d.ts.map +1 -0
  40. package/dist/client/kafka.client/consumer/retry-topic.d.ts +68 -0
  41. package/dist/client/kafka.client/consumer/retry-topic.d.ts.map +1 -0
  42. package/dist/client/kafka.client/consumer/setup.d.ts +66 -0
  43. package/dist/client/kafka.client/consumer/setup.d.ts.map +1 -0
  44. package/dist/client/kafka.client/consumer/start.d.ts +7 -0
  45. package/dist/client/kafka.client/consumer/start.d.ts.map +1 -0
  46. package/dist/client/kafka.client/consumer/stop.d.ts +19 -0
  47. package/dist/client/kafka.client/consumer/stop.d.ts.map +1 -0
  48. package/dist/client/kafka.client/consumer/subscribe-retry.d.ts +4 -0
  49. package/dist/client/kafka.client/consumer/subscribe-retry.d.ts.map +1 -0
  50. package/dist/client/kafka.client/context.d.ts +75 -0
  51. package/dist/client/kafka.client/context.d.ts.map +1 -0
  52. package/dist/client/kafka.client/index.d.ts +155 -0
  53. package/dist/client/kafka.client/index.d.ts.map +1 -0
  54. package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts +61 -0
  55. package/dist/client/kafka.client/infra/circuit-breaker.manager.d.ts.map +1 -0
  56. package/dist/client/kafka.client/infra/dedup.store.d.ts +28 -0
  57. package/dist/client/kafka.client/infra/dedup.store.d.ts.map +1 -0
  58. package/dist/client/kafka.client/infra/inflight.tracker.d.ts +22 -0
  59. package/dist/client/kafka.client/infra/inflight.tracker.d.ts.map +1 -0
  60. package/dist/client/kafka.client/infra/metrics.manager.d.ts +67 -0
  61. package/dist/client/kafka.client/infra/metrics.manager.d.ts.map +1 -0
  62. package/dist/client/kafka.client/producer/lifecycle.d.ts +41 -0
  63. package/dist/client/kafka.client/producer/lifecycle.d.ts.map +1 -0
  64. package/dist/client/kafka.client/producer/ops.d.ts +79 -0
  65. package/dist/client/kafka.client/producer/ops.d.ts.map +1 -0
  66. package/dist/client/kafka.client/producer/send.d.ts +21 -0
  67. package/dist/client/kafka.client/producer/send.d.ts.map +1 -0
  68. package/dist/client/kafka.client/validate-options.d.ts +11 -0
  69. package/dist/client/kafka.client/validate-options.d.ts.map +1 -0
  70. package/dist/client/message/envelope.d.ts +105 -0
  71. package/dist/client/message/envelope.d.ts.map +1 -0
  72. package/dist/client/message/schema-registry.d.ts +124 -0
  73. package/dist/client/message/schema-registry.d.ts.map +1 -0
  74. package/dist/client/message/serde.d.ts +68 -0
  75. package/dist/client/message/serde.d.ts.map +1 -0
  76. package/dist/client/message/topic.d.ts +159 -0
  77. package/dist/client/message/topic.d.ts.map +1 -0
  78. package/dist/client/message/versioned-schema.d.ts +53 -0
  79. package/dist/client/message/versioned-schema.d.ts.map +1 -0
  80. package/dist/client/outbox/index.d.ts +4 -0
  81. package/dist/client/outbox/index.d.ts.map +1 -0
  82. package/dist/client/outbox/outbox.relay.d.ts +90 -0
  83. package/dist/client/outbox/outbox.relay.d.ts.map +1 -0
  84. package/dist/client/outbox/outbox.store.d.ts +42 -0
  85. package/dist/client/outbox/outbox.store.d.ts.map +1 -0
  86. package/dist/client/outbox/outbox.types.d.ts +144 -0
  87. package/dist/client/outbox/outbox.types.d.ts.map +1 -0
  88. package/dist/client/security/acl.d.ts +108 -0
  89. package/dist/client/security/acl.d.ts.map +1 -0
  90. package/dist/client/security/index.d.ts +5 -0
  91. package/dist/client/security/index.d.ts.map +1 -0
  92. package/dist/client/security/providers.d.ts +88 -0
  93. package/dist/client/security/providers.d.ts.map +1 -0
  94. package/dist/client/security/resolve-security.d.ts +19 -0
  95. package/dist/client/security/resolve-security.d.ts.map +1 -0
  96. package/dist/client/security/security.types.d.ts +76 -0
  97. package/dist/client/security/security.types.d.ts.map +1 -0
  98. package/dist/client/transport/confluent.transport.d.ts +32 -0
  99. package/dist/client/transport/confluent.transport.d.ts.map +1 -0
  100. package/dist/client/transport/transport.interface.d.ts +221 -0
  101. package/dist/client/transport/transport.interface.d.ts.map +1 -0
  102. package/dist/client/types/admin.interface.d.ts +174 -0
  103. package/dist/client/types/admin.interface.d.ts.map +1 -0
  104. package/dist/client/types/admin.types.d.ts +140 -0
  105. package/dist/client/types/admin.types.d.ts.map +1 -0
  106. package/dist/client/types/client.d.ts +21 -0
  107. package/dist/client/types/client.d.ts.map +1 -0
  108. package/dist/client/types/common.d.ts +84 -0
  109. package/dist/client/types/common.d.ts.map +1 -0
  110. package/dist/client/types/config.types.d.ts +167 -0
  111. package/dist/client/types/config.types.d.ts.map +1 -0
  112. package/dist/client/types/consumer.interface.d.ts +115 -0
  113. package/dist/client/types/consumer.interface.d.ts.map +1 -0
  114. package/dist/{consumer.types-fFCag3VJ.d.mts → client/types/consumer.types.d.ts} +62 -383
  115. package/dist/client/types/consumer.types.d.ts.map +1 -0
  116. package/dist/client/types/dedup.types.d.ts +50 -0
  117. package/dist/client/types/dedup.types.d.ts.map +1 -0
  118. package/dist/client/types/lifecycle.interface.d.ts +72 -0
  119. package/dist/client/types/lifecycle.interface.d.ts.map +1 -0
  120. package/dist/client/types/producer.interface.d.ts +52 -0
  121. package/dist/client/types/producer.interface.d.ts.map +1 -0
  122. package/dist/client/types/producer.types.d.ts +90 -0
  123. package/dist/client/types/producer.types.d.ts.map +1 -0
  124. package/dist/client/types.d.ts +8 -0
  125. package/dist/client/types.d.ts.map +1 -0
  126. package/dist/core.d.ts +13 -314
  127. package/dist/core.d.ts.map +1 -0
  128. package/dist/core.js +1466 -123
  129. package/dist/core.js.map +1 -1
  130. package/dist/core.mjs +45 -3
  131. package/dist/index.d.ts +7 -128
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +1483 -123
  134. package/dist/index.js.map +1 -1
  135. package/dist/index.mjs +62 -3
  136. package/dist/index.mjs.map +1 -1
  137. package/dist/nest/kafka.constants.d.ts +5 -0
  138. package/dist/nest/kafka.constants.d.ts.map +1 -0
  139. package/dist/nest/kafka.decorator.d.ts +49 -0
  140. package/dist/nest/kafka.decorator.d.ts.map +1 -0
  141. package/dist/nest/kafka.explorer.d.ts +17 -0
  142. package/dist/nest/kafka.explorer.d.ts.map +1 -0
  143. package/dist/nest/kafka.health.d.ts +7 -0
  144. package/dist/nest/kafka.health.d.ts.map +1 -0
  145. package/dist/nest/kafka.module.d.ts +61 -0
  146. package/dist/nest/kafka.module.d.ts.map +1 -0
  147. package/dist/otel.d.ts +83 -5
  148. package/dist/otel.d.ts.map +1 -0
  149. package/dist/otel.js +100 -6
  150. package/dist/otel.js.map +1 -1
  151. package/dist/otel.mjs +98 -5
  152. package/dist/otel.mjs.map +1 -1
  153. package/dist/serde.d.ts +157 -0
  154. package/dist/serde.d.ts.map +1 -0
  155. package/dist/serde.js +308 -0
  156. package/dist/serde.js.map +1 -0
  157. package/dist/serde.mjs +158 -0
  158. package/dist/serde.mjs.map +1 -0
  159. package/dist/testing/client.mock.d.ts +47 -0
  160. package/dist/testing/client.mock.d.ts.map +1 -0
  161. package/dist/testing/index.d.ts +4 -0
  162. package/dist/testing/index.d.ts.map +1 -0
  163. package/dist/testing/test.container.d.ts +63 -0
  164. package/dist/testing/test.container.d.ts.map +1 -0
  165. package/dist/{testing.d.mts → testing/transport.fake.d.ts} +7 -111
  166. package/dist/testing/transport.fake.d.ts.map +1 -0
  167. package/dist/testing.d.ts +2 -318
  168. package/dist/testing.d.ts.map +1 -0
  169. package/dist/testing.js +26 -0
  170. package/dist/testing.js.map +1 -1
  171. package/dist/testing.mjs +26 -0
  172. package/dist/testing.mjs.map +1 -1
  173. package/package.json +40 -8
  174. package/dist/chunk-SM4FZKAZ.mjs.map +0 -1
  175. package/dist/client-1irhGEu0.d.mts +0 -751
  176. package/dist/client-BpFjkHhr.d.ts +0 -751
  177. package/dist/consumer.types-fFCag3VJ.d.ts +0 -958
  178. package/dist/core.d.mts +0 -314
  179. package/dist/index.d.mts +0 -128
  180. package/dist/otel.d.mts +0 -27
package/dist/otel.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { BeforeConsumeResult, KafkaInstrumentation } from \"./client/types\";\nimport type { EventEnvelope } from \"./client/message/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n const activeSpans = new Map<string, ReturnType<typeof tracer.startSpan>>();\n\n return {\n beforeSend(_topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n const spanCtx = trace.setSpan(parentCtx, span);\n activeSpans.set(envelope.eventId, span);\n return {\n cleanup() {\n span.end();\n activeSpans.delete(envelope.eventId);\n },\n wrap(fn) {\n return context.with(spanCtx, fn);\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = activeSpans.get(envelope.eventId);\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAMO;AA0BA,SAAS,sBAA4C;AAC1D,QAAM,SAAS,iBAAM,UAAU,wBAAwB;AACvD,QAAM,cAAc,oBAAI,IAAiD;AAEzE,SAAO;AAAA,IACL,WAAW,QAAgB,SAAiC;AAC1D,6BAAY,OAAO,mBAAQ,OAAO,GAAG,OAAO;AAAA,IAC9C;AAAA,IAEA,UAAU,QAAgB;AAAA,IAI1B;AAAA,IAEA,cAAc,UAAmD;AAC/D,YAAM,YAAY,uBAAY,QAAQ,mBAAQ,OAAO,GAAG,SAAS,OAAO;AACxE,YAAM,OAAO,OAAO;AAAA,QAClB,iBAAiB,SAAS,KAAK;AAAA,QAC/B;AAAA,UACE,MAAM,oBAAS;AAAA,UACf,YAAY;AAAA,YACV,oBAAoB;AAAA,YACpB,8BAA8B,SAAS;AAAA,YACvC,wBAAwB,SAAS;AAAA,YACjC,6BAA6B,SAAS;AAAA,YACtC,0BAA0B,SAAS;AAAA,UACrC;AAAA,QACF;AAAA,QACA;AAAA,MACF;AACA,YAAM,UAAU,iBAAM,QAAQ,WAAW,IAAI;AAC7C,kBAAY,IAAI,SAAS,SAAS,IAAI;AACtC,aAAO;AAAA,QACL,UAAU;AACR,eAAK,IAAI;AACT,sBAAY,OAAO,SAAS,OAAO;AAAA,QACrC;AAAA,QACA,KAAK,IAAI;AACP,iBAAO,mBAAQ,KAAK,SAAS,EAAE;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAAA,IAEA,eAAe,UAA8B,OAAc;AACzD,YAAM,OAAO,YAAY,IAAI,SAAS,OAAO;AAC7C,UAAI,MAAM;AACR,aAAK,UAAU,EAAE,MAAM,0BAAe,OAAO,SAAS,MAAM,QAAQ,CAAC;AACrE,aAAK,gBAAgB,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n metrics,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { Meter, ObservableResult } from \"@opentelemetry/api\";\nimport type { BeforeConsumeResult, KafkaInstrumentation } from \"./client/types\";\nimport type { IKafkaAdmin } from \"./client/types/admin.interface\";\nimport type { TopicMapConstraint } from \"./client/types/common\";\nimport type { EventEnvelope } from \"./client/message/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n // Keyed by envelope object identity (not eventId) so two in-flight messages\n // that share an eventId cannot overwrite each other's span. WeakMap also\n // guarantees no leak if a cleanup path is ever missed.\n const activeSpans = new WeakMap<\n EventEnvelope<any>,\n ReturnType<typeof tracer.startSpan>\n >();\n\n return {\n beforeSend(_topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n const spanCtx = trace.setSpan(parentCtx, span);\n activeSpans.set(envelope, span);\n return {\n cleanup() {\n span.end();\n activeSpans.delete(envelope);\n },\n wrap(fn) {\n return context.with(spanCtx, fn);\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = activeSpans.get(envelope);\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n\n/**\n * Create a `KafkaInstrumentation` that records OpenTelemetry **metrics** for\n * both the send and consume paths.\n *\n * Requires `@opentelemetry/api` as a peer dependency. Instruments are created\n * once per instrumentation instance (not per message), so a single call to this\n * factory registers all counters/histograms exactly once.\n *\n * Recorded instruments (all under meter `@drarzter/kafka-client`):\n *\n * | Instrument | Type | Attributes | Recorded in |\n * |---|---|---|---|\n * | `kafka.client.messages.sent` | Counter | `topic` | `afterSend` |\n * | `kafka.client.messages.processed` | Counter | `topic` | `onMessage` |\n * | `kafka.client.messages.retried` | Counter | `topic` | `onRetry` |\n * | `kafka.client.messages.dlq` | Counter | `topic`, `reason` | `onDlq` |\n * | `kafka.client.messages.duplicate` | Counter | `topic`, `strategy` | `onDuplicate` |\n * | `kafka.client.consume.errors` | Counter | `topic` | `onConsumeError` |\n * | `kafka.client.consume.duration` | Histogram (ms) | `topic` | `beforeConsume` → `cleanup()` |\n *\n * Composes with `otelInstrumentation()` (traces): list both in\n * `instrumentation`. They share nothing and can be added in any order.\n *\n * @param options.meter - Override the meter used to create instruments.\n * Defaults to `metrics.getMeter(\"@drarzter/kafka-client\")`.\n *\n * @example\n * ```ts\n * import {\n * otelInstrumentation,\n * otelMetricsInstrumentation,\n * } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation(), otelMetricsInstrumentation()],\n * });\n * ```\n */\nexport function otelMetricsInstrumentation(options?: {\n meter?: Meter;\n}): KafkaInstrumentation {\n const meter = options?.meter ?? metrics.getMeter(\"@drarzter/kafka-client\");\n\n const sentCounter = meter.createCounter(\"kafka.client.messages.sent\", {\n description: \"Number of messages successfully sent to Kafka.\",\n });\n const processedCounter = meter.createCounter(\n \"kafka.client.messages.processed\",\n {\n description: \"Number of messages successfully processed by a consumer.\",\n },\n );\n const retriedCounter = meter.createCounter(\"kafka.client.messages.retried\", {\n description: \"Number of messages queued for retry.\",\n });\n const dlqCounter = meter.createCounter(\"kafka.client.messages.dlq\", {\n description: \"Number of messages routed to a DLQ topic.\",\n });\n const duplicateCounter = meter.createCounter(\n \"kafka.client.messages.duplicate\",\n {\n description: \"Number of Lamport-clock duplicate messages detected.\",\n },\n );\n const consumeErrorsCounter = meter.createCounter(\n \"kafka.client.consume.errors\",\n {\n description: \"Number of consumer handler errors.\",\n },\n );\n const consumeDuration = meter.createHistogram(\n \"kafka.client.consume.duration\",\n {\n description: \"Consumer handler duration.\",\n unit: \"ms\",\n },\n );\n\n return {\n afterSend(topic: string) {\n sentCounter.add(1, { topic });\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const topic = envelope.topic;\n const start = Date.now();\n return {\n cleanup() {\n consumeDuration.record(Date.now() - start, { topic });\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, _error: Error) {\n consumeErrorsCounter.add(1, { topic: envelope.topic });\n },\n\n onRetry(envelope: EventEnvelope<any>, _attempt: number, _max: number) {\n retriedCounter.add(1, { topic: envelope.topic });\n },\n\n onDlq(envelope: EventEnvelope<any>, reason: string) {\n dlqCounter.add(1, { topic: envelope.topic, reason });\n },\n\n onDuplicate(\n envelope: EventEnvelope<any>,\n strategy: \"drop\" | \"dlq\" | \"topic\",\n ) {\n duplicateCounter.add(1, { topic: envelope.topic, strategy });\n },\n\n onMessage(envelope: EventEnvelope<any>) {\n processedCounter.add(1, { topic: envelope.topic });\n },\n };\n}\n\n/**\n * Register an OpenTelemetry **ObservableGauge** `kafka.client.consumer.lag`\n * that reports per-partition consumer lag by polling `kafka.getConsumerLag()`\n * on each metric collection cycle.\n *\n * Requires `@opentelemetry/api` as a peer dependency. Accepts any object\n * implementing the `IKafkaAdmin` sub-interface (i.e. any `KafkaClient`).\n *\n * The async callback swallows errors silently — a broker query failure during\n * a collection cycle simply reports no lag samples for that cycle rather than\n * throwing inside the OTel metric reader.\n *\n * Gauge attributes: `topic`, `partition`, and `groupId` (empty string when the\n * client's default group is used).\n *\n * @param kafka - The client (or any `IKafkaAdmin`) to poll for lag.\n * @param options.meter - Override the meter. Defaults to\n * `metrics.getMeter(\"@drarzter/kafka-client\")`.\n * @param options.groupId - Consumer group to query. Defaults to the client's\n * constructor group.\n * @returns An unregister function that removes the observable callback. Call it\n * on shutdown to stop observing.\n *\n * @example\n * ```ts\n * import { otelLagGauge } from '@drarzter/kafka-client/otel';\n *\n * const unregister = otelLagGauge(kafka, { groupId: 'billing-service' });\n * // ...later, on shutdown:\n * unregister();\n * ```\n */\nexport function otelLagGauge<T extends TopicMapConstraint<T>>(\n kafka: IKafkaAdmin<T>,\n options?: { meter?: Meter; groupId?: string },\n): () => void {\n const meter = options?.meter ?? metrics.getMeter(\"@drarzter/kafka-client\");\n const groupId = options?.groupId;\n\n const gauge = meter.createObservableGauge(\"kafka.client.consumer.lag\", {\n description: \"Consumer group lag per topic partition.\",\n });\n\n const callback = async (result: ObservableResult) => {\n try {\n const lag = await kafka.getConsumerLag(groupId);\n for (const entry of lag) {\n result.observe(entry.lag, {\n topic: entry.topic,\n partition: entry.partition,\n groupId: groupId ?? \"\",\n });\n }\n } catch {\n // Swallow — a failed lag query should never break metric collection.\n }\n };\n\n gauge.addCallback(callback);\n\n return () => {\n gauge.removeCallback(callback);\n };\n}\n"],"mappings":"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","names":[]}
package/dist/otel.mjs CHANGED
@@ -5,12 +5,13 @@ import {
5
5
  trace,
6
6
  context,
7
7
  propagation,
8
+ metrics,
8
9
  SpanKind,
9
10
  SpanStatusCode
10
11
  } from "@opentelemetry/api";
11
12
  function otelInstrumentation() {
12
13
  const tracer = trace.getTracer("@drarzter/kafka-client");
13
- const activeSpans = /* @__PURE__ */ new Map();
14
+ const activeSpans = /* @__PURE__ */ new WeakMap();
14
15
  return {
15
16
  beforeSend(_topic, headers) {
16
17
  propagation.inject(context.active(), headers);
@@ -34,11 +35,11 @@ function otelInstrumentation() {
34
35
  parentCtx
35
36
  );
36
37
  const spanCtx = trace.setSpan(parentCtx, span);
37
- activeSpans.set(envelope.eventId, span);
38
+ activeSpans.set(envelope, span);
38
39
  return {
39
40
  cleanup() {
40
41
  span.end();
41
- activeSpans.delete(envelope.eventId);
42
+ activeSpans.delete(envelope);
42
43
  },
43
44
  wrap(fn) {
44
45
  return context.with(spanCtx, fn);
@@ -46,7 +47,7 @@ function otelInstrumentation() {
46
47
  };
47
48
  },
48
49
  onConsumeError(envelope, error) {
49
- const span = activeSpans.get(envelope.eventId);
50
+ const span = activeSpans.get(envelope);
50
51
  if (span) {
51
52
  span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
52
53
  span.recordException(error);
@@ -54,7 +55,99 @@ function otelInstrumentation() {
54
55
  }
55
56
  };
56
57
  }
58
+ function otelMetricsInstrumentation(options) {
59
+ const meter = options?.meter ?? metrics.getMeter("@drarzter/kafka-client");
60
+ const sentCounter = meter.createCounter("kafka.client.messages.sent", {
61
+ description: "Number of messages successfully sent to Kafka."
62
+ });
63
+ const processedCounter = meter.createCounter(
64
+ "kafka.client.messages.processed",
65
+ {
66
+ description: "Number of messages successfully processed by a consumer."
67
+ }
68
+ );
69
+ const retriedCounter = meter.createCounter("kafka.client.messages.retried", {
70
+ description: "Number of messages queued for retry."
71
+ });
72
+ const dlqCounter = meter.createCounter("kafka.client.messages.dlq", {
73
+ description: "Number of messages routed to a DLQ topic."
74
+ });
75
+ const duplicateCounter = meter.createCounter(
76
+ "kafka.client.messages.duplicate",
77
+ {
78
+ description: "Number of Lamport-clock duplicate messages detected."
79
+ }
80
+ );
81
+ const consumeErrorsCounter = meter.createCounter(
82
+ "kafka.client.consume.errors",
83
+ {
84
+ description: "Number of consumer handler errors."
85
+ }
86
+ );
87
+ const consumeDuration = meter.createHistogram(
88
+ "kafka.client.consume.duration",
89
+ {
90
+ description: "Consumer handler duration.",
91
+ unit: "ms"
92
+ }
93
+ );
94
+ return {
95
+ afterSend(topic) {
96
+ sentCounter.add(1, { topic });
97
+ },
98
+ beforeConsume(envelope) {
99
+ const topic = envelope.topic;
100
+ const start = Date.now();
101
+ return {
102
+ cleanup() {
103
+ consumeDuration.record(Date.now() - start, { topic });
104
+ }
105
+ };
106
+ },
107
+ onConsumeError(envelope, _error) {
108
+ consumeErrorsCounter.add(1, { topic: envelope.topic });
109
+ },
110
+ onRetry(envelope, _attempt, _max) {
111
+ retriedCounter.add(1, { topic: envelope.topic });
112
+ },
113
+ onDlq(envelope, reason) {
114
+ dlqCounter.add(1, { topic: envelope.topic, reason });
115
+ },
116
+ onDuplicate(envelope, strategy) {
117
+ duplicateCounter.add(1, { topic: envelope.topic, strategy });
118
+ },
119
+ onMessage(envelope) {
120
+ processedCounter.add(1, { topic: envelope.topic });
121
+ }
122
+ };
123
+ }
124
+ function otelLagGauge(kafka, options) {
125
+ const meter = options?.meter ?? metrics.getMeter("@drarzter/kafka-client");
126
+ const groupId = options?.groupId;
127
+ const gauge = meter.createObservableGauge("kafka.client.consumer.lag", {
128
+ description: "Consumer group lag per topic partition."
129
+ });
130
+ const callback = async (result) => {
131
+ try {
132
+ const lag = await kafka.getConsumerLag(groupId);
133
+ for (const entry of lag) {
134
+ result.observe(entry.lag, {
135
+ topic: entry.topic,
136
+ partition: entry.partition,
137
+ groupId: groupId ?? ""
138
+ });
139
+ }
140
+ } catch {
141
+ }
142
+ };
143
+ gauge.addCallback(callback);
144
+ return () => {
145
+ gauge.removeCallback(callback);
146
+ };
147
+ }
57
148
  export {
58
- otelInstrumentation
149
+ otelInstrumentation,
150
+ otelLagGauge,
151
+ otelMetricsInstrumentation
59
152
  };
60
153
  //# sourceMappingURL=otel.mjs.map
package/dist/otel.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { BeforeConsumeResult, KafkaInstrumentation } from \"./client/types\";\nimport type { EventEnvelope } from \"./client/message/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n const activeSpans = new Map<string, ReturnType<typeof tracer.startSpan>>();\n\n return {\n beforeSend(_topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n const spanCtx = trace.setSpan(parentCtx, span);\n activeSpans.set(envelope.eventId, span);\n return {\n cleanup() {\n span.end();\n activeSpans.delete(envelope.eventId);\n },\n wrap(fn) {\n return context.with(spanCtx, fn);\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = activeSpans.get(envelope.eventId);\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n"],"mappings":";;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA0BA,SAAS,sBAA4C;AAC1D,QAAM,SAAS,MAAM,UAAU,wBAAwB;AACvD,QAAM,cAAc,oBAAI,IAAiD;AAEzE,SAAO;AAAA,IACL,WAAW,QAAgB,SAAiC;AAC1D,kBAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;AAAA,IAC9C;AAAA,IAEA,UAAU,QAAgB;AAAA,IAI1B;AAAA,IAEA,cAAc,UAAmD;AAC/D,YAAM,YAAY,YAAY,QAAQ,QAAQ,OAAO,GAAG,SAAS,OAAO;AACxE,YAAM,OAAO,OAAO;AAAA,QAClB,iBAAiB,SAAS,KAAK;AAAA,QAC/B;AAAA,UACE,MAAM,SAAS;AAAA,UACf,YAAY;AAAA,YACV,oBAAoB;AAAA,YACpB,8BAA8B,SAAS;AAAA,YACvC,wBAAwB,SAAS;AAAA,YACjC,6BAA6B,SAAS;AAAA,YACtC,0BAA0B,SAAS;AAAA,UACrC;AAAA,QACF;AAAA,QACA;AAAA,MACF;AACA,YAAM,UAAU,MAAM,QAAQ,WAAW,IAAI;AAC7C,kBAAY,IAAI,SAAS,SAAS,IAAI;AACtC,aAAO;AAAA,QACL,UAAU;AACR,eAAK,IAAI;AACT,sBAAY,OAAO,SAAS,OAAO;AAAA,QACrC;AAAA,QACA,KAAK,IAAI;AACP,iBAAO,QAAQ,KAAK,SAAS,EAAE;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAAA,IAEA,eAAe,UAA8B,OAAc;AACzD,YAAM,OAAO,YAAY,IAAI,SAAS,OAAO;AAC7C,UAAI,MAAM;AACR,aAAK,UAAU,EAAE,MAAM,eAAe,OAAO,SAAS,MAAM,QAAQ,CAAC;AACrE,aAAK,gBAAgB,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/otel.ts"],"sourcesContent":["import {\n trace,\n context,\n propagation,\n metrics,\n SpanKind,\n SpanStatusCode,\n} from \"@opentelemetry/api\";\nimport type { Meter, ObservableResult } from \"@opentelemetry/api\";\nimport type { BeforeConsumeResult, KafkaInstrumentation } from \"./client/types\";\nimport type { IKafkaAdmin } from \"./client/types/admin.interface\";\nimport type { TopicMapConstraint } from \"./client/types/common\";\nimport type { EventEnvelope } from \"./client/message/envelope\";\n\n/**\n * Create a `KafkaInstrumentation` that automatically propagates\n * W3C Trace Context via Kafka headers.\n *\n * Requires `@opentelemetry/api` as a peer dependency.\n *\n * **Send path:** injects `traceparent` into message headers from the\n * active OpenTelemetry context.\n *\n * **Consume path:** extracts `traceparent` from message headers,\n * starts a `CONSUMER` span as a child of the extracted context,\n * and ends it when the handler completes.\n *\n * @example\n * ```ts\n * import { otelInstrumentation } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation()],\n * });\n * ```\n */\nexport function otelInstrumentation(): KafkaInstrumentation {\n const tracer = trace.getTracer(\"@drarzter/kafka-client\");\n // Keyed by envelope object identity (not eventId) so two in-flight messages\n // that share an eventId cannot overwrite each other's span. WeakMap also\n // guarantees no leak if a cleanup path is ever missed.\n const activeSpans = new WeakMap<\n EventEnvelope<any>,\n ReturnType<typeof tracer.startSpan>\n >();\n\n return {\n beforeSend(_topic: string, headers: Record<string, string>) {\n propagation.inject(context.active(), headers);\n },\n\n afterSend(_topic: string) {\n // Span management for producers is left to the caller's OTel setup.\n // We only inject context — creating producer spans here would be\n // inaccurate since buildSendPayload runs synchronously per-message.\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const parentCtx = propagation.extract(context.active(), envelope.headers);\n const span = tracer.startSpan(\n `kafka.consume ${envelope.topic}`,\n {\n kind: SpanKind.CONSUMER,\n attributes: {\n \"messaging.system\": \"kafka\",\n \"messaging.destination.name\": envelope.topic,\n \"messaging.message.id\": envelope.eventId,\n \"messaging.kafka.partition\": envelope.partition,\n \"messaging.kafka.offset\": envelope.offset,\n },\n },\n parentCtx,\n );\n const spanCtx = trace.setSpan(parentCtx, span);\n activeSpans.set(envelope, span);\n return {\n cleanup() {\n span.end();\n activeSpans.delete(envelope);\n },\n wrap(fn) {\n return context.with(spanCtx, fn);\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, error: Error) {\n const span = activeSpans.get(envelope);\n if (span) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.recordException(error);\n }\n },\n };\n}\n\n/**\n * Create a `KafkaInstrumentation` that records OpenTelemetry **metrics** for\n * both the send and consume paths.\n *\n * Requires `@opentelemetry/api` as a peer dependency. Instruments are created\n * once per instrumentation instance (not per message), so a single call to this\n * factory registers all counters/histograms exactly once.\n *\n * Recorded instruments (all under meter `@drarzter/kafka-client`):\n *\n * | Instrument | Type | Attributes | Recorded in |\n * |---|---|---|---|\n * | `kafka.client.messages.sent` | Counter | `topic` | `afterSend` |\n * | `kafka.client.messages.processed` | Counter | `topic` | `onMessage` |\n * | `kafka.client.messages.retried` | Counter | `topic` | `onRetry` |\n * | `kafka.client.messages.dlq` | Counter | `topic`, `reason` | `onDlq` |\n * | `kafka.client.messages.duplicate` | Counter | `topic`, `strategy` | `onDuplicate` |\n * | `kafka.client.consume.errors` | Counter | `topic` | `onConsumeError` |\n * | `kafka.client.consume.duration` | Histogram (ms) | `topic` | `beforeConsume` → `cleanup()` |\n *\n * Composes with `otelInstrumentation()` (traces): list both in\n * `instrumentation`. They share nothing and can be added in any order.\n *\n * @param options.meter - Override the meter used to create instruments.\n * Defaults to `metrics.getMeter(\"@drarzter/kafka-client\")`.\n *\n * @example\n * ```ts\n * import {\n * otelInstrumentation,\n * otelMetricsInstrumentation,\n * } from '@drarzter/kafka-client/otel';\n *\n * const kafka = new KafkaClient('my-app', 'my-group', brokers, {\n * instrumentation: [otelInstrumentation(), otelMetricsInstrumentation()],\n * });\n * ```\n */\nexport function otelMetricsInstrumentation(options?: {\n meter?: Meter;\n}): KafkaInstrumentation {\n const meter = options?.meter ?? metrics.getMeter(\"@drarzter/kafka-client\");\n\n const sentCounter = meter.createCounter(\"kafka.client.messages.sent\", {\n description: \"Number of messages successfully sent to Kafka.\",\n });\n const processedCounter = meter.createCounter(\n \"kafka.client.messages.processed\",\n {\n description: \"Number of messages successfully processed by a consumer.\",\n },\n );\n const retriedCounter = meter.createCounter(\"kafka.client.messages.retried\", {\n description: \"Number of messages queued for retry.\",\n });\n const dlqCounter = meter.createCounter(\"kafka.client.messages.dlq\", {\n description: \"Number of messages routed to a DLQ topic.\",\n });\n const duplicateCounter = meter.createCounter(\n \"kafka.client.messages.duplicate\",\n {\n description: \"Number of Lamport-clock duplicate messages detected.\",\n },\n );\n const consumeErrorsCounter = meter.createCounter(\n \"kafka.client.consume.errors\",\n {\n description: \"Number of consumer handler errors.\",\n },\n );\n const consumeDuration = meter.createHistogram(\n \"kafka.client.consume.duration\",\n {\n description: \"Consumer handler duration.\",\n unit: \"ms\",\n },\n );\n\n return {\n afterSend(topic: string) {\n sentCounter.add(1, { topic });\n },\n\n beforeConsume(envelope: EventEnvelope<any>): BeforeConsumeResult {\n const topic = envelope.topic;\n const start = Date.now();\n return {\n cleanup() {\n consumeDuration.record(Date.now() - start, { topic });\n },\n };\n },\n\n onConsumeError(envelope: EventEnvelope<any>, _error: Error) {\n consumeErrorsCounter.add(1, { topic: envelope.topic });\n },\n\n onRetry(envelope: EventEnvelope<any>, _attempt: number, _max: number) {\n retriedCounter.add(1, { topic: envelope.topic });\n },\n\n onDlq(envelope: EventEnvelope<any>, reason: string) {\n dlqCounter.add(1, { topic: envelope.topic, reason });\n },\n\n onDuplicate(\n envelope: EventEnvelope<any>,\n strategy: \"drop\" | \"dlq\" | \"topic\",\n ) {\n duplicateCounter.add(1, { topic: envelope.topic, strategy });\n },\n\n onMessage(envelope: EventEnvelope<any>) {\n processedCounter.add(1, { topic: envelope.topic });\n },\n };\n}\n\n/**\n * Register an OpenTelemetry **ObservableGauge** `kafka.client.consumer.lag`\n * that reports per-partition consumer lag by polling `kafka.getConsumerLag()`\n * on each metric collection cycle.\n *\n * Requires `@opentelemetry/api` as a peer dependency. Accepts any object\n * implementing the `IKafkaAdmin` sub-interface (i.e. any `KafkaClient`).\n *\n * The async callback swallows errors silently — a broker query failure during\n * a collection cycle simply reports no lag samples for that cycle rather than\n * throwing inside the OTel metric reader.\n *\n * Gauge attributes: `topic`, `partition`, and `groupId` (empty string when the\n * client's default group is used).\n *\n * @param kafka - The client (or any `IKafkaAdmin`) to poll for lag.\n * @param options.meter - Override the meter. Defaults to\n * `metrics.getMeter(\"@drarzter/kafka-client\")`.\n * @param options.groupId - Consumer group to query. Defaults to the client's\n * constructor group.\n * @returns An unregister function that removes the observable callback. Call it\n * on shutdown to stop observing.\n *\n * @example\n * ```ts\n * import { otelLagGauge } from '@drarzter/kafka-client/otel';\n *\n * const unregister = otelLagGauge(kafka, { groupId: 'billing-service' });\n * // ...later, on shutdown:\n * unregister();\n * ```\n */\nexport function otelLagGauge<T extends TopicMapConstraint<T>>(\n kafka: IKafkaAdmin<T>,\n options?: { meter?: Meter; groupId?: string },\n): () => void {\n const meter = options?.meter ?? metrics.getMeter(\"@drarzter/kafka-client\");\n const groupId = options?.groupId;\n\n const gauge = meter.createObservableGauge(\"kafka.client.consumer.lag\", {\n description: \"Consumer group lag per topic partition.\",\n });\n\n const callback = async (result: ObservableResult) => {\n try {\n const lag = await kafka.getConsumerLag(groupId);\n for (const entry of lag) {\n result.observe(entry.lag, {\n topic: entry.topic,\n partition: entry.partition,\n groupId: groupId ?? \"\",\n });\n }\n } catch {\n // Swallow — a failed lag query should never break metric collection.\n }\n };\n\n gauge.addCallback(callback);\n\n return () => {\n gauge.removeCallback(callback);\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,157 @@
1
+ import type { MessageSerde, SerdeContext } from "./client/message/serde";
2
+ import type { SchemaRegistryClient } from "./client/message/schema-registry";
3
+ export { JsonSerde } from "./client/message/serde";
4
+ export type { MessageSerde, SerdeContext } from "./client/message/serde";
5
+ export { SchemaRegistryClient } from "./client/message/schema-registry";
6
+ /** Injectable dynamic-import function — overridable in tests. */
7
+ type ImportFn = (specifier: string) => Promise<any>;
8
+ /** Options common to the registry-backed serdes. */
9
+ interface RegistrySerdeCommonOptions {
10
+ /** Schema Registry client used to resolve/register schema ids. */
11
+ registry: SchemaRegistryClient;
12
+ /**
13
+ * Subject name. Defaults to Confluent `TopicNameStrategy`
14
+ * (`<topic>-value` / `<topic>-key`). Provide a literal string or a
15
+ * function of the {@link SerdeContext} to override.
16
+ */
17
+ subject?: string | ((ctx: SerdeContext) => string);
18
+ /**
19
+ * Register `schema` on first serialize to obtain its id (dev-friendly).
20
+ * Default `false` → the id is resolved via `getLatestSchema(subject)`.
21
+ */
22
+ autoRegister?: boolean;
23
+ /** @internal Injectable dynamic import for tests. */
24
+ importFn?: ImportFn;
25
+ }
26
+ /** Options for {@link avroSerde}. */
27
+ export interface AvroSerdeOptions extends RegistrySerdeCommonOptions {
28
+ /**
29
+ * Avro schema (JSON string or object) used to serialize, and as the
30
+ * write-schema fallback. Required to serialize; deserialize resolves the
31
+ * writer schema from the registry via the wire-format id.
32
+ */
33
+ schema?: string | object;
34
+ }
35
+ /**
36
+ * Confluent-wire-format **Avro** serde backed by a Schema Registry.
37
+ *
38
+ * Produces/consumes the exact byte layout Java/Go clients use, so this library
39
+ * interoperates with them through a shared registry:
40
+ *
41
+ * ```
42
+ * [magic 0x00][schema id: 4-byte big-endian][avro binary]
43
+ * ```
44
+ *
45
+ * Uses the optional peer dependency [`avsc`](https://www.npmjs.com/package/avsc)
46
+ * via dynamic import — install it to enable Avro:
47
+ *
48
+ * ```bash
49
+ * npm install avsc
50
+ * ```
51
+ *
52
+ * - **serialize**: resolves the subject from the context, obtains the schema id
53
+ * (`registerSchema` when `autoRegister`, else `getLatestSchema`), Avro-encodes
54
+ * `value` against `schema`, and frames the bytes.
55
+ * - **deserialize**: reads the magic byte + big-endian id, resolves the writer
56
+ * schema via `registry.getSchemaById(id)` (cached forever), and Avro-decodes
57
+ * the remainder. The reader schema equals the writer schema in v1 — full
58
+ * reader-schema resolution / schema evolution is a future enhancement.
59
+ *
60
+ * The parsed `avsc` type is cached per schema string so repeated messages don't
61
+ * re-parse the schema.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { avroSerde } from '@drarzter/kafka-client/serde';
66
+ * import { SchemaRegistryClient } from '@drarzter/kafka-client';
67
+ *
68
+ * const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
69
+ * const orderSchema = {
70
+ * type: 'record',
71
+ * name: 'Order',
72
+ * fields: [{ name: 'orderId', type: 'string' }, { name: 'amount', type: 'double' }],
73
+ * };
74
+ *
75
+ * // Per-topic:
76
+ * const Orders = topic('orders')
77
+ * .serde(avroSerde({ registry, schema: orderSchema }))
78
+ * .type<Order>();
79
+ *
80
+ * // Client-wide:
81
+ * const kafka = new KafkaClient(id, group, brokers, {
82
+ * serde: avroSerde({ registry, schema: orderSchema }),
83
+ * });
84
+ * ```
85
+ */
86
+ export declare function avroSerde(options: AvroSerdeOptions): MessageSerde;
87
+ /** Options for {@link protobufSerde}. */
88
+ export interface ProtobufSerdeOptions extends RegistrySerdeCommonOptions {
89
+ /**
90
+ * Fully-qualified Protobuf message name to encode/decode,
91
+ * e.g. `"com.acme.orders.Order"`.
92
+ */
93
+ messageType: string;
94
+ /**
95
+ * `.proto` source string defining {@link ProtobufSerdeOptions.messageType}.
96
+ * Required to serialize; deserialize resolves the writer `.proto` from the
97
+ * registry via the wire-format id.
98
+ */
99
+ schema?: string;
100
+ }
101
+ /**
102
+ * Confluent-wire-format **Protobuf** serde backed by a Schema Registry.
103
+ *
104
+ * Produces/consumes the exact byte layout Java/Go clients use:
105
+ *
106
+ * ```
107
+ * [magic 0x00][schema id: 4-byte big-endian][message-index][protobuf binary]
108
+ * ```
109
+ *
110
+ * The **message-index** identifies which message type within the `.proto` file
111
+ * was used. For the first/top-level message type (index `[0]`) Confluent writes
112
+ * the single byte `0x00`. This serde implements that top-level case only —
113
+ * multiple/nested message types are a documented v1 limitation and cause a
114
+ * clear error on deserialize.
115
+ *
116
+ * Uses the optional peer dependency
117
+ * [`protobufjs`](https://www.npmjs.com/package/protobufjs) via dynamic import —
118
+ * install it to enable Protobuf:
119
+ *
120
+ * ```bash
121
+ * npm install protobufjs
122
+ * ```
123
+ *
124
+ * - **serialize**: obtains the schema id (`registerSchema` when `autoRegister`,
125
+ * else `getLatestSchema`), encodes `value` with the `protobufjs` `Type`, and
126
+ * frames it with the `0x00` message-index byte.
127
+ * - **deserialize**: reads the magic byte + big-endian id + message-index (which
128
+ * must be the single `0x00` byte), resolves the writer `.proto` via
129
+ * `registry.getSchemaById(id)` (cached forever), and decodes the remainder.
130
+ *
131
+ * The parsed `protobufjs` `Type` is cached per schema string so repeated
132
+ * messages don't re-parse the `.proto`.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * import { protobufSerde } from '@drarzter/kafka-client/serde';
137
+ * import { SchemaRegistryClient } from '@drarzter/kafka-client';
138
+ *
139
+ * const registry = new SchemaRegistryClient({ baseUrl: 'http://localhost:8081' });
140
+ * const proto = `
141
+ * syntax = "proto3";
142
+ * message Order { string orderId = 1; double amount = 2; }
143
+ * `;
144
+ *
145
+ * // Per-topic:
146
+ * const Orders = topic('orders')
147
+ * .serde(protobufSerde({ registry, schema: proto, messageType: 'Order' }))
148
+ * .type<Order>();
149
+ *
150
+ * // Client-wide:
151
+ * const kafka = new KafkaClient(id, group, brokers, {
152
+ * serde: protobufSerde({ registry, schema: proto, messageType: 'Order' }),
153
+ * });
154
+ * ```
155
+ */
156
+ export declare function protobufSerde(options: ProtobufSerdeOptions): MessageSerde;
157
+ //# sourceMappingURL=serde.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serde.d.ts","sourceRoot":"","sources":["../src/serde.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAI7E,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAKxE,iEAAiE;AACjE,KAAK,QAAQ,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAqEpD,oDAAoD;AACpD,UAAU,0BAA0B;IAClC,kEAAkE;IAClE,QAAQ,EAAE,oBAAoB,CAAC;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,YAAY,KAAK,MAAM,CAAC,CAAC;IACnD;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAiB,SAAQ,0BAA0B;IAClE;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,CA4DjE;AAED,yCAAyC;AACzC,MAAM,WAAW,oBAAqB,SAAQ,0BAA0B;IACtE;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,YAAY,CAiFzE"}