Skip to content

Commit 44f75d3

Browse files
committed
fix(irc): preserve one-unit chunk limits
1 parent df96d5c commit 44f75d3

2 files changed

Lines changed: 23 additions & 3 deletions

File tree

extensions/irc/src/client.surrogate.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ describe("irc client PRIVMSG chunking", () => {
142142
client.close();
143143
});
144144

145+
it("preserves one-unit chunks for BMP text", async () => {
146+
const { client, socket } = await connectReadyClient(1);
147+
148+
client.sendPrivmsg("#room", "ABC");
149+
150+
expect(privmsgBodies(socket)).toEqual(["A", "B", "C"]);
151+
152+
client.close();
153+
});
154+
145155
it("splits an all-emoji message into whole emoji when the budget is one code unit", async () => {
146156
const { client, socket } = await connectReadyClient(1);
147157
const text = "\u{1F642}\u{1F642}\u{1F642}";

extensions/irc/src/client.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ import {
1111
sanitizeIrcTarget,
1212
} from "./protocol.js";
1313

14+
function sliceIrcPrivmsgChunk(text: string, end: number): string {
15+
const sliced = sliceUtf16Safe(text, 0, end);
16+
if (sliced || end <= 0) {
17+
return sliced;
18+
}
19+
// A one-unit budget cannot contain an astral code point. Emit that point
20+
// whole so chunking advances without changing one-unit behavior for BMP text.
21+
const firstCodePoint = text.codePointAt(0);
22+
return firstCodePoint !== undefined && firstCodePoint > 0xffff ? text.slice(0, 2) : sliced;
23+
}
24+
1425
const IRC_ERROR_CODES = new Set(["432", "464", "465"]);
1526
const IRC_NICK_COLLISION_CODES = new Set(["433", "436"]);
1627

@@ -108,8 +119,7 @@ export function buildIrcNickServCommands(options?: IrcNickServOptions): string[]
108119

109120
export async function connectIrcClient(options: IrcClientOptions): Promise<IrcClient> {
110121
const timeoutMs = options.connectTimeoutMs != null ? options.connectTimeoutMs : 15000;
111-
// Two UTF-16 code units are the smallest budget that can hold one full code point.
112-
const messageChunkMaxChars = Math.max(2, Math.floor(options.messageChunkMaxChars ?? 350));
122+
const messageChunkMaxChars = Math.max(1, Math.floor(options.messageChunkMaxChars ?? 350));
113123

114124
if (!options.host.trim()) {
115125
throw new Error("IRC host is required");
@@ -221,7 +231,7 @@ export async function connectIrcClient(options: IrcClientOptions): Promise<IrcCl
221231
if (splitAt < Math.floor(messageChunkMaxChars / 2)) {
222232
splitAt = messageChunkMaxChars;
223233
}
224-
chunk = sliceUtf16Safe(chunk, 0, splitAt).trim();
234+
chunk = sliceIrcPrivmsgChunk(chunk, splitAt).trim();
225235
}
226236
if (!chunk) {
227237
break;

0 commit comments

Comments
 (0)