@hytaleone/query 1.0.0 → 1.0.1

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.
package/dist/index.cjs CHANGED
@@ -176,9 +176,15 @@ function sendQuery(host, port, type, timeout) {
176
176
  return new Promise((resolve, reject) => {
177
177
  const socket = import_node_dgram.default.createSocket("udp4");
178
178
  let timeoutHandle;
179
+ let closed = false;
179
180
  const cleanup = () => {
181
+ if (closed) return;
182
+ closed = true;
180
183
  clearTimeout(timeoutHandle);
181
- socket.close();
184
+ try {
185
+ socket.close();
186
+ } catch {
187
+ }
182
188
  };
183
189
  socket.on("message", (msg) => {
184
190
  cleanup();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/query.ts","../src/protocol.ts"],"sourcesContent":["export { query } from './query.js';\nexport type {\n ServerInfo,\n ServerInfoFull,\n Player,\n Plugin,\n QueryOptions,\n} from './types.js';\n","import dgram from 'node:dgram';\nimport {\n buildRequest,\n parseBasicResponse,\n parseFullResponse,\n TYPE_BASIC,\n TYPE_FULL,\n} from './protocol.js';\nimport type { QueryOptions, ServerInfo, ServerInfoFull } from './types.js';\n\nconst DEFAULT_TIMEOUT = 5000;\n\n/**\n * Send a UDP query and wait for response.\n */\nfunction sendQuery(\n host: string,\n port: number,\n type: number,\n timeout: number\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const socket = dgram.createSocket('udp4');\n let timeoutHandle: NodeJS.Timeout;\n\n const cleanup = () => {\n clearTimeout(timeoutHandle);\n socket.close();\n };\n\n socket.on('message', (msg) => {\n cleanup();\n resolve(msg);\n });\n\n socket.on('error', (err) => {\n cleanup();\n reject(err);\n });\n\n timeoutHandle = setTimeout(() => {\n cleanup();\n reject(new Error(`Query timeout after ${timeout}ms`));\n }, timeout);\n\n const request = buildRequest(type);\n socket.send(request, port, host, (err) => {\n if (err) {\n cleanup();\n reject(err);\n }\n });\n });\n}\n\n/**\n * Query a Hytale server for information.\n *\n * @param host - Server hostname or IP address\n * @param port - Server port (default: 5520)\n * @param options - Query options\n * @returns Server information (full if options.full is true)\n *\n * @example\n * ```typescript\n * // Basic query\n * const info = await query('play.example.com', 5520);\n * console.log(`${info.serverName}: ${info.currentPlayers}/${info.maxPlayers}`);\n *\n * // Full query (includes players + plugins)\n * const full = await query('play.example.com', 5520, { full: true });\n * console.log('Players:', full.players.map(p => p.name).join(', '));\n * ```\n */\nexport async function query(\n host: string,\n port?: number,\n options?: QueryOptions & { full?: false }\n): Promise<ServerInfo>;\nexport async function query(\n host: string,\n port: number,\n options: QueryOptions & { full: true }\n): Promise<ServerInfoFull>;\nexport async function query(\n host: string,\n port = 5520,\n options: QueryOptions & { full?: boolean } = {}\n): Promise<ServerInfo | ServerInfoFull> {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n const type = options.full ? TYPE_FULL : TYPE_BASIC;\n const response = await sendQuery(host, port, type, timeout);\n\n if (options.full) {\n return parseFullResponse(response);\n }\n return parseBasicResponse(response);\n}\n","import type { Player, Plugin, ServerInfo, ServerInfoFull } from './types.js';\n\n// Protocol constants\nexport const REQUEST_MAGIC = Buffer.from('HYQUERY\\0', 'ascii');\nexport const RESPONSE_MAGIC = Buffer.from('HYREPLY\\0', 'ascii');\nexport const TYPE_BASIC = 0x00;\nexport const TYPE_FULL = 0x01;\n\n/**\n * Build a query request packet.\n */\nexport function buildRequest(type: number): Buffer {\n const buf = Buffer.alloc(REQUEST_MAGIC.length + 1);\n REQUEST_MAGIC.copy(buf, 0);\n buf[REQUEST_MAGIC.length] = type;\n return buf;\n}\n\n/**\n * Buffer reader helper for parsing responses.\n */\nclass BufferReader {\n private offset = 0;\n\n constructor(private buf: Buffer) {}\n\n readBytes(length: number): Buffer {\n const slice = this.buf.subarray(this.offset, this.offset + length);\n this.offset += length;\n return slice;\n }\n\n readUInt16LE(): number {\n const value = this.buf.readUInt16LE(this.offset);\n this.offset += 2;\n return value;\n }\n\n readInt32LE(): number {\n const value = this.buf.readInt32LE(this.offset);\n this.offset += 4;\n return value;\n }\n\n readBigInt64BE(): bigint {\n const value = this.buf.readBigInt64BE(this.offset);\n this.offset += 8;\n return value;\n }\n\n readBoolean(): boolean {\n return this.buf[this.offset++] !== 0;\n }\n\n readString(): string {\n const length = this.readUInt16LE();\n const bytes = this.readBytes(length);\n return bytes.toString('utf8');\n }\n\n readUUID(): string {\n const msb = this.readBigInt64BE();\n const lsb = this.readBigInt64BE();\n return formatUUID(msb, lsb);\n }\n\n get remaining(): number {\n return this.buf.length - this.offset;\n }\n}\n\n/**\n * Format UUID from most/least significant bits.\n */\nfunction formatUUID(msb: bigint, lsb: bigint): string {\n const toHex = (n: bigint): string => {\n if (n < 0n) {\n n = BigInt.asUintN(64, n);\n }\n return n.toString(16).padStart(16, '0');\n };\n\n const hex = toHex(msb) + toHex(lsb);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/**\n * Validate response magic bytes.\n */\nexport function validateResponse(buf: Buffer): boolean {\n if (buf.length < RESPONSE_MAGIC.length + 1) {\n return false;\n }\n return buf.subarray(0, RESPONSE_MAGIC.length).equals(RESPONSE_MAGIC);\n}\n\n/**\n * Parse a basic query response.\n */\nexport function parseBasicResponse(buf: Buffer): ServerInfo {\n if (!validateResponse(buf)) {\n throw new Error('Invalid response: magic mismatch');\n }\n\n const reader = new BufferReader(buf);\n\n // Skip magic\n reader.readBytes(RESPONSE_MAGIC.length);\n\n // Skip type\n reader.readBytes(1);\n\n return {\n serverName: reader.readString(),\n motd: reader.readString(),\n currentPlayers: reader.readInt32LE(),\n maxPlayers: reader.readInt32LE(),\n hostPort: reader.readUInt16LE(),\n version: reader.readString(),\n protocolVersion: reader.readInt32LE(),\n protocolHash: reader.readString(),\n };\n}\n\n/**\n * Parse a full query response.\n */\nexport function parseFullResponse(buf: Buffer): ServerInfoFull {\n if (!validateResponse(buf)) {\n throw new Error('Invalid response: magic mismatch');\n }\n\n const reader = new BufferReader(buf);\n\n // Skip magic\n reader.readBytes(RESPONSE_MAGIC.length);\n\n // Skip type\n reader.readBytes(1);\n\n // Base info\n const serverName = reader.readString();\n const motd = reader.readString();\n const currentPlayers = reader.readInt32LE();\n const maxPlayers = reader.readInt32LE();\n const hostPort = reader.readUInt16LE();\n const version = reader.readString();\n const protocolVersion = reader.readInt32LE();\n const protocolHash = reader.readString();\n\n // Player list\n const playerCount = reader.readInt32LE();\n const players: Player[] = [];\n for (let i = 0; i < playerCount; i++) {\n players.push({\n name: reader.readString(),\n uuid: reader.readUUID(),\n });\n }\n\n // Plugin list\n const pluginCount = reader.readInt32LE();\n const plugins: Plugin[] = [];\n for (let i = 0; i < pluginCount; i++) {\n plugins.push({\n id: reader.readString(),\n version: reader.readString(),\n enabled: reader.readBoolean(),\n });\n }\n\n return {\n serverName,\n motd,\n currentPlayers,\n maxPlayers,\n hostPort,\n version,\n protocolVersion,\n protocolHash,\n players,\n plugins,\n };\n}\n"],"mappings":"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","names":["dgram"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/query.ts","../src/protocol.ts"],"sourcesContent":["export { query } from './query.js';\nexport type {\n ServerInfo,\n ServerInfoFull,\n Player,\n Plugin,\n QueryOptions,\n} from './types.js';\n","import dgram from 'node:dgram';\nimport {\n buildRequest,\n parseBasicResponse,\n parseFullResponse,\n TYPE_BASIC,\n TYPE_FULL,\n} from './protocol.js';\nimport type { QueryOptions, ServerInfo, ServerInfoFull } from './types.js';\n\nconst DEFAULT_TIMEOUT = 5000;\n\n/**\n * Send a UDP query and wait for response.\n */\nfunction sendQuery(\n host: string,\n port: number,\n type: number,\n timeout: number\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const socket = dgram.createSocket('udp4');\n let timeoutHandle: NodeJS.Timeout;\n let closed = false;\n\n const cleanup = () => {\n if (closed) return;\n closed = true;\n clearTimeout(timeoutHandle);\n try {\n socket.close();\n } catch {\n // Already closed\n }\n };\n\n socket.on('message', (msg) => {\n cleanup();\n resolve(msg);\n });\n\n socket.on('error', (err) => {\n cleanup();\n reject(err);\n });\n\n timeoutHandle = setTimeout(() => {\n cleanup();\n reject(new Error(`Query timeout after ${timeout}ms`));\n }, timeout);\n\n const request = buildRequest(type);\n socket.send(request, port, host, (err) => {\n if (err) {\n cleanup();\n reject(err);\n }\n });\n });\n}\n\n/**\n * Query a Hytale server for information.\n *\n * @param host - Server hostname or IP address\n * @param port - Server port (default: 5520)\n * @param options - Query options\n * @returns Server information (full if options.full is true)\n *\n * @example\n * ```typescript\n * // Basic query\n * const info = await query('play.example.com', 5520);\n * console.log(`${info.serverName}: ${info.currentPlayers}/${info.maxPlayers}`);\n *\n * // Full query (includes players + plugins)\n * const full = await query('play.example.com', 5520, { full: true });\n * console.log('Players:', full.players.map(p => p.name).join(', '));\n * ```\n */\nexport async function query(\n host: string,\n port?: number,\n options?: QueryOptions & { full?: false }\n): Promise<ServerInfo>;\nexport async function query(\n host: string,\n port: number,\n options: QueryOptions & { full: true }\n): Promise<ServerInfoFull>;\nexport async function query(\n host: string,\n port = 5520,\n options: QueryOptions & { full?: boolean } = {}\n): Promise<ServerInfo | ServerInfoFull> {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n const type = options.full ? TYPE_FULL : TYPE_BASIC;\n const response = await sendQuery(host, port, type, timeout);\n\n if (options.full) {\n return parseFullResponse(response);\n }\n return parseBasicResponse(response);\n}\n","import type { Player, Plugin, ServerInfo, ServerInfoFull } from './types.js';\n\n// Protocol constants\nexport const REQUEST_MAGIC = Buffer.from('HYQUERY\\0', 'ascii');\nexport const RESPONSE_MAGIC = Buffer.from('HYREPLY\\0', 'ascii');\nexport const TYPE_BASIC = 0x00;\nexport const TYPE_FULL = 0x01;\n\n/**\n * Build a query request packet.\n */\nexport function buildRequest(type: number): Buffer {\n const buf = Buffer.alloc(REQUEST_MAGIC.length + 1);\n REQUEST_MAGIC.copy(buf, 0);\n buf[REQUEST_MAGIC.length] = type;\n return buf;\n}\n\n/**\n * Buffer reader helper for parsing responses.\n */\nclass BufferReader {\n private offset = 0;\n\n constructor(private buf: Buffer) {}\n\n readBytes(length: number): Buffer {\n const slice = this.buf.subarray(this.offset, this.offset + length);\n this.offset += length;\n return slice;\n }\n\n readUInt16LE(): number {\n const value = this.buf.readUInt16LE(this.offset);\n this.offset += 2;\n return value;\n }\n\n readInt32LE(): number {\n const value = this.buf.readInt32LE(this.offset);\n this.offset += 4;\n return value;\n }\n\n readBigInt64BE(): bigint {\n const value = this.buf.readBigInt64BE(this.offset);\n this.offset += 8;\n return value;\n }\n\n readBoolean(): boolean {\n return this.buf[this.offset++] !== 0;\n }\n\n readString(): string {\n const length = this.readUInt16LE();\n const bytes = this.readBytes(length);\n return bytes.toString('utf8');\n }\n\n readUUID(): string {\n const msb = this.readBigInt64BE();\n const lsb = this.readBigInt64BE();\n return formatUUID(msb, lsb);\n }\n\n get remaining(): number {\n return this.buf.length - this.offset;\n }\n}\n\n/**\n * Format UUID from most/least significant bits.\n */\nfunction formatUUID(msb: bigint, lsb: bigint): string {\n const toHex = (n: bigint): string => {\n if (n < 0n) {\n n = BigInt.asUintN(64, n);\n }\n return n.toString(16).padStart(16, '0');\n };\n\n const hex = toHex(msb) + toHex(lsb);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/**\n * Validate response magic bytes.\n */\nexport function validateResponse(buf: Buffer): boolean {\n if (buf.length < RESPONSE_MAGIC.length + 1) {\n return false;\n }\n return buf.subarray(0, RESPONSE_MAGIC.length).equals(RESPONSE_MAGIC);\n}\n\n/**\n * Parse a basic query response.\n */\nexport function parseBasicResponse(buf: Buffer): ServerInfo {\n if (!validateResponse(buf)) {\n throw new Error('Invalid response: magic mismatch');\n }\n\n const reader = new BufferReader(buf);\n\n // Skip magic\n reader.readBytes(RESPONSE_MAGIC.length);\n\n // Skip type\n reader.readBytes(1);\n\n return {\n serverName: reader.readString(),\n motd: reader.readString(),\n currentPlayers: reader.readInt32LE(),\n maxPlayers: reader.readInt32LE(),\n hostPort: reader.readUInt16LE(),\n version: reader.readString(),\n protocolVersion: reader.readInt32LE(),\n protocolHash: reader.readString(),\n };\n}\n\n/**\n * Parse a full query response.\n */\nexport function parseFullResponse(buf: Buffer): ServerInfoFull {\n if (!validateResponse(buf)) {\n throw new Error('Invalid response: magic mismatch');\n }\n\n const reader = new BufferReader(buf);\n\n // Skip magic\n reader.readBytes(RESPONSE_MAGIC.length);\n\n // Skip type\n reader.readBytes(1);\n\n // Base info\n const serverName = reader.readString();\n const motd = reader.readString();\n const currentPlayers = reader.readInt32LE();\n const maxPlayers = reader.readInt32LE();\n const hostPort = reader.readUInt16LE();\n const version = reader.readString();\n const protocolVersion = reader.readInt32LE();\n const protocolHash = reader.readString();\n\n // Player list\n const playerCount = reader.readInt32LE();\n const players: Player[] = [];\n for (let i = 0; i < playerCount; i++) {\n players.push({\n name: reader.readString(),\n uuid: reader.readUUID(),\n });\n }\n\n // Plugin list\n const pluginCount = reader.readInt32LE();\n const plugins: Plugin[] = [];\n for (let i = 0; i < pluginCount; i++) {\n plugins.push({\n id: reader.readString(),\n version: reader.readString(),\n enabled: reader.readBoolean(),\n });\n }\n\n return {\n serverName,\n motd,\n currentPlayers,\n maxPlayers,\n hostPort,\n version,\n protocolVersion,\n protocolHash,\n players,\n plugins,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,wBAAkB;;;ACGX,IAAM,gBAAgB,OAAO,KAAK,aAAa,OAAO;AACtD,IAAM,iBAAiB,OAAO,KAAK,aAAa,OAAO;AACvD,IAAM,aAAa;AACnB,IAAM,YAAY;AAKlB,SAAS,aAAa,MAAsB;AACjD,QAAM,MAAM,OAAO,MAAM,cAAc,SAAS,CAAC;AACjD,gBAAc,KAAK,KAAK,CAAC;AACzB,MAAI,cAAc,MAAM,IAAI;AAC5B,SAAO;AACT;AAKA,IAAM,eAAN,MAAmB;AAAA,EAGjB,YAAoB,KAAa;AAAb;AAAA,EAAc;AAAA,EAF1B,SAAS;AAAA,EAIjB,UAAU,QAAwB;AAChC,UAAM,QAAQ,KAAK,IAAI,SAAS,KAAK,QAAQ,KAAK,SAAS,MAAM;AACjE,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA,EAEA,eAAuB;AACrB,UAAM,QAAQ,KAAK,IAAI,aAAa,KAAK,MAAM;AAC/C,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA,EAEA,cAAsB;AACpB,UAAM,QAAQ,KAAK,IAAI,YAAY,KAAK,MAAM;AAC9C,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA,EAEA,iBAAyB;AACvB,UAAM,QAAQ,KAAK,IAAI,eAAe,KAAK,MAAM;AACjD,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA,EAEA,cAAuB;AACrB,WAAO,KAAK,IAAI,KAAK,QAAQ,MAAM;AAAA,EACrC;AAAA,EAEA,aAAqB;AACnB,UAAM,SAAS,KAAK,aAAa;AACjC,UAAM,QAAQ,KAAK,UAAU,MAAM;AACnC,WAAO,MAAM,SAAS,MAAM;AAAA,EAC9B;AAAA,EAEA,WAAmB;AACjB,UAAM,MAAM,KAAK,eAAe;AAChC,UAAM,MAAM,KAAK,eAAe;AAChC,WAAO,WAAW,KAAK,GAAG;AAAA,EAC5B;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK,IAAI,SAAS,KAAK;AAAA,EAChC;AACF;AAKA,SAAS,WAAW,KAAa,KAAqB;AACpD,QAAM,QAAQ,CAAC,MAAsB;AACnC,QAAI,IAAI,IAAI;AACV,UAAI,OAAO,QAAQ,IAAI,CAAC;AAAA,IAC1B;AACA,WAAO,EAAE,SAAS,EAAE,EAAE,SAAS,IAAI,GAAG;AAAA,EACxC;AAEA,QAAM,MAAM,MAAM,GAAG,IAAI,MAAM,GAAG;AAClC,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,EAAE,CAAC;AAC1G;AAKO,SAAS,iBAAiB,KAAsB;AACrD,MAAI,IAAI,SAAS,eAAe,SAAS,GAAG;AAC1C,WAAO;AAAA,EACT;AACA,SAAO,IAAI,SAAS,GAAG,eAAe,MAAM,EAAE,OAAO,cAAc;AACrE;AAKO,SAAS,mBAAmB,KAAyB;AAC1D,MAAI,CAAC,iBAAiB,GAAG,GAAG;AAC1B,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAEA,QAAM,SAAS,IAAI,aAAa,GAAG;AAGnC,SAAO,UAAU,eAAe,MAAM;AAGtC,SAAO,UAAU,CAAC;AAElB,SAAO;AAAA,IACL,YAAY,OAAO,WAAW;AAAA,IAC9B,MAAM,OAAO,WAAW;AAAA,IACxB,gBAAgB,OAAO,YAAY;AAAA,IACnC,YAAY,OAAO,YAAY;AAAA,IAC/B,UAAU,OAAO,aAAa;AAAA,IAC9B,SAAS,OAAO,WAAW;AAAA,IAC3B,iBAAiB,OAAO,YAAY;AAAA,IACpC,cAAc,OAAO,WAAW;AAAA,EAClC;AACF;AAKO,SAAS,kBAAkB,KAA6B;AAC7D,MAAI,CAAC,iBAAiB,GAAG,GAAG;AAC1B,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAEA,QAAM,SAAS,IAAI,aAAa,GAAG;AAGnC,SAAO,UAAU,eAAe,MAAM;AAGtC,SAAO,UAAU,CAAC;AAGlB,QAAM,aAAa,OAAO,WAAW;AACrC,QAAM,OAAO,OAAO,WAAW;AAC/B,QAAM,iBAAiB,OAAO,YAAY;AAC1C,QAAM,aAAa,OAAO,YAAY;AACtC,QAAM,WAAW,OAAO,aAAa;AACrC,QAAM,UAAU,OAAO,WAAW;AAClC,QAAM,kBAAkB,OAAO,YAAY;AAC3C,QAAM,eAAe,OAAO,WAAW;AAGvC,QAAM,cAAc,OAAO,YAAY;AACvC,QAAM,UAAoB,CAAC;AAC3B,WAAS,IAAI,GAAG,IAAI,aAAa,KAAK;AACpC,YAAQ,KAAK;AAAA,MACX,MAAM,OAAO,WAAW;AAAA,MACxB,MAAM,OAAO,SAAS;AAAA,IACxB,CAAC;AAAA,EACH;AAGA,QAAM,cAAc,OAAO,YAAY;AACvC,QAAM,UAAoB,CAAC;AAC3B,WAAS,IAAI,GAAG,IAAI,aAAa,KAAK;AACpC,YAAQ,KAAK;AAAA,MACX,IAAI,OAAO,WAAW;AAAA,MACtB,SAAS,OAAO,WAAW;AAAA,MAC3B,SAAS,OAAO,YAAY;AAAA,IAC9B,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AD7KA,IAAM,kBAAkB;AAKxB,SAAS,UACP,MACA,MACA,MACA,SACiB;AACjB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAS,kBAAAA,QAAM,aAAa,MAAM;AACxC,QAAI;AACJ,QAAI,SAAS;AAEb,UAAM,UAAU,MAAM;AACpB,UAAI,OAAQ;AACZ,eAAS;AACT,mBAAa,aAAa;AAC1B,UAAI;AACF,eAAO,MAAM;AAAA,MACf,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,WAAO,GAAG,WAAW,CAAC,QAAQ;AAC5B,cAAQ;AACR,cAAQ,GAAG;AAAA,IACb,CAAC;AAED,WAAO,GAAG,SAAS,CAAC,QAAQ;AAC1B,cAAQ;AACR,aAAO,GAAG;AAAA,IACZ,CAAC;AAED,oBAAgB,WAAW,MAAM;AAC/B,cAAQ;AACR,aAAO,IAAI,MAAM,uBAAuB,OAAO,IAAI,CAAC;AAAA,IACtD,GAAG,OAAO;AAEV,UAAM,UAAU,aAAa,IAAI;AACjC,WAAO,KAAK,SAAS,MAAM,MAAM,CAAC,QAAQ;AACxC,UAAI,KAAK;AACP,gBAAQ;AACR,eAAO,GAAG;AAAA,MACZ;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AA+BA,eAAsB,MACpB,MACA,OAAO,MACP,UAA6C,CAAC,GACR;AACtC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,OAAO,QAAQ,OAAO,YAAY;AACxC,QAAM,WAAW,MAAM,UAAU,MAAM,MAAM,MAAM,OAAO;AAE1D,MAAI,QAAQ,MAAM;AAChB,WAAO,kBAAkB,QAAQ;AAAA,EACnC;AACA,SAAO,mBAAmB,QAAQ;AACpC;","names":["dgram"]}
package/dist/index.js CHANGED
@@ -140,9 +140,15 @@ function sendQuery(host, port, type, timeout) {
140
140
  return new Promise((resolve, reject) => {
141
141
  const socket = dgram.createSocket("udp4");
142
142
  let timeoutHandle;
143
+ let closed = false;
143
144
  const cleanup = () => {
145
+ if (closed) return;
146
+ closed = true;
144
147
  clearTimeout(timeoutHandle);
145
- socket.close();
148
+ try {
149
+ socket.close();
150
+ } catch {
151
+ }
146
152
  };
147
153
  socket.on("message", (msg) => {
148
154
  cleanup();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/query.ts","../src/protocol.ts"],"sourcesContent":["import dgram from 'node:dgram';\nimport {\n buildRequest,\n parseBasicResponse,\n parseFullResponse,\n TYPE_BASIC,\n TYPE_FULL,\n} from './protocol.js';\nimport type { QueryOptions, ServerInfo, ServerInfoFull } from './types.js';\n\nconst DEFAULT_TIMEOUT = 5000;\n\n/**\n * Send a UDP query and wait for response.\n */\nfunction sendQuery(\n host: string,\n port: number,\n type: number,\n timeout: number\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const socket = dgram.createSocket('udp4');\n let timeoutHandle: NodeJS.Timeout;\n\n const cleanup = () => {\n clearTimeout(timeoutHandle);\n socket.close();\n };\n\n socket.on('message', (msg) => {\n cleanup();\n resolve(msg);\n });\n\n socket.on('error', (err) => {\n cleanup();\n reject(err);\n });\n\n timeoutHandle = setTimeout(() => {\n cleanup();\n reject(new Error(`Query timeout after ${timeout}ms`));\n }, timeout);\n\n const request = buildRequest(type);\n socket.send(request, port, host, (err) => {\n if (err) {\n cleanup();\n reject(err);\n }\n });\n });\n}\n\n/**\n * Query a Hytale server for information.\n *\n * @param host - Server hostname or IP address\n * @param port - Server port (default: 5520)\n * @param options - Query options\n * @returns Server information (full if options.full is true)\n *\n * @example\n * ```typescript\n * // Basic query\n * const info = await query('play.example.com', 5520);\n * console.log(`${info.serverName}: ${info.currentPlayers}/${info.maxPlayers}`);\n *\n * // Full query (includes players + plugins)\n * const full = await query('play.example.com', 5520, { full: true });\n * console.log('Players:', full.players.map(p => p.name).join(', '));\n * ```\n */\nexport async function query(\n host: string,\n port?: number,\n options?: QueryOptions & { full?: false }\n): Promise<ServerInfo>;\nexport async function query(\n host: string,\n port: number,\n options: QueryOptions & { full: true }\n): Promise<ServerInfoFull>;\nexport async function query(\n host: string,\n port = 5520,\n options: QueryOptions & { full?: boolean } = {}\n): Promise<ServerInfo | ServerInfoFull> {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n const type = options.full ? TYPE_FULL : TYPE_BASIC;\n const response = await sendQuery(host, port, type, timeout);\n\n if (options.full) {\n return parseFullResponse(response);\n }\n return parseBasicResponse(response);\n}\n","import type { Player, Plugin, ServerInfo, ServerInfoFull } from './types.js';\n\n// Protocol constants\nexport const REQUEST_MAGIC = Buffer.from('HYQUERY\\0', 'ascii');\nexport const RESPONSE_MAGIC = Buffer.from('HYREPLY\\0', 'ascii');\nexport const TYPE_BASIC = 0x00;\nexport const TYPE_FULL = 0x01;\n\n/**\n * Build a query request packet.\n */\nexport function buildRequest(type: number): Buffer {\n const buf = Buffer.alloc(REQUEST_MAGIC.length + 1);\n REQUEST_MAGIC.copy(buf, 0);\n buf[REQUEST_MAGIC.length] = type;\n return buf;\n}\n\n/**\n * Buffer reader helper for parsing responses.\n */\nclass BufferReader {\n private offset = 0;\n\n constructor(private buf: Buffer) {}\n\n readBytes(length: number): Buffer {\n const slice = this.buf.subarray(this.offset, this.offset + length);\n this.offset += length;\n return slice;\n }\n\n readUInt16LE(): number {\n const value = this.buf.readUInt16LE(this.offset);\n this.offset += 2;\n return value;\n }\n\n readInt32LE(): number {\n const value = this.buf.readInt32LE(this.offset);\n this.offset += 4;\n return value;\n }\n\n readBigInt64BE(): bigint {\n const value = this.buf.readBigInt64BE(this.offset);\n this.offset += 8;\n return value;\n }\n\n readBoolean(): boolean {\n return this.buf[this.offset++] !== 0;\n }\n\n readString(): string {\n const length = this.readUInt16LE();\n const bytes = this.readBytes(length);\n return bytes.toString('utf8');\n }\n\n readUUID(): string {\n const msb = this.readBigInt64BE();\n const lsb = this.readBigInt64BE();\n return formatUUID(msb, lsb);\n }\n\n get remaining(): number {\n return this.buf.length - this.offset;\n }\n}\n\n/**\n * Format UUID from most/least significant bits.\n */\nfunction formatUUID(msb: bigint, lsb: bigint): string {\n const toHex = (n: bigint): string => {\n if (n < 0n) {\n n = BigInt.asUintN(64, n);\n }\n return n.toString(16).padStart(16, '0');\n };\n\n const hex = toHex(msb) + toHex(lsb);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/**\n * Validate response magic bytes.\n */\nexport function validateResponse(buf: Buffer): boolean {\n if (buf.length < RESPONSE_MAGIC.length + 1) {\n return false;\n }\n return buf.subarray(0, RESPONSE_MAGIC.length).equals(RESPONSE_MAGIC);\n}\n\n/**\n * Parse a basic query response.\n */\nexport function parseBasicResponse(buf: Buffer): ServerInfo {\n if (!validateResponse(buf)) {\n throw new Error('Invalid response: magic mismatch');\n }\n\n const reader = new BufferReader(buf);\n\n // Skip magic\n reader.readBytes(RESPONSE_MAGIC.length);\n\n // Skip type\n reader.readBytes(1);\n\n return {\n serverName: reader.readString(),\n motd: reader.readString(),\n currentPlayers: reader.readInt32LE(),\n maxPlayers: reader.readInt32LE(),\n hostPort: reader.readUInt16LE(),\n version: reader.readString(),\n protocolVersion: reader.readInt32LE(),\n protocolHash: reader.readString(),\n };\n}\n\n/**\n * Parse a full query response.\n */\nexport function parseFullResponse(buf: Buffer): ServerInfoFull {\n if (!validateResponse(buf)) {\n throw new Error('Invalid response: magic mismatch');\n }\n\n const reader = new BufferReader(buf);\n\n // Skip magic\n reader.readBytes(RESPONSE_MAGIC.length);\n\n // Skip type\n reader.readBytes(1);\n\n // Base info\n const serverName = reader.readString();\n const motd = reader.readString();\n const currentPlayers = reader.readInt32LE();\n const maxPlayers = reader.readInt32LE();\n const hostPort = reader.readUInt16LE();\n const version = reader.readString();\n const protocolVersion = reader.readInt32LE();\n const protocolHash = reader.readString();\n\n // Player list\n const playerCount = reader.readInt32LE();\n const players: Player[] = [];\n for (let i = 0; i < playerCount; i++) {\n players.push({\n name: reader.readString(),\n uuid: reader.readUUID(),\n });\n }\n\n // Plugin list\n const pluginCount = reader.readInt32LE();\n const plugins: Plugin[] = [];\n for (let i = 0; i < pluginCount; i++) {\n plugins.push({\n id: reader.readString(),\n version: reader.readString(),\n enabled: reader.readBoolean(),\n });\n }\n\n return {\n serverName,\n motd,\n currentPlayers,\n maxPlayers,\n hostPort,\n version,\n protocolVersion,\n protocolHash,\n players,\n plugins,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/query.ts","../src/protocol.ts"],"sourcesContent":["import dgram from 'node:dgram';\nimport {\n buildRequest,\n parseBasicResponse,\n parseFullResponse,\n TYPE_BASIC,\n TYPE_FULL,\n} from './protocol.js';\nimport type { QueryOptions, ServerInfo, ServerInfoFull } from './types.js';\n\nconst DEFAULT_TIMEOUT = 5000;\n\n/**\n * Send a UDP query and wait for response.\n */\nfunction sendQuery(\n host: string,\n port: number,\n type: number,\n timeout: number\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const socket = dgram.createSocket('udp4');\n let timeoutHandle: NodeJS.Timeout;\n let closed = false;\n\n const cleanup = () => {\n if (closed) return;\n closed = true;\n clearTimeout(timeoutHandle);\n try {\n socket.close();\n } catch {\n // Already closed\n }\n };\n\n socket.on('message', (msg) => {\n cleanup();\n resolve(msg);\n });\n\n socket.on('error', (err) => {\n cleanup();\n reject(err);\n });\n\n timeoutHandle = setTimeout(() => {\n cleanup();\n reject(new Error(`Query timeout after ${timeout}ms`));\n }, timeout);\n\n const request = buildRequest(type);\n socket.send(request, port, host, (err) => {\n if (err) {\n cleanup();\n reject(err);\n }\n });\n });\n}\n\n/**\n * Query a Hytale server for information.\n *\n * @param host - Server hostname or IP address\n * @param port - Server port (default: 5520)\n * @param options - Query options\n * @returns Server information (full if options.full is true)\n *\n * @example\n * ```typescript\n * // Basic query\n * const info = await query('play.example.com', 5520);\n * console.log(`${info.serverName}: ${info.currentPlayers}/${info.maxPlayers}`);\n *\n * // Full query (includes players + plugins)\n * const full = await query('play.example.com', 5520, { full: true });\n * console.log('Players:', full.players.map(p => p.name).join(', '));\n * ```\n */\nexport async function query(\n host: string,\n port?: number,\n options?: QueryOptions & { full?: false }\n): Promise<ServerInfo>;\nexport async function query(\n host: string,\n port: number,\n options: QueryOptions & { full: true }\n): Promise<ServerInfoFull>;\nexport async function query(\n host: string,\n port = 5520,\n options: QueryOptions & { full?: boolean } = {}\n): Promise<ServerInfo | ServerInfoFull> {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n const type = options.full ? TYPE_FULL : TYPE_BASIC;\n const response = await sendQuery(host, port, type, timeout);\n\n if (options.full) {\n return parseFullResponse(response);\n }\n return parseBasicResponse(response);\n}\n","import type { Player, Plugin, ServerInfo, ServerInfoFull } from './types.js';\n\n// Protocol constants\nexport const REQUEST_MAGIC = Buffer.from('HYQUERY\\0', 'ascii');\nexport const RESPONSE_MAGIC = Buffer.from('HYREPLY\\0', 'ascii');\nexport const TYPE_BASIC = 0x00;\nexport const TYPE_FULL = 0x01;\n\n/**\n * Build a query request packet.\n */\nexport function buildRequest(type: number): Buffer {\n const buf = Buffer.alloc(REQUEST_MAGIC.length + 1);\n REQUEST_MAGIC.copy(buf, 0);\n buf[REQUEST_MAGIC.length] = type;\n return buf;\n}\n\n/**\n * Buffer reader helper for parsing responses.\n */\nclass BufferReader {\n private offset = 0;\n\n constructor(private buf: Buffer) {}\n\n readBytes(length: number): Buffer {\n const slice = this.buf.subarray(this.offset, this.offset + length);\n this.offset += length;\n return slice;\n }\n\n readUInt16LE(): number {\n const value = this.buf.readUInt16LE(this.offset);\n this.offset += 2;\n return value;\n }\n\n readInt32LE(): number {\n const value = this.buf.readInt32LE(this.offset);\n this.offset += 4;\n return value;\n }\n\n readBigInt64BE(): bigint {\n const value = this.buf.readBigInt64BE(this.offset);\n this.offset += 8;\n return value;\n }\n\n readBoolean(): boolean {\n return this.buf[this.offset++] !== 0;\n }\n\n readString(): string {\n const length = this.readUInt16LE();\n const bytes = this.readBytes(length);\n return bytes.toString('utf8');\n }\n\n readUUID(): string {\n const msb = this.readBigInt64BE();\n const lsb = this.readBigInt64BE();\n return formatUUID(msb, lsb);\n }\n\n get remaining(): number {\n return this.buf.length - this.offset;\n }\n}\n\n/**\n * Format UUID from most/least significant bits.\n */\nfunction formatUUID(msb: bigint, lsb: bigint): string {\n const toHex = (n: bigint): string => {\n if (n < 0n) {\n n = BigInt.asUintN(64, n);\n }\n return n.toString(16).padStart(16, '0');\n };\n\n const hex = toHex(msb) + toHex(lsb);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/**\n * Validate response magic bytes.\n */\nexport function validateResponse(buf: Buffer): boolean {\n if (buf.length < RESPONSE_MAGIC.length + 1) {\n return false;\n }\n return buf.subarray(0, RESPONSE_MAGIC.length).equals(RESPONSE_MAGIC);\n}\n\n/**\n * Parse a basic query response.\n */\nexport function parseBasicResponse(buf: Buffer): ServerInfo {\n if (!validateResponse(buf)) {\n throw new Error('Invalid response: magic mismatch');\n }\n\n const reader = new BufferReader(buf);\n\n // Skip magic\n reader.readBytes(RESPONSE_MAGIC.length);\n\n // Skip type\n reader.readBytes(1);\n\n return {\n serverName: reader.readString(),\n motd: reader.readString(),\n currentPlayers: reader.readInt32LE(),\n maxPlayers: reader.readInt32LE(),\n hostPort: reader.readUInt16LE(),\n version: reader.readString(),\n protocolVersion: reader.readInt32LE(),\n protocolHash: reader.readString(),\n };\n}\n\n/**\n * Parse a full query response.\n */\nexport function parseFullResponse(buf: Buffer): ServerInfoFull {\n if (!validateResponse(buf)) {\n throw new Error('Invalid response: magic mismatch');\n }\n\n const reader = new BufferReader(buf);\n\n // Skip magic\n reader.readBytes(RESPONSE_MAGIC.length);\n\n // Skip type\n reader.readBytes(1);\n\n // Base info\n const serverName = reader.readString();\n const motd = reader.readString();\n const currentPlayers = reader.readInt32LE();\n const maxPlayers = reader.readInt32LE();\n const hostPort = reader.readUInt16LE();\n const version = reader.readString();\n const protocolVersion = reader.readInt32LE();\n const protocolHash = reader.readString();\n\n // Player list\n const playerCount = reader.readInt32LE();\n const players: Player[] = [];\n for (let i = 0; i < playerCount; i++) {\n players.push({\n name: reader.readString(),\n uuid: reader.readUUID(),\n });\n }\n\n // Plugin list\n const pluginCount = reader.readInt32LE();\n const plugins: Plugin[] = [];\n for (let i = 0; i < pluginCount; i++) {\n plugins.push({\n id: reader.readString(),\n version: reader.readString(),\n enabled: reader.readBoolean(),\n });\n }\n\n return {\n serverName,\n motd,\n currentPlayers,\n maxPlayers,\n hostPort,\n version,\n protocolVersion,\n protocolHash,\n players,\n plugins,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hytaleone/query",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Query Hytale servers using the UDP query protocol",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",