blob: 097a9ed1041f08cb0c397a49f04418cb8d5302fa [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { type Id } from "../identifier.utils.js";
import { type ValueOf, reverseRecord } from "../../type.utils.js";
import { toDate } from "../serialize.utils.js";
import { serializeGetOffset, type Consumer } from "../offset/offset.utils.js";
import { deserializeHeaders, type ParsedHeaderEntry } from "./header.utils.js";
import { Transform, type TransformCallback } from "node:stream";
import {
BATCH_HEADER_SIZE,
FRAME_HEADER_SIZE,
deserializeBatchHeader,
deserializeFrameHeader,
type IggyMessageHeader,
} from "./iggy-header.utils.js";
/**
* Enumeration of message polling strategies.
*/
export const PollingStrategyKind = {
/** Poll from a specific offset */
Offset: 1,
/** Poll from a specific timestamp */
Timestamp: 2,
/** Poll from the first message */
First: 3,
/** Poll from the last message */
Last: 4,
/** Poll the next unconsumed message */
Next: 5,
} as const;
/** Type alias for the PollingStrategyKind object */
export type PollingStrategyKind = typeof PollingStrategyKind;
/** String literal type of polling strategy names */
export type PollingStrategyKindId = keyof PollingStrategyKind;
/** Numeric values of polling strategies */
export type PollingStrategyKindValue = ValueOf<PollingStrategyKind>;
/** Polling from a specific offset */
export type OffsetPollingStrategy = {
kind: PollingStrategyKind["Offset"];
/** Offset to start polling from */
value: bigint;
};
/** Polling from a specific timestamp */
export type TimestampPollingStrategy = {
kind: PollingStrategyKind["Timestamp"];
/** Timestamp in microseconds */
value: bigint;
};
/** Polling from the first message */
export type FirstPollingStrategy = {
kind: PollingStrategyKind["First"];
value: 0n;
};
/** Polling from the last message */
export type LastPollingStrategy = {
kind: PollingStrategyKind["Last"];
value: 0n;
};
/** Polling the next unconsumed message */
export type NextPollingStrategy = {
kind: PollingStrategyKind["Next"];
value: 0n;
};
/** Union of all polling strategy types */
export type PollingStrategy =
| OffsetPollingStrategy
| TimestampPollingStrategy
| FirstPollingStrategy
| LastPollingStrategy
| NextPollingStrategy;
/** Next polling strategy constant */
const Next: NextPollingStrategy = {
kind: PollingStrategyKind.Next,
value: 0n,
};
/** First polling strategy constant */
const First: FirstPollingStrategy = {
kind: PollingStrategyKind.First,
value: 0n,
};
/** Last polling strategy constant */
const Last: LastPollingStrategy = {
kind: PollingStrategyKind.Last,
value: 0n,
};
/**
* Creates an offset polling strategy.
*
* @param n - Offset to start from
* @returns Offset polling strategy
*/
const Offset = (n: bigint): OffsetPollingStrategy => ({
kind: PollingStrategyKind.Offset,
value: n,
});
/**
* Creates a timestamp polling strategy.
*
* @param n - Timestamp in microseconds
* @returns Timestamp polling strategy
*/
const Timestamp = (n: bigint): TimestampPollingStrategy => ({
kind: PollingStrategyKind.Timestamp,
value: n,
});
/**
* Factory object for creating polling strategies.
*/
export const PollingStrategy = {
Next,
First,
Last,
Offset,
Timestamp,
};
/**
* Serializes a poll messages command payload.
*
* @param streamId - Stream identifier
* @param topicId - Topic identifier
* @param consumer - Consumer configuration
* @param partitionId - Partition ID (null for all partitions)
* @param pollingStrategy - Strategy for selecting messages
* @param count - Maximum number of messages to poll
* @param autocommit - Whether to auto-commit offset after polling
* @returns Serialized command payload
*/
export const serializePollMessages = (
streamId: Id,
topicId: Id,
consumer: Consumer,
partitionId: number | null,
pollingStrategy: PollingStrategy, // default to OffsetPollingStrategy
count = 10,
autocommit = false,
) => {
const b = Buffer.allocUnsafe(14);
b.writeUInt8(pollingStrategy.kind, 0);
b.writeBigUInt64LE(pollingStrategy.value, 1);
b.writeUInt32LE(count, 9);
b.writeUInt8(!!autocommit ? 1 : 0, 13);
return Buffer.concat([
serializeGetOffset(streamId, topicId, consumer, partitionId),
b,
]);
};
/**
* Enumeration of message states.
*/
export const MessageState = {
/** Message is available for consumption */
Available: 1,
/** Message is temporarily unavailable */
Unavailable: 10,
/** Message processing failed */
Poisoned: 20,
/** Message is scheduled for deletion */
MarkedForDeletion: 30,
};
/** Type alias for the MessageState object */
type MessageState = typeof MessageState;
/** String literal type of message state names */
type MessageStateId = keyof MessageState;
/** Numeric values of message states */
type MessageStateValue = ValueOf<MessageState>;
/** Reverse mapping from numeric value to state name */
const ReverseMessageState = reverseRecord(MessageState);
/**
* Maps a numeric message state to its string identifier.
*
* @param k - Numeric state value
* @returns State identifier string
* @throws Error if the state is unknown
*/
export const mapMessageState = (k: number): MessageStateId => {
if (!ReverseMessageState[k as MessageStateValue])
throw new Error(`unknown message state: ${k}`);
return ReverseMessageState[k as MessageStateValue];
};
/**
* A polled message with headers, payload, and user headers.
*/
export type Message = {
/** Iggy message header metadata */
headers: IggyMessageHeader;
/** Message payload data */
payload: Buffer;
/** User-defined headers */
userHeaders: ParsedHeaderEntry[];
};
/**
* Response from a poll messages command.
*/
export type PollMessagesResponse = {
/** Partition the messages came from */
partitionId: number;
/** Current offset in the partition */
currentOffset: bigint;
/** Number of messages returned */
count: number;
/** Array of polled messages */
messages: Message[];
};
/**
* A message frame decoded from a batch record, with absolute values
* resolved against the record header.
*/
export type BatchMessage = {
/** Frame checksum, passed through unverified */
checksum: bigint;
/** Unique message identifier */
id: bigint;
/** Absolute message offset within the partition */
offset: bigint;
/** Server timestamp of the record in microseconds */
timestamp: bigint;
/** Message origin timestamp in microseconds */
originTimestamp: bigint;
/** Message payload data */
payload: Buffer;
/** Raw user header bytes */
userHeaders: Buffer;
};
/**
* Deserializes batch records into message frames with absolute values.
* Each record is [batch header][frames], walked by the record's batch
* length. Records may be server-sliced, so the first frame of a record
* can carry a non-zero offset delta.
*
* @param b - Buffer containing serialized batch records
* @param pos - Starting position in the buffer
* @returns Array of decoded message frames
* @throws Error if a record or frame is malformed
*/
export const deserializeBatchMessages = (
b: Buffer,
pos = 0,
): BatchMessage[] => {
const messages: BatchMessage[] = [];
const len = b.length;
while (pos < len) {
if (pos + BATCH_HEADER_SIZE > len)
throw new Error("truncated batch header in poll response");
const batch = deserializeBatchHeader(b, pos);
const recordEnd = pos + Number(batch.batchLength);
if (Number(batch.batchLength) < BATCH_HEADER_SIZE || recordEnd > len)
throw new Error(
`invalid batch length ${batch.batchLength} in poll response`,
);
pos += BATCH_HEADER_SIZE;
while (pos < recordEnd) {
if (pos + FRAME_HEADER_SIZE > recordEnd)
throw new Error("truncated message frame in poll response");
const frame = deserializeFrameHeader(b, pos);
if (frame.reserved !== 0n)
throw new Error(
`non-zero reserved field ${frame.reserved} in message frame`,
);
const payloadEnd = pos + FRAME_HEADER_SIZE + frame.payloadLength;
const frameEnd = payloadEnd + frame.userHeadersLength;
if (frameEnd > recordEnd)
throw new Error("truncated message frame in poll response");
messages.push({
checksum: frame.checksum,
id: frame.id,
offset: batch.baseOffset + BigInt(frame.offsetDelta),
timestamp: batch.baseTimestamp,
originTimestamp:
batch.originTimestamp + BigInt(frame.timestampDelta),
payload: b.subarray(pos + FRAME_HEADER_SIZE, payloadEnd),
userHeaders: b.subarray(payloadEnd, frameEnd),
});
pos = frameEnd;
}
}
return messages;
};
/**
* Deserializes an array of messages from a buffer of batch records.
*
* @param b - Buffer containing serialized batch records
* @param pos - Starting position in the buffer
* @returns Array of deserialized messages
*/
export const deserializeMessages = (b: Buffer, pos = 0): Message[] =>
deserializeBatchMessages(b, pos).map((message) => ({
headers: {
checksum: message.checksum,
id: message.id,
offset: message.offset,
timestamp: toDate(message.timestamp),
originTimestamp: toDate(message.originTimestamp),
userHeadersLength: message.userHeaders.length,
payloadLength: message.payload.length,
reserved: 0n,
},
payload: message.payload,
userHeaders:
message.userHeaders.length > 0
? deserializeHeaders(message.userHeaders)
: ([] as ParsedHeaderEntry[]),
}));
/**
* Deserializes a poll messages response from a buffer.
*
* @param r - Response buffer
* @param pos - Starting position
* @returns Parsed PollMessagesResponse
*/
export const deserializePollMessages = (r: Buffer, pos = 0) => {
const partitionId = r.readUInt32LE(pos);
const currentOffset = r.readBigUInt64LE(pos + 4);
const count = r.readUInt32LE(pos + 12);
const messages = deserializeMessages(r, pos + 16);
return {
partitionId,
currentOffset,
count,
messages,
};
};
/**
* Creates a Transform stream for deserializing poll messages responses.
*
* @returns Transform stream that outputs PollMessagesResponse objects
*/
export const deserializePollMessagesTransform = () =>
new Transform({
objectMode: true,
transform(chunk: Buffer, encoding: BufferEncoding, cb: TransformCallback) {
try {
return cb(null, deserializePollMessages(chunk));
} catch (err: unknown) {
cb(
new Error("deserializePollMessage::transform error", { cause: err }),
null,
);
}
},
});