151 lines
4.1 KiB
TypeScript
151 lines
4.1 KiB
TypeScript
import { StrictMessageContent } from "../utils";
|
|
import Timeout = NodeJS.Timeout;
|
|
import { calculateEmbedSize } from "./calculateEmbedSize";
|
|
|
|
type ConsumeFn = (part: StrictMessageContent) => void;
|
|
|
|
type ContentType = "mixed" | "plain" | "embeds";
|
|
|
|
export type MessageBufferContent = Pick<StrictMessageContent, "content" | "embeds">;
|
|
|
|
type Chunk = {
|
|
type: ContentType;
|
|
content: MessageBufferContent;
|
|
};
|
|
|
|
export interface MessageBufferOpts {
|
|
consume?: ConsumeFn;
|
|
timeout?: number;
|
|
textSeparator?: string;
|
|
}
|
|
|
|
const MAX_CHARS_PER_MESSAGE = 2000;
|
|
const MAX_EMBED_LENGTH_PER_MESSAGE = 6000;
|
|
const MAX_EMBEDS_PER_MESSAGE = 10;
|
|
|
|
/**
|
|
* Allows buffering and automatic partitioning of message contents. Useful for e.g. high volume log channels, message chunking, etc.
|
|
*/
|
|
export class MessageBuffer {
|
|
protected autoConsumeFn: ConsumeFn | null = null;
|
|
|
|
protected timeoutMs: number | null = null;
|
|
|
|
protected textSeparator: string = "";
|
|
|
|
protected chunk: Chunk | null = null;
|
|
|
|
protected chunkTimeout: Timeout | null = null;
|
|
|
|
protected finalizedChunks: MessageBufferContent[] = [];
|
|
|
|
constructor(opts: MessageBufferOpts = {}) {
|
|
if (opts.consume) {
|
|
this.autoConsumeFn = opts.consume;
|
|
}
|
|
|
|
if (opts.timeout) {
|
|
this.timeoutMs = opts.timeout;
|
|
}
|
|
|
|
if (opts.textSeparator) {
|
|
this.textSeparator = opts.textSeparator;
|
|
}
|
|
}
|
|
|
|
push(content: MessageBufferContent): void {
|
|
let contentType: ContentType;
|
|
if (content.content && !content.embeds?.length) {
|
|
contentType = "plain";
|
|
} else if (content.embeds?.length && !content.content) {
|
|
contentType = "embeds";
|
|
} else {
|
|
contentType = "mixed";
|
|
}
|
|
|
|
// Plain text can't be merged with mixed or embeds
|
|
if (contentType === "plain" && this.chunk && this.chunk.type !== "plain") {
|
|
this.startNewChunk(contentType);
|
|
}
|
|
// Mixed can't be merged at all
|
|
if (contentType === "mixed" && this.chunk) {
|
|
this.startNewChunk(contentType);
|
|
}
|
|
|
|
if (!this.chunk) this.startNewChunk(contentType);
|
|
const chunk = this.chunk!;
|
|
|
|
if (content.content) {
|
|
if (chunk.content.content && chunk.content.content.length + content.content.length > MAX_CHARS_PER_MESSAGE) {
|
|
this.startNewChunk(contentType);
|
|
}
|
|
|
|
if (chunk.content.content == null || chunk.content.content === "") {
|
|
chunk.content.content = content.content;
|
|
} else {
|
|
chunk.content.content += this.textSeparator + content.content;
|
|
}
|
|
}
|
|
|
|
if (content.embeds) {
|
|
if (chunk.content.embeds) {
|
|
if (chunk.content.embeds.length + content.embeds.length > MAX_EMBEDS_PER_MESSAGE) {
|
|
this.startNewChunk(contentType);
|
|
} else {
|
|
const existingEmbedsLength = chunk.content.embeds.reduce((sum, embed) => sum + calculateEmbedSize(embed), 0);
|
|
const embedsLength = content.embeds.reduce((sum, embed) => sum + calculateEmbedSize(embed), 0);
|
|
if (existingEmbedsLength + embedsLength > MAX_EMBED_LENGTH_PER_MESSAGE) {
|
|
this.startNewChunk(contentType);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (chunk.content.embeds == null) chunk.content.embeds = [];
|
|
chunk.content.embeds.push(...content.embeds);
|
|
}
|
|
}
|
|
|
|
protected startNewChunk(type: ContentType): void {
|
|
if (this.chunk) {
|
|
this.finalizeChunk();
|
|
}
|
|
this.chunk = {
|
|
type,
|
|
content: {},
|
|
};
|
|
if (this.timeoutMs) {
|
|
this.chunkTimeout = setTimeout(() => this.finalizeChunk(), this.timeoutMs);
|
|
}
|
|
}
|
|
|
|
protected finalizeChunk(): void {
|
|
if (!this.chunk) return;
|
|
const chunk = this.chunk;
|
|
this.chunk = null;
|
|
|
|
if (this.chunkTimeout) {
|
|
clearTimeout(this.chunkTimeout);
|
|
this.chunkTimeout = null;
|
|
}
|
|
|
|
// Discard empty chunks
|
|
if (!chunk.content.content && !chunk.content.embeds?.length) return;
|
|
|
|
if (this.autoConsumeFn) {
|
|
this.autoConsumeFn(chunk.content);
|
|
return;
|
|
}
|
|
|
|
this.finalizedChunks.push(chunk.content);
|
|
}
|
|
|
|
consume(): StrictMessageContent[] {
|
|
return Array.from(this.finalizedChunks);
|
|
this.finalizedChunks = [];
|
|
}
|
|
|
|
finalizeAndConsume(): StrictMessageContent[] {
|
|
this.finalizeChunk();
|
|
return this.consume();
|
|
}
|
|
}
|