typescript-a2s
v1.0.1
Published
TypeScript library to query Source and GoldSource servers using Valve's Server Query Protocol
Maintainers
Readme
TypeScript A2S
ALL CODE GENERATED BY VIBE CODING
A modern, fully-typed TypeScript library for querying Source and GoldSource game servers using Valve's A2S (Application-level Server Query) protocol. This is a complete TypeScript/Node.js implementation inspired by the original python-a2s library.
Features
- 🚀 Full TypeScript Support: Complete type definitions with generics and strict typing
- 🎯 Comprehensive Queries: Server info, player lists, and server rules/configuration
- 🔧 Multi-Engine Support: Both Source Engine and GoldSource Engine compatibility
- 📦 Modern Build System: Built with Vite for optimal performance and tree-shaking
- 🌐 Universal Compatibility: ESM and CommonJS support for all Node.js environments
- 🛡️ Robust Error Handling: Custom exception types with detailed error information
- ⚡ Async/Promise API: Modern Promise-based interface with async/await support
- 🔤 Flexible Encoding: UTF-8, Latin1, or raw Buffer support for international servers
- 🧩 Advanced Utilities: ByteReader/ByteWriter for custom protocol implementations
- 🔀 Multi-packet Support: Automatic handling of fragmented server responses
- 🗜️ Compression Support: Built-in bz2 decompression for compressed responses
- 🧪 Fully Tested: Comprehensive test suite with 100% coverage
Installation
npm install typescript-a2s
# or
yarn add typescript-a2s
# or
pnpm add typescript-a2sQuick Start
import { info, players, rules } from 'typescript-a2s';
// Query server information
const serverInfo = await info('127.0.0.1', 27015);
console.log(`Server: ${serverInfo.serverName}`);
console.log(`Map: ${serverInfo.mapName}`);
console.log(`Players: ${serverInfo.playerCount}/${serverInfo.maxPlayers}`);
console.log(`Ping: ${serverInfo.ping}ms`);
// Query connected players
const playerList = await players('127.0.0.1', 27015);
playerList.forEach((player) => {
const duration = Math.floor(player.duration / 60);
console.log(`${player.name}: ${player.score} points (${duration}m)`);
});
// Query server configuration
const serverRules = await rules('127.0.0.1', 27015);
console.log(
`Server has ${Object.keys(serverRules).length} configuration rules`
);API Reference
Core Functions
info(address: string, port: number, timeout?: number, encoding?: string | null): Promise<SourceInfo | GoldSrcInfo>
Queries comprehensive server information including name, map, player counts, and technical details.
Parameters:
address- Server IP address or hostnameport- Server query port (usually game port + 1)timeout- Request timeout in seconds (default: 3.0)encoding- String encoding:'utf-8','latin1', ornullfor raw Buffer (default:'utf-8')
Returns: Promise resolving to engine-specific server information
players(address: string, port: number, timeout?: number, encoding?: string | null): Promise<Player[]>
Retrieves a list of currently connected players with scores and connection times.
Parameters: Same as info()
Returns: Promise resolving to an array of player objects
rules(address: string, port: number, timeout?: number, encoding?: string | null): Promise<Rules>
Fetches server configuration variables and custom settings.
Parameters: Same as info()
Returns: Promise resolving to a key-value object of server rules
Advanced Client Class
A2SClient
Advanced client class for persistent connections and batch queries.
import { A2SClient } from 'typescript-a2s';
const client = new A2SClient('127.0.0.1', 27015, 5000, 'utf-8');
try {
// Do NOT use Promise.all with a single A2SClient instance!
// The A2S protocol and UDP socket are not concurrency-safe.
// Always await each query sequentially to avoid protocol errors.
const serverInfo = await client.info();
const playerList = await client.players();
const serverRules = await client.rules();
console.log('Query results:', {
server: serverInfo.serverName,
players: playerList.length,
rules: Object.keys(serverRules).length,
});
} finally {
client.close(); // Always clean up resources
}Constructor Parameters:
address- Server IP address or hostnameport- Server query porttimeout- Request timeout in milliseconds (default: 3000)encoding- String encoding (default:'utf-8')
Type Definitions
SourceInfo - Source Engine Servers
interface SourceInfo {
protocol: number;
serverName: string | Buffer;
mapName: string | Buffer;
folder: string | Buffer;
game: string | Buffer;
appId: number;
playerCount: number;
maxPlayers: number;
botCount: number;
serverType: string | Buffer;
platform: string | Buffer;
passwordProtected: boolean;
vacEnabled: boolean;
version: string | Buffer;
edf: number;
ping: number;
// Optional extended fields
port?: number;
steamId?: bigint;
stvPort?: number;
stvName?: string | Buffer;
keywords?: string | Buffer;
gameId?: bigint;
}GoldSrcInfo - GoldSource Engine Servers
interface GoldSrcInfo {
address: string | Buffer;
serverName: string | Buffer;
mapName: string | Buffer;
folder: string | Buffer;
game: string | Buffer;
playerCount: number;
maxPlayers: number;
protocol: number;
serverType: string | Buffer;
platform: string | Buffer;
passwordProtected: boolean;
isMod: boolean;
vacEnabled: boolean;
botCount: number;
ping: number;
// Optional mod information
modWebsite?: string | Buffer;
modDownload?: string | Buffer;
modVersion?: number;
modSize?: number;
multiplayerOnly?: boolean;
usesCustomDll?: boolean;
}Player<T> - Player Information
interface Player<T = string> {
index: number; // Player slot index
name: T; // Player name (string or Buffer based on encoding)
score: number; // Player score/kills
duration: number; // Connection time in seconds
}Rules<T> - Server Configuration
type Rules<T = string> = Record<string, T>;Exception Handling
Custom Exception Types
// Protocol-level errors
class BrokenMessageError extends Error {
// Thrown when server response is malformed or invalid
}
// Buffer operation errors
class BufferExhaustedError extends BrokenMessageError {
// Thrown when trying to read beyond buffer boundaries
}Error Handling Example
import { info, BrokenMessageError, BufferExhaustedError } from 'typescript-a2s';
try {
const serverInfo = await info('127.0.0.1', 27015);
console.log(serverInfo);
} catch (error) {
if (error instanceof BrokenMessageError) {
console.error('Server returned invalid data:', error.message);
} else if (error instanceof BufferExhaustedError) {
console.error('Data parsing error:', error.message);
} else if (error.code === 'ECONNREFUSED') {
console.error('Server is offline or unreachable');
} else if (error.code === 'ETIMEDOUT') {
console.error('Query timed out - server may be overloaded');
} else {
console.error('Unexpected error:', error);
}
}Advanced Utilities
Binary Data Processing
import { ByteReader, ByteWriter } from 'typescript-a2s';
// Reading binary data
const reader = new ByteReader(buffer, true, 'utf-8');
const value = reader.readUint32();
const text = reader.readString();
// Writing binary data
const writer = new ByteWriter();
writer.writeUint32(12345);
writer.writeString('Hello World');
const result = writer.toBuffer();Constants and Defaults
import {
DEFAULT_TIMEOUT, // 3.0 seconds
DEFAULT_ENCODING, // 'utf-8'
DEFAULT_RETRIES, // 5 attempts
} from 'typescript-a2s';Usage Examples
Basic Server Monitoring
import { info, players, rules } from 'typescript-a2s';
async function monitorServer(address: string, port: number) {
try {
// This is safe because info() and players() are stateless functions.
// Do NOT use Promise.all with a single A2SClient instance!
const [serverInfo, playerList] = await Promise.all([
info(address, port),
players(address, port),
]);
console.log(`📊 ${serverInfo.serverName}`);
console.log(`🗺️ Map: ${serverInfo.mapName}`);
console.log(
`👥 Players: ${serverInfo.playerCount}/${serverInfo.maxPlayers}`
);
console.log(`🏓 Ping: ${(serverInfo.ping * 1000).toFixed(1)}ms`);
if (playerList.length > 0) {
console.log('\n🎮 Top Players:');
playerList
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.forEach((player, i) => {
const time = Math.floor(player.duration / 60);
console.log(
` ${i + 1}. ${player.name} - ${player.score} (${time}m)`
);
});
}
} catch (error) {
console.error(`❌ Failed to query ${address}:${port}`, error.message);
}
}
await monitorServer('127.0.0.1', 27015);Multi-Server Batch Queries
import { A2SClient } from 'typescript-a2s';
const servers = [
{ name: 'Server 1', host: '127.0.0.1', port: 27015 },
{ name: 'Server 2', host: '127.0.0.1', port: 27016 },
{ name: 'Server 3', host: '127.0.0.1', port: 27017 },
];
async function queryMultipleServers() {
const results = await Promise.allSettled(
servers.map(async (server) => {
const client = new A2SClient(server.host, server.port, 3000);
try {
const info = await client.info();
return {
server: server.name,
name: info.serverName,
players: info.playerCount,
maxPlayers: info.maxPlayers,
ping: info.ping,
};
} finally {
client.close();
}
})
);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
const data = result.value;
console.log(
`✅ ${data.server}: ${data.players}/${data.maxPlayers} players`
);
} else {
console.log(`❌ ${servers[index].name}: ${result.reason.message}`);
}
});
}
await queryMultipleServers();Encoding and Internationalization
import { info } from 'typescript-a2s';
async function testEncodings(address: string, port: number) {
// UTF-8 encoding (default)
const utf8Info = await info(address, port, 3000, 'utf-8');
console.log('UTF-8 server name:', utf8Info.serverName);
// Latin1 encoding for older servers
const latin1Info = await info(address, port, 3000, 'latin1');
console.log('Latin1 server name:', latin1Info.serverName);
// Raw binary data (no string decoding)
const rawInfo = await info(address, port, 3000, null);
console.log('Raw server name buffer:', rawInfo.serverName);
// Convert raw buffer to string manually if needed
if (Buffer.isBuffer(rawInfo.serverName)) {
const decoded = rawInfo.serverName.toString('utf-8');
console.log('Manually decoded:', decoded);
}
}
await testEncodings('127.0.0.1', 27015);Real-time Server Monitoring
import { info } from 'typescript-a2s';
class ServerMonitor {
private interval: NodeJS.Timeout | null = null;
private lastPlayerCount = -1;
async start(address: string, port: number, intervalMs = 10000) {
console.log(`🔄 Starting monitor for ${address}:${port}`);
this.interval = setInterval(async () => {
try {
const serverInfo = await info(address, port, 2000);
if (serverInfo.playerCount !== this.lastPlayerCount) {
const timestamp = new Date().toLocaleTimeString();
const change =
this.lastPlayerCount === -1
? ''
: ` (${serverInfo.playerCount > this.lastPlayerCount ? '+' : ''}${
serverInfo.playerCount - this.lastPlayerCount
})`;
console.log(
`[${timestamp}] ${serverInfo.serverName}: ` +
`${serverInfo.playerCount}/${serverInfo.maxPlayers}${change}`
);
this.lastPlayerCount = serverInfo.playerCount;
}
} catch (error) {
console.error(
`[${new Date().toLocaleTimeString()}] Error:`,
error.message
);
}
}, intervalMs);
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
console.log('🛑 Monitor stopped');
}
}
}
const monitor = new ServerMonitor();
await monitor.start('127.0.0.1', 27015);
// Stop monitoring on Ctrl+C
process.on('SIGINT', () => {
monitor.stop();
process.exit(0);
});Important Notes & Best Practices
Server Behavior Variations
- Data Consistency: Some servers may return inconsistent or incomplete data. Always validate critical fields before use.
- Player Count Discrepancies: The
playerCountin server info may not exactly match the length of the players array due to server implementation differences. - Port Configuration: Query ports often differ from game connection ports. Common patterns include game port + 1, or separate configured query ports.
Protocol Limitations
- Player Limits: The A2S protocol cannot return more than 255 players due to byte field limitations.
- Unicode Support: String encoding varies by server configuration. Use appropriate encoding settings for international servers.
- Rate Limiting: This library does not implement rate limiting. Implement appropriate delays between requests to avoid being blocked.
Performance Considerations
- Connection Management: Use
A2SClientclass for multiple queries to the same server to reuse connections. - Timeout Tuning: Adjust timeout values based on network conditions and server responsiveness.
- Error Handling: Always implement proper error handling, especially for production monitoring systems.
Supported Games & Engines
Source Engine Games
- Half-Life 2
- Team Fortress 2
- Counter-Strike: Global Offensive
- Counter-Strike: Source
- Left 4 Dead 2
- Portal 2
- Garry's Mod
- Day of Defeat: Source
GoldSource Engine Games
- Half-Life
- Counter-Strike 1.6
- Day of Defeat
- Team Fortress Classic
- Ricochet
Development
Project Setup
# Clone the repository
git clone https://github.com/Yepoleb/python-a2s.git
cd python-a2s/typescript-a2s
# Install dependencies
pnpm install
# Run development server
pnpm devBuild Commands
# Build for production
pnpm build
# Generate type declarations
pnpm build:types
# Run tests
pnpm test
# Run tests with UI
pnpm test:ui
# Generate coverage report
pnpm test:coverage
# Lint code
pnpm lint
# Fix linting issues
pnpm lint:fix
# Run example
pnpm exampleProject Structure
typescript-a2s/
├── src/
│ ├── a2s.ts # Core client implementation
│ ├── socket.ts # UDP network layer
│ ├── protocols.ts # A2S protocol handlers
│ ├── fragment.ts # Multi-packet response handling
│ ├── byteio.ts # Binary data processing
│ ├── types.ts # TypeScript definitions
│ ├── exceptions.ts # Error classes
│ ├── defaults.ts # Configuration constants
│ ├── index.ts # Public API exports
│ └── __tests__/ # Test suite
├── examples/ # Usage examples
├── dist/ # Built output
└── docs/ # DocumentationRequirements
- Node.js: 18.0.0 or higher
- TypeScript: 5.0.0 or higher (for development)
- Dependencies: Zero runtime dependencies
License
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please read CONTRIBUTING.md for guidelines.
Related Projects
- python-a2s - Original Python implementation
Acknowledgments
- Based on the excellent python-a2s library by Yepoleb
- Valve Corporation for the A2S protocol specification
- The TypeScript and Node.js communities for excellent tooling
