refactor!: decouple Raknet MOTD parsing and response shaping

The previous implementation of the RakNet ping was monolithic, mixing
socket management, raw packet validation, and data transformation into
a single, complex flow.

This refactor introduces a clear, multi-stage
processing pipeline that separates these concerns. The logic is now
broken down into multi-stage pipeline: extracting the MOTD string
from the raw pong packet -> parsing that string into a raw
object -> transforming the raw data into a
user-friendly response object.

Additionally, the socket handling logic is improved
with idempotent cleanup function to prevent resource
leaks or race conditions.

As part of this overhaul, external TypeScript definition (`.d.ts`)
files have been removed in favor of rich JSDoc annotations.

BREAKING CHANGE: The structure of the resolved `BedrockPingResponse`
object has been significantly changed to improve clarity and
consistency.
This commit is contained in:
2025-06-16 01:09:41 +03:00
parent cbaa1a3e3e
commit 3c2c049c19
7 changed files with 258 additions and 321 deletions

View File

@ -34,13 +34,13 @@ describe("bedrock.js", () => {
vi.useRealTimers();
});
it("should ping a server and parse MOTD", async () => {
it("should ping a 3rd party server and parse MOTD", async () => {
const host = "play.example.com";
const options = { port: 25565, timeout: 10000 };
const pingPromise = pingBedrock(host, options);
const motd =
"MCPE;§l§b§f;0;1337;1096;1999;-37530542056358113;oasys-pe.ru;Adventure;1";
"MCPE;§l§b§f  §eГриф§7, §cДуэли§7, §aКейсы;0;1337;1070;1999;-138584171542148188;oasys-pe.ru;Adventure;1";
const mockPongPacket = createMockPongPacket(motd);
mockSocket.emit("message", mockPongPacket);
@ -58,18 +58,75 @@ describe("bedrock.js", () => {
expect(mockSocket.close).toHaveBeenCalled();
expect(result).toEqual({
edition: "MCPE",
name: "§l§b§f",
version: { protocolVersion: 0, minecraftVersion: "1337" },
players: { online: 1096, max: 1999 },
serverId: "-37530542056358113",
mapName: "oasys-pe.ru",
gameMode: "Adventure",
name: "§l§b§f  §eГриф§7, §cДуэли§7, §aКейсы",
levelName: "oasys-pe.ru",
gamemode: "Adventure",
version: {
protocol: 0,
minecraft: "1337",
},
players: {
online: 1070,
max: 1999,
},
port: {
v4: undefined,
v6: undefined,
},
guid: -138584171542148188n,
isNintendoLimited: false,
isEditorModeEnabled: undefined,
});
});
it("should ping a BDS server with default `server.properties` and parse MOTD", async () => {
const host = "play.example.com";
const options = { port: 25565, timeout: 10000 };
const pingPromise = pingBedrock(host, options);
const motd =
"MCPE;Dedicated Server;800;1.21.84;0;10;11546321190880321782;Bedrock level;Survival;1;19132;19133;0;";
const mockPongPacket = createMockPongPacket(motd);
mockSocket.emit("message", mockPongPacket);
const result = await pingPromise;
expect(dgram.createSocket).toHaveBeenCalledWith("udp4");
expect(mockSocket.send).toHaveBeenCalledWith(
expect.any(Buffer),
0,
33,
options.port,
host
);
expect(mockSocket.close).toHaveBeenCalled();
expect(result).toEqual({
edition: "MCPE",
name: "Dedicated Server",
levelName: "Bedrock level",
gamemode: "Survival",
version: {
protocol: 800,
minecraft: "1.21.84",
},
players: {
online: 0,
max: 10,
},
port: {
v4: 19132,
v6: 19133,
},
guid: 11546321190880321782n,
isNintendoLimited: false,
isEditorModeEnabled: false,
});
});
describe("errors", () => {
it("should throw an error if host is not provided", () => {
expect(() => pingBedrock(null)).toThrow("Host argument is not provided");
expect(() => pingBedrock(null)).toThrow("Host argument is required");
});
it("should reject on socket timeout", async () => {